PostgreSQL 大规模扩展实战
讨论数据库性能优化和架构扩展方案。与 AI 编程关系有限,但对全栈开发者有参考价值。
讨论数据库性能优化和架构扩展方案。与 AI 编程关系有限,但对全栈开发者有参考价值。
在 PGConf.dev 2025 全球开发者大会上,来自 OpenAI 的 Bohan Zhang 分享了 OpenAI 在 PostgreSQL 方面的最佳实践,让人们得以一窥这家最知名独角兽公司的数据库使用情况。
Bohan Zhang 是 OpenAI 基础设施团队的成员。他在卡内基梅隆大学师从 Andy Pavlo 教授,并与他联合创办了 OtterTune。
PostgreSQL 是支撑 OpenAI 大多数关键系统的核心数据库。如果 PostgreSQL 宕机,OpenAI 的许多关键服务都会直接受到影响。过去曾出现过多次 PostgreSQL 相关问题导致 ChatGPT 中断的情况。
OpenAI 在 Azure(Azure Database for PostgreSQL)上使用托管数据库,采用经典的 PostgreSQL 主从复制架构,不使用分片。该设置由一个主数据库和数十个副本组成。对于拥有数百万活跃用户的 OpenAI 这样的服务,可扩展性是一个重大问题。
在 OpenAI 的主从 PostgreSQL 架构中,读取可扩展性很好。然而,"写入请求"已经成为主要瓶颈。OpenAI 在这方面实施了众多优化,例如尽可能多地卸载写入负载,以及避免向主数据库添加新服务。
PostgreSQL 的多版本并发控制(MVCC)设计存在一些已知问题,包括表和索引臃肿。调整自动垃圾回收(清理)可能很复杂,因为每个写操作都会生成一个完整的新版本,索引访问可能需要额外的可见性检查。这些设计方面在扩展读副本时带来了挑战:例如,增加的预写日志(WAL)可能导致更大的复制延迟,随着副本数量的显著增长,网络带宽可能成为新的瓶颈。
为了解决这些问题,我们在多个方面进行了努力:
第一项优化涉及平滑处理主数据库上的写入峰值,以最小化其负载。例如:
卸载所有可能的写入操作。
在应用层避免不必要的写入。
使用延迟写入来平滑写入突发。
在数据回填过程中控制频率。
此外,OpenAI 努力将尽可能多的读取请求卸载到副本。对于因为是读写事务的一部分而无法从主数据库中移除的读取请求,需要高效率。
第二项优化专注于查询层。由于长事务可能阻碍垃圾回收并消耗资源,配置了超时以避免长期的"事务中闲置"会话,在会话、语句和客户端级别都设置了超时。此外,复杂的多连接查询已被优化。演讲还特别提到使用 ORM 容易导致低效查询,应谨慎使用。
主数据库是单点故障;如果它宕机,写操作无法进行。相比之下,我们有许多只读副本;如果其中一个失败,应用仍然可以从其他副本读取。事实上,许多关键请求都是只读的,所以即使主数据库失败,它们仍然可以从只读副本继续读取。
此外,我们区分了低优先级和高优先级请求。对于高优先级请求,OpenAI 分配专用的只读副本,以防止它们受到低优先级请求的影响。
第四项措施是只允许在该集群上进行轻量级架构更改。这意味着:
不允许创建新表或引入新工作负载。
允许添加或删除列(5 秒超时),但不允许任何需要完整表重写的操作。
允许创建或删除索引,但必须使用 CONCURRENTLY 选项。
提到的另一个问题是,操作期间长时间运行的查询(>1s)可能持续阻止架构更改,最终导致它们失败。解决方案是让应用优化或将这些查询卸载到只读副本,以便主数据库上的架构更改不会被阻止。
扩展了 Azure 托管的 PostgreSQL 以在整个集群中处理数百万 QPS(合并的读写),支持 OpenAI 的关键服务。
添加了数十个副本而不增加复制延迟。
在不同地理区域部署只读副本,同时保持低延迟。
在过去九个月中仅经历了一次 PostgreSQL 相关的 SEV0 事件。
为未来增长预留了充足的容量。
OpenAI 还分享了遇到的几个问题的案例研究:
第一个案例涉及缓存故障导致的级联效应。
第二个事件特别有趣:在极高 CPU 使用率下,触发了一个 bug,即使在 CPU 水平恢复正常后,WALSender 进程继续在循环中运行,而不是正确地将 WAL 日志发送到副本,导致复制延迟增加。
第二个事件特别有趣:在极高 CPU 使用率下,触发了一个 bug,即使在 CPU 水平恢复正常后,WALSender 进程继续在循环中运行,而不是正确地将 WAL 日志发送到副本,导致复制延迟增加。
最后,Bohan 向 PostgreSQL 开发者社区提出了几个问题和功能请求:
关于索引管理:未使用的索引会导致写放大和额外的维护开销。OpenAI 希望移除不必要的索引,但为了最小化风险,他们提议为索引添加"禁用"功能。这将允许监控性能指标,以在永久删除索引之前确保稳定性。
关于索引管理:未使用的索引会导致写放大和额外的维护开销。OpenAI 希望移除不必要的索引,但为了最小化风险,他们提议为索引添加"禁用"功能。这将允许监控性能指标,以在永久删除索引之前确保稳定性。
关于可观测性:目前,pg_stat_statements 仅提供每种查询类型的平均响应时间,缺乏对 p95 和 p99 延迟指标的直接访问。他们希望获得更多类似直方图和百分位延迟的指标。
关于可观测性:目前,pg_stat_statements 仅提供每种查询类型的平均响应时间,缺乏对 p95 和 p99 延迟指标的直接访问。他们希望获得更多类似直方图和百分位延迟的指标。
关于架构更改:他们希望 PostgreSQL 记录架构更改事件的历史记录,例如添加或删除列和其他 DDL 操作。
关于架构更改:他们希望 PostgreSQL 记录架构更改事件的历史记录,例如添加或删除列和其他 DDL 操作。
监控视图语义:他们观察到一个状态为 Active 且 wait_event = ClientRead 的会话持续了两个多小时。这表明连接在 QueryStart 之后的一段时间内保持活跃,这样的连接无法通过 idle_in_transaction 超时终止。他们想了解这是否是一个 bug 以及如何解决。
监控视图语义:他们观察到一个状态为 Active 且 wait_event = ClientRead 的会话持续了两个多小时。这表明连接在 QueryStart 之后的一段时间内保持活跃,这样的连接无法通过 idle_in_transaction 超时终止。他们想了解这是否是一个 bug 以及如何解决。
最后,他们建议优化 PostgreSQL 的默认参数,指出当前的默认值过于保守。他们询问是否可以实现更好的默认值或基于启发式的设置。
最后,他们建议优化 PostgreSQL 的默认参数,指出当前的默认值过于保守。他们询问是否可以实现更好的默认值或基于启发式的设置。
虽然 PGConf.Dev 2025 主要关注开发,但通常也会有用户端的用例分享——就像 OpenAI 在 PostgreSQL 上的可扩展性实践。这样的话题对核心开发者来说实际上很有趣,因为他们中许多人对 PostgreSQL 在极端现实场景中的使用方式没有概念。
自 2017 年末以来,老冯在 Tantan(当时中国互联网部门最大、最复杂的部署之一)管理了数十个 PostgreSQL 集群:数十个 PostgreSQL 集群处理大约 250 万 QPS。当时,他们最大的核心集群使用了一个包含 33 个副本的主数据库,并承载了大约 40 万 QPS。瓶颈也在单节点写入性能上,他们最终通过在应用侧进行数据库和表分片来解决了这个问题。
可以说,OpenAI 演讲中所遇到的问题和应用的解决方案都是他们之前处理过的。当然,现在的区别是,当今的顶级硬件比八年前强大得多。这使得像 OpenAI 这样的初创公司能够使用单个 PostgreSQL 集群——无需分片或分区——来服务整个业务。这无疑为"分布式数据库是一个伪需求"这一观点提供了又一个有力证据。
OpenAI 在 Azure 上使用托管 PostgreSQL,配置顶级服务器规格。副本数量相对较多,包括一些跨地域副本。这个庞大的集群总共处理数百万 QPS(读+写)。他们使用 Datadog 进行监控,其服务通过 Kubernetes 内的应用侧 PgBouncer 连接池访问 Azure Database for Postgres 集群。
由于 OpenAI 是战略级客户,Azure PostgreSQL 团队提供了非常亲身的支持。但显然,即使是顶级云数据库服务,用户仍然需要在应用和运维方面具备强大的意识和能力。即使是 OpenAI 这样的聪慧团队,在 PostgreSQL 运维实践中仍然会遇到陷阱。
会议后,在晚间社交活动中,Lao Feng 与 Bohan 以及另外两位数据库创始人进行了长时间的交流,一直聊到天明。这次私下对话非常有意思,尽管 Lao Feng 无法透露更多细节——哈哈。
关于 Bohan 提出的问题和功能需求,Lao Feng 在这里提供一些回答。实际上,OpenAI 寻求的大部分功能已经存在于 PostgreSQL 生态中——只是可能不在核心 PostgreSQL 或 Azure Database for Postgres 中可用。
PostgreSQL 实际上确实有禁用索引的功能。你可以简单地在 pg_index 系统目录中将 indisvalid 字段设置为 false。这会让规划器忽略该索引,尽管在 DML 操作期间仍然会维护它。从技术角度来说,这完全没问题——这与通过 isready 和 isvalid 标志进行的并发索引创建所使用的机制相同。这不是黑魔法。
也就是说,可以理解为什么 OpenAI 无法使用这个方法——Azure Database for Postgres 不授予超级用户权限,所以你无法直接修改系统目录来实现这一点。
但回到原始目标——避免意外删除索引——有一个更简单的解决方案:只需通过监控视图确认该索引既没有在主库上也没有在副本上被使用。如果很长时间都没有被访问过,删除它是安全的。
使用 Pigsty 监控系统,你可以观察 PGSQL 表的实时索引切换过程。
CREATE UNIQUE INDEX CONCURRENTLY pgbench_accounts_pkey2
ON pgbench_accounts USING BTREE(aid);
-- 将原始索引标记为无效(不会被使用),但仍然会被维护
UPDATE pg_index SET indisvalid = false
WHERE indexrelid = 'pgbench_accounts_pkey'::regclass;
pg_stat_statements 短期内不太可能提供 P95 或 P99 百分位指标,因为这会大幅增加该扩展的内存占用——可能增加几十倍。虽然现代服务器可以处理,但极其保守的环境可能不行。我问过 pg_stat_statements 的维护者,这不太可能发生。我也问过 pgbouncer 的维护者 Jelte,这样的功能在短期内也不太可能实现。
但这个问题是可以解决的。首先,pg_stat_monitor 扩展确实提供了详细的百分位延迟(RT)指标,肯定可以工作,尽管你需要考虑收集这些指标的性能开销。第二个选项是使用 eBPF 被动收集 RT 指标,当然,最简单的方法是直接在应用的数据访问层(DAL)中添加查询延迟监控。
最优雅的解决方案可能是基于 eBPF 的边信道收集,但由于他们使用的是 Azure 托管 PostgreSQL,没有服务器访问权限,这个选项可能行不通。
实际上,PostgreSQL 日志已经提供了这个功能——只需将 log_statement 设置为 ddl(或更详细的 mod 或 all),所有 DDL 语句都会被记录。pgaudit 扩展提供了类似的功能。
但我怀疑他们真正想要的不是日志,而是一个可以通过 SQL 查询的系统视图。在这种情况下,另一个选项是使用 CREATE EVENT TRIGGER 将 DDL 事件直接记录到数据表中。pg_ddl_historization 扩展提供了一个更简单的方式来做这件事,我已经编译并打包了这个扩展。
然而,创建事件触发器也需要超级用户权限。AWS RDS 有一些特殊处理使这成为可能,但 Azure 的 PostgreSQL 似乎不支持它。
在 OpenAI 的例子中,State = Active 意味着后端进程仍处于单个 SQL 语句的生命周期内——它还没有向前端发送 ReadyForQuery 消息,所以 PostgreSQL 仍然认为该语句"尚未完成"。因此,行锁、缓冲区锁、快照和文件句柄等资源仍然被认为是"正在使用中"。WaitEvent = ClientRead 意味着进程正在等待来自客户端的输入。当两者一起出现时,典型的情况是空闲的 COPY FROM STDIN,但也可能是由于 TCP 阻塞或卡在 BIND 和 EXECUTE 之间。所以很难明确地说这是否是一个 bug——这取决于连接实际在做什么。
有些人可能会争论,从 CPU 角度来看,等待客户端 I/O 应该算作"空闲"。但 State 跟踪的是语句的执行状态,而不是进程是否在主动使用 CPU。一个查询可以在 Active 状态,同时不在 CPU 上运行(当 WaitEvent 为 NULL 时),或者它可以在 CPU 上循环等待客户端输入(即 ClientRead)。
回到核心问题——有办法解决。例如,在 Pigsty 中,当 PostgreSQL 通过 HAProxy 访问时,主服务在负载均衡器级别设置了最大连接生命周期(例如 24 小时)。在更严格的环境中,这可以短至一小时。这意味着超过生命周期的连接会被终止。不过理想情况下,客户端连接池应该主动强制执行连接生命周期,而不是被强制断开。对于离线的只读服务,不需要这个超时——允许可能持续数天的长运行查询。这种方法为连接处于 Active 但等待 I/O 的情况提供了一道安全网。
也就是说,不清楚 Azure PostgreSQL 是否提供这种控制。
PostgreSQL 的默认参数非常保守。例如,它默认只有 256 MB 内存(最低可设置为 256 KB!)。好处是 PostgreSQL 可以在几乎任何环境中启动和运行。坏处呢?我见过一个拥有 1 TB 物理内存的生产设置仍然以默认的 256 MB 配置运行……(由于双重缓冲,它实际上运行了相当长的时间。)
总的来说,我认为保守的默认值并不是坏事。这个问题可以通过更灵活的动态配置来解决。Azure Database for Postgres 和 Pigsty 等服务为初始参数调优提供了精心设计的启发式方法,已经很好地解决了这个问题。也就是说,这个功能仍然可以内置到 PostgreSQL 命令行工具中——例如,在 initdb 期间,该工具可以自动检测 CPU、内存、磁盘大小和类型,并相应地设置合理的默认值。
OpenAI 设置中的真正挑战并不来自 PostgreSQL 本身,而是来自在 Azure 上使用托管 PostgreSQL 的限制。一个解决方案是通过在本地 NVMe SSD 实例上使用 Azure 或其他云的 IaaS 层来部署自托管 PostgreSQL 集群,从而绕过这些限制。
实际上,Pigsty 正是由 Lao Feng 为了应对这个规模的 PostgreSQL 挑战而构建的——它本质上是一个自托管的 Azure Database for Postgres 解决方案,并且扩展性很好。OpenAI 已经遇到或将来会遇到的许多问题在 Pigsty 中已经有了解决方案,而且它是开源和免费的。
如果 OpenAI 感兴趣,我很乐意提供帮助。也就是说,当一家公司以他们这样的速度扩展时,调整数据库基础设施可能不是首要任务。幸运的是,他们拥有一些出色的 PostgreSQL 数据库管理员,可以继续前进并探索这些路径。
本文由 Lao Feng 授权翻译和重新发布。原文链接为 https://mp.weixin.qq.com/s/ykrasJ2UeKZAMtHCmtG93Q