每当有大公司的基础设施相关文章发布时,Hacker News 上总会出现一堆评论,内容大多是这样的变体:“当然了,他们用Kubernetes撑起了47个微服务,还加了一套自主开发的分布式共识协议数据库。”然而,当真相是他们只是用单纯的PostgreSQL主库和一点使用规范支撑其业务时,评论区就会陷入一片尴尬的沉默。
这次,OpenAI就再次打了这样一个大脸。
超出所有人想象的数字
OpenAI基础设施工程师Bohan Zhang刚刚分享了他们如何用PostgreSQL支持ChatGPT的具体细节。令人惊讶的数据如下:
- 8亿用户
- 单一PostgreSQL主库(写入操作专用)部署在Azure上
- ~50个只读副本
- 每秒百万次查询
- p99延迟仅10-19毫秒
- 99.999%的可用性
- 一年内仅出现一次SEV-0事故 (而且还是因为ImageGen产品的病毒式传播,让一周之内新增了1亿用户)
再读一遍。一个。主库。支撑8亿用户。
“可是他们为什么不分片?”
不需要。背后的原因非常简单而务实。
给PostgreSQL分片需要改动数百个应用的端点。每一个默认假设所有数据都在同一个数据库查询——基本是所有查询——都需要重写,来判断每个数据属于哪个分片。
这么迁移需要付出的成本呢?是数月的工作量、新的bug层出不穷,再加上一个混乱的迁移过渡期——同时需要维护老旧和新系统。
于是他们采用了另一种方式:识别最占用写入负载的数据,然后将其移至Cosmos DB。而这样做的原因并不是因为Cosmos比PostgreSQL更好,而是因为这些特定的工作负载更适合文档数据库模型。而其他大部分业务逻辑依旧保留在PostgreSQL中。
用大白话说:他们没有让整个系统变复杂,而是精确识别出问题所在,并有针对性地解决它。精密手术刀式的调整,而不是拿电锯一通乱砍。
PgBouncer:将连接延迟从50毫秒降到5毫秒
他们遇到的第一大瓶颈是建立连接的延迟。PostgreSQL会为每个新连接创建一个独立进程。而随着来自成百上千个应用Pods的并发连接数增加,光是处理新连接的开销就已达到50毫秒——甚至还没开始执行查询呢。
他们的解决方案是:使用PgBouncer作为连接池。PgBouncer会维护一个已经建立好的连接池,复用这些连接。结果是连接延迟从50毫秒降至5毫秒,直接减少了90%的延迟。仅仅通过更换一项底层工具,问题就得到了解决。
值得提到的是,这根本不是什么新技术。PgBouncer已经有15年以上的历史,并一直被各种规模的企业用于生产环境中。然而,它再次证明,一款久经考验的不起眼的工具,解决了这个地球上使用最频繁应用之一的问题。
那个做了12个表联接的ORM
这个问题是我的最爱。我见过它出现在学生的项目、初创公司,甚至银行的系统里。到处都有。
他们的ORM生成了包含12个表联合查询(join)的SQL语句。罪魁祸首并不是哪个设计人员,而是因为数据模型过于复杂且关系交织,ORM顺着这些关系“不假思索”地将每个可能的相关表都加载了进来。
解决办法既不是换ORM,也不是手动改写所有的查询。他们选择了将一些逻辑转移到应用层。与其让PostgreSQL执行一个庞大的join操作,他们分拆成多个简单查询,然后用代码对数据进行整合。
这么做优雅吗?的确没那么优雅。速度快吗?快得多。因为PostgreSQL处理简单查询比处理含有交叉条件的12表联合查询高效得多。而且,部分结果还能进行缓存和复用。
| |
每一条单独的查询都很简单。查询解析器几微秒内就可以完成。并且一旦其中有一条失败或者变慢,你可以直接找到问题所在。
不为人知的防御措施
读Bohan Zhang的文章最让我惊叹的并不是什么“大数字”,而是那些避免整个系统崩溃的小型防御机制:
idle_in_transaction_session_timeout
如果一个事务未做任何操作却保持打开状态一段时间,PostgreSQL会强制终止它。为什么重要?因为一个开着的事务会阻止autovacuum的运行。而一旦autovacuum停顿,表体积会暴涨,索引会退化,最终你的数据库每天都会变得越来越慢。
这就像冰箱门敞开了5分钟也没关系。但要是开了一整晚,次日早晨开冰箱时,里面就全是常温的食材了。
设置5秒超时时的Schema变更
在PostgreSQL中,执行ALTER TABLE时需要对表加锁。如果当前有长时间事务未完成,这个锁就只能等待。而在等待锁的同时,会阻塞所有新的查询。所以,一次只需200毫秒的迁移可能会因一个老事务导致整个数据库瘫痪。
他们的做法:SET lock_timeout = '5s'。如果迁移在5秒内无法获取锁,直接中止。失败快、重试快,总比等待永远拿不到锁好。
四层速率限制(Rate Limiting)
不是一层,不是两层,而是整整四层速率限制:
- 边缘/CDN层 — 在流量到达应用前就拦截歹意流量
- API网关层 — 针对用户或API密钥设置访问限制
- 应用层 — 按操作类型限制
- 数据库层 — 连接限制和查询超时
每一层过滤那些从上一层漏掉的流量。这是“深度防御”策略。类似于我在对抗幻觉的五大防御中描述的分层保护,只不过这次是针对基础设施的。
按优先级隔离工作负载
并不是所有查询都有同样的重要性。“显示用户聊天记录”的查询至关重要——失败了用户会看到错误页面。而“生成分析报告”的查询虽然重要,但可以等待30秒再完成。
OpenAI根据优先级将查询分流至不同的只读副本。高优先级副本负载较轻,响应速度更快。低优先级副本则可承载更多负载,而不会影响用户体验。
从常识上讲,这再合理不过,但需要执行力。必须分类每条查询、配置分流,并一定要抗住诱惑,不把所有查询都丢到最快的副本上。
长时间的回填操作
要为8亿用户的表新增一列并填充值,绝对不能直接执行UPDATE users SET new_column = computed_value。因为那样会锁住整张表、占满磁盘,并可能直接拖垮主库。
在OpenAI,数据回填操作严格限速执行。这种任务可能需要数周时间才能完成。
这听起来很糟糕?其实恰恰相反。这才是一个深刻理解稳定性重要性的团队所做的明智之选:慢慢完成,不引人注意,比起深夜2点因过载崩溃引发的紧急事故,不知道好了多少!
即将实现的级联复制
目前,他们维护着约50个直接连接到主库的副本。每个副本都会消耗一个同步连接以及主库的带宽。50个的情况下还勉强可以应付,但如果再多就会成为大问题。
他们正在开发的解决方案是:级联复制(Cascading Replication)。副本从其他副本中同步,而不是直接从主库同步。实现一个树状结构取代现有的星型结构。主库将数据发送至5-10个一级副本,而后续这些副本再将数据传递至其他副本。
就像BitTorrent的理念一样。与其让所有设备都从一个服务器下载,不如让节点之间互相共享。这个概念用于盗版电影行得通,也同样适用于PostgreSQL的WAL段。
一堂没人愿意听的课
业界对**过度工程(over-engineering)**有种隐形的依赖症。每周都有新数据库问世,声称能解决大部分公司根本不会遇到的问题。而且每周都有工程团队因为“扩展性更强”或“更现代”而采用这些技术,而从未问过PostgreSQL加上一点使用规范是否也能完成任务。
但OpenAI——这家正在定义AI未来、拥有史上增长速度最快产品之一的公司——用的是PostgreSQL。只有一个主库。没有分片。没有奇异的分布式数据库。
他们用了PgBouncer(发布于2007年)。只读副本(上世纪90年代的概念)。连接池(与关系型数据库一样古老的技术)。速率限制(比我们大多数人都出生得早)。
真正的“魔法”不在于技术,而在于他们的使用纪律:
- 简单查询代替繁杂联接
- 激进的超时设置代替无限等待
- 隔离工作负载而不是“所有任务丢一个服务器”
- 仅迁移确实需要迁移的部分,而不是重写整套系统
下一个晨会时
下次,如果团队中的某位成员建议你迁移到分布式数据库,或者给PostgreSQL分片,或者在API和数据库之间加个队列服务“因为单体架构可能无法扩展”,请将这些数据展示给他看。
8亿用户。一个主库。p99延迟10-19毫秒。99.999%的正常运行时间。
然后问他:“我们的问题真的在于PostgreSQL无法扩展吗?还是在于我们的查询还不够优化?”
几乎每次,答案都会是后者。
来源: Inside the Postgres Setup Powering 800M ChatGPT Users — Bohan Zhang, OpenAI。如果今年只能读一篇基础设施相关的文章,选这篇吧。