每当有大公司的基础设施相关文章发布时,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表联合查询高效得多。而且,部分结果还能进行缓存和复用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 之前:ORM生成的SQL
SELECT u.*, p.*, s.*, t.*, ...
FROM users u
JOIN profiles p ON ...
JOIN settings s ON ...
JOIN teams t ON ...
JOIN ... -- 总计12个表
WHERE u.id = $1;

-- 之后:语句被拆分,逻辑移到应用层
SELECT * FROM users WHERE id = $1;
SELECT * FROM profiles WHERE user_id = $1;
-- 可缓存、可并行、可调试

每一条单独的查询都很简单。查询解析器几微秒内就可以完成。并且一旦其中有一条失败或者变慢,你可以直接找到问题所在。

不为人知的防御措施

读Bohan Zhang的文章最让我惊叹的并不是什么“大数字”,而是那些避免整个系统崩溃的小型防御机制:

idle_in_transaction_session_timeout

如果一个事务未做任何操作却保持打开状态一段时间,PostgreSQL会强制终止它。为什么重要?因为一个开着的事务会阻止autovacuum的运行。而一旦autovacuum停顿,表体积会暴涨,索引会退化,最终你的数据库每天都会变得越来越慢。

这就像冰箱门敞开了5分钟也没关系。但要是开了一整晚,次日早晨开冰箱时,里面就全是常温的食材了。

设置5秒超时时的Schema变更

在PostgreSQL中,执行ALTER TABLE时需要对表加锁。如果当前有长时间事务未完成,这个锁就只能等待。而在等待锁的同时,会阻塞所有新的查询。所以,一次只需200毫秒的迁移可能会因一个老事务导致整个数据库瘫痪。

他们的做法:SET lock_timeout = '5s'。如果迁移在5秒内无法获取锁,直接中止。失败快、重试快,总比等待永远拿不到锁好。

四层速率限制(Rate Limiting)

不是一层,不是两层,而是整整四层速率限制:

  1. 边缘/CDN层 — 在流量到达应用前就拦截歹意流量
  2. API网关层 — 针对用户或API密钥设置访问限制
  3. 应用层 — 按操作类型限制
  4. 数据库层 — 连接限制和查询超时

每一层过滤那些从上一层漏掉的流量。这是“深度防御”策略。类似于我在对抗幻觉的五大防御中描述的分层保护,只不过这次是针对基础设施的。

按优先级隔离工作负载

并不是所有查询都有同样的重要性。“显示用户聊天记录”的查询至关重要——失败了用户会看到错误页面。而“生成分析报告”的查询虽然重要,但可以等待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。如果今年只能读一篇基础设施相关的文章,选这篇吧。