一个 PostgreSQL 主库扛住 8 亿用户:OpenAI 的数据库扩展哲学

一个 PostgreSQL 主库扛住 8 亿用户:OpenAI 的数据库扩展哲学

8 亿用户,每秒数百万次查询,99.999% 可用性。你大概会以为这背后是一套极其复杂的分布式数据库集群,横跨几十个分片,搭配自研协调层。

事实是:一个主库,加不到 50 个只读副本。

这就是 OpenAI 在 2026 年 1 月发布的工程博客中披露的 ChatGPT 数据库架构。这篇博客由技术团队成员 Bohan Zhang 撰写,没有炫技式的架构图,也没有"我们重写了 PostgreSQL"式的叙事。它的核心信息非常朴素:在考虑分片之前,先把你手上的数据库压榨到极限。

OpenAI PostgreSQL 架构示意

先理解问题:为什么不能直接分片

很多人的直觉是"用户量到亿级就必须分库分表"。这个直觉不完全错,但忽略了一个关键前提:分片的代价极高。

OpenAI 在博客中解释了为什么没有走分片路线:将现有应用工作负载分片,需要修改数百个应用端点,可能耗时数月甚至数年。他们的工作负载以读为主,当前架构还有充足的余量支撑流量增长。

换句话说,分片不是"到了某个量级就该做"的事,而是"当你穷尽了其他所有优化手段之后才考虑"的终极手段。OpenAI 的选择是:先把 PostgreSQL 本身的能力发挥到极致,只把确实需要分片的高频写入工作负载迁移到 Cosmos DB 这样的原生分片数据库。

这个思路值得每个工程团队思考:在你考虑引入更复杂的架构之前,是否已经做完了所有"无聊但有效"的基础优化?

PostgreSQL 为什么不擅长高并发写入

理解 OpenAI 的优化策略,需要先理解 PostgreSQL 的一个底层机制:MVCC(多版本并发控制)。

MVCC 的设计目标是让读写互不阻塞。当一条记录被更新时,PostgreSQL 不会原地修改数据,而是创建一条全新的行副本,将旧行标记为"死亡"(dead tuple)。这意味着每次 UPDATE 操作实际上是一次 INSERT 加一次标记删除。

这个设计在读多写少的场景下表现优异,但在高并发写入时会暴露三个问题:

  1. 写放大:一次逻辑更新在物理层面产生大量新数据,包括表行和所有相关索引条目
  2. 存储膨胀:死元组不断累积,导致表和索引膨胀,查询需要扫描更多页面
  3. 清理开销:VACUUM 进程需要定期扫描并回收死元组占用的空间,这本身又会消耗 CPU 和 I/O 资源

ChatGPT 的场景恰好是读多写少,所以 MVCC 对 OpenAI 的核心负载影响有限。但新功能发布时的写入峰值仍然是一个真实挑战,这也是 OpenAI 将部分写入密集型工作负载迁移到 Cosmos DB 的原因。

OpenAI 做了哪些优化

OpenAI 的博客将这些优化分为几个层面,每个都谈不上什么黑科技,但组合在一起产生了巨大效果。

减少主库写入压力

他们首先排查并修复了应用层导致冗余写入的代码缺陷。接下来引入了延迟写入(lazy writes)策略:不需要实时持久化的操作改为异步批量写入。这减少了对主库的瞬时写入压力。同时实施了严格的基础设施级限流,防止突发流量击穿数据库。

查询优化

这一部分的细节最能引起后端工程师的共鸣。OpenAI 发现了一条连接了 12 张表的多表 JOIN 查询,极其消耗 CPU。他们的解决方案是把 JOIN 逻辑上移到应用层,拆解为多条独立的 SELECT 查询。这些拆分后的查询可以走只读副本或缓存,大幅减轻主库负担。

此外,他们还审计了 ORM 生成的 SQL 查询。ORM 框架生成的查询往往不够高效,经典的 N+1 查询问题、查询计划器没有选择最优索引等问题,在高并发场景下会被放大。对 ORM 查询的逐条审计和手动重写,是数据库性能优化中常被忽视但收益极高的工作。

单点故障消除

只有一个主库意味着它是整个系统的单点故障。OpenAI 的策略有两层:

  • 读写分离:将所有读取流量导向只读副本。主库宕机时,系统仍然可以提供读服务,只是写入暂时不可用。这把"完全宕机"降级为"部分可用"。
  • 热备切换:部署一个不承载流量的热备节点(hot standby),通过同步复制保持数据一致。主库故障时热备节点立即接管,实现自动故障切换。

工作负载隔离

"吵闹邻居"问题在共享基础设施中很常见:一个高负载的查询可能拖慢整个数据库实例上的其他所有请求。OpenAI 的做法是将不同的业务用例路由到特定的只读副本,确保一个用例的查询峰值不会影响其他用例的服务质量。

连接池

PostgreSQL 的连接模型与 MySQL 不同:每个客户端连接对应一个独立的操作系统进程,而不是线程。进程的创建和销毁开销远高于线程,这使得 PostgreSQL 在高并发连接场景下容易出现连接数耗尽的问题。

几乎所有的 PostgreSQL 生产部署都会在数据库前面放一个 PgBouncer 做连接池化。PgBouncer 维护一组到数据库的持久连接,客户端连接到 PgBouncer 而非数据库本身。这样即使有上千个客户端连接,实际到数据库的活跃连接可能只有几十个,连接建立延迟从数十毫秒降到几毫秒。

缓存击穿防护

缓存是读多写少场景的标准配置,但缓存失效时会引发"缓存击穿":大量请求同时穿透缓存涌向数据库。OpenAI 采用了一种叫做 cache locking 的技术,CDN 领域也广泛使用类似机制(通常称为 request hedging 或 single-flight)。

核心思路是:当缓存未命中时,只有一个请求去数据库回源加载数据,其他请求等待这个请求完成后共享结果。这样即使出现大规模缓存未命中,实际打到数据库的查询也只有一条,而非 N 条。

还能继续扩展吗:级联复制的未来

50 个只读副本听起来很多,但 PostgreSQL 的流复制机制存在一个物理瓶颈:主库需要向每个副本单独发送 WAL(预写日志)数据流。副本越多,主库的 WAL 发送开销越大。

Azure PostgreSQL Flexible Server 默认只允许主库直连 5 个副本。OpenAI 目前已经运行近 50 个,这得益于 Azure 在基础设施层的优化和超大实例规格。但他们也知道这条路有尽头。

OpenAI 正在与 Azure 团队合作实现级联复制(cascading replication)。在这种树形拓扑中,主库只向少数几个中间副本发送 WAL,每个中间副本再向下游副本转发。这样主库的复制负载不再随副本总数线性增长,理论上可以扩展到 100 个以上副本。

代价是额外的运维复杂度,尤其是故障切换管理:当中间副本故障时,需要确保其下游的所有副本都能正确切换数据源。OpenAI 表示这个功能仍在测试中,在确认故障切换的安全性之前不会投入生产。

对普通工程团队的启示

OpenAI 的经验并非"大厂才能做的事"。其中大部分优化策略适用于任何使用 PostgreSQL 的团队:

  • 先审计 ORM 查询,再考虑分库分表。很多性能问题的根源是低效的查询,而非数据库本身的能力上限
  • 把 JOIN 拆到应用层不是退步,而是用可控的多次查询换取灵活的路由和缓存能力
  • PgBouncer 应该是默认配置,而非"出了连接问题之后才加"
  • 缓存击穿防护用几行代码就能实现(single-flight 模式),但能在关键时刻挽救数据库
  • 延迟写入是一个被低估的优化手段:如果数据不需要实时可见,为什么要同步写入?

当你面临数据库扩展问题时,最该问的第一个问题可能不是"要不要分片",而是"我们是否已经用好了现有的工具"。

相关推荐