ChatGPT 如何通过 PostgreSQL 为 8 亿用户提供服务
1. 架构策略
单主多从:采用“1 主 + 50 从”架构,强制读写分离。
负载外迁:将写密集型任务迁移至分布式数据库(如 Cosmos DB),新功能默认不再使用 PostgreSQL。
2. 性能优化
连接管理:引入 PgBouncer 连接池,将连接延迟从 50ms 降至 5ms。
查询重构:拆分复杂的多表联查(如 12 表关联),将逻辑移至应用层。
变更规范:强制 Schema 变更必须在 5 秒内完成,严禁长事务锁表。
3. 系统保护(防雪崩)
缓存锁定(Cache Locking):防止缓存失效时大量请求瞬间击垮数据库(避免“惊群效应”)。
多级限流:在各层级实施严格限流,确保数据库不被异常流量冲垮。
4. 取得的成果
极高性能:支撑数百万 QPS,P99 客户端延迟稳定在低两位数毫秒级。
高可用性:实现了五个九(99.999%)的可用性。
稳定性:在过去 12 个月中,仅发生过一次与 PostgreSQL 相关的重大事故(SEV-0)。
我们为提升生产基础设施以维持这种增长所做的努力揭示了一个新的见解:PostgreSQL 可以被扩展以可靠地支持比许多人先前想象的要大得多的读取密集型工作负载。该系统(最初由加州大学伯克利分校的一组科学家创建)使我们能够通过单个主 Azure PostgreSQL 灵活服务器实例以及遍布全球多个区域的近 50 个只读副本,来支持海量的全球流量。本文讲述了我们如何在 OpenAI 扩展 PostgreSQL,通过严格的优化和扎实的工程技术,为 8 亿用户提供每秒数百万次查询支持的故事;我们还将介绍在此过程中学到的关键经验。
我们最初设计中的缺陷
ChatGPT 发布后,流量以前所未有的速度增长。为了支持它,我们迅速在应用程序和 PostgreSQL 数据库层都实施了广泛的优化,通过增加实例大小进行纵向扩展,并通过添加更多只读副本进行横向扩展。这个架构在很长一段时间内都很好地为我们服务。随着不断的改进,它继续为未来的增长提供了充足的空间。
单一主库架构能够满足 OpenAI 的规模需求,这听起来可能令人惊讶;然而,在实践中要实现这一点并不简单。我们已经遇到了几次由 Postgres 过载引起的严重事件(SEV),它们通常遵循相同的模式:上游问题导致数据库负载突然飙升,例如缓存层故障导致的大范围缓存未命中、昂贵的多路连接查询使 CPU 饱和,或者新功能发布引发的写入风暴。随着资源利用率攀升,查询延迟增加,请求开始超时。然后,重试会进一步加重负载,引发恶性循环,有可能导致整个 ChatGPT 和 API 服务降级。
尽管 PostgreSQL 对于我们的读取密集型工作负载具有良好的扩展性,但在高写入流量期间我们仍然会遇到挑战。这在很大程度上是由于 PostgreSQL 的多版本并发控制(MVCC)实现,这使得它在写入密集型工作负载下效率较低。例如,当查询更新一个元组甚至单个字段时,整个行都会被复制以创建一个新版本。在繁重的写入负载下,这会导致显著的写入放大。它还会增加读取放大,因为查询必须扫描多个元组版本(死元组)才能检索到最新的一个。MVCC 还带来了其他挑战,例如表和索引膨胀、索引维护开销增加以及复杂的自动清理(autovacuum)调整。(您可以在我与卡内基梅隆大学的 Andy Pavlo 教授合著的一篇名为《我们最讨厌的 PostgreSQL 的一部分》的博客中找到对这些问题的深入探讨,该博客在 PostgreSQL 的维基百科页面中被引用。)
将 PostgreSQL 扩展到数百万 QPS
为了缓解这些限制并减少写入压力,我们已经迁移并将继续迁移可分片(即可水平分区的工作负载)的写入密集型工作负载到分片系统,例如 Azure Cosmos DB,并优化应用程序逻辑以最小化不必要的写入。我们也不再允许向当前的 PostgreSQL 部署中添加新表。新工作负载默认使用分片系统。
尽管我们的基础设施不断发展,PostgreSQL 仍然保持未分片状态,由单个主实例处理所有写入。主要原因是,对现有应用程序工作负载进行分片将非常复杂和耗时,需要更改数百个应用程序端点,并可能需要数月甚至数年的时间。由于我们的工作负载主要是读取密集型,并且我们已经实施了广泛的优化,因此当前的架构仍然为支持持续的流量增长提供了充足的空间。虽然我们不排除将来对 PostgreSQL 进行分片的可能性,但鉴于我们目前和未来的增长有足够的空间,这并不是近期的优先事项。
在以下部分中,我们将深入探讨我们面临的挑战以及为解决这些挑战并防止未来中断而实施的广泛优化,将 PostgreSQL 推向极限,并将其扩展到每秒数百万次查询(QPS)。
减少主库负载
挑战: 只有一个写入节点,单主设置无法扩展写入。繁重的写入峰值会迅速使主库过载,并影响 ChatGPT 和我们的 API 等服务。
解决方案: 我们尽可能地减少主库的负载——包括读取和写入——以确保其有足够的能力处理写入峰值。读取流量尽可能地被分流到副本。然而,一些读取查询必须保留在主库上,因为它们是写入事务的一部分。对于这些查询,我们专注于确保它们是高效的,并避免慢查询。对于写入流量,我们已将可分片的写入密集型工作负载迁移到分片系统,例如 Azure CosmosDB。那些更难分片但仍产生高写入量的工作负载需要更长的时间来迁移,这个过程仍在进行中。我们还积极优化我们的应用程序以减少写入负载;例如,我们修复了导致冗余写入的应用程序错误,并在适当的情况下引入了延迟写入,以平滑流量峰值。此外,在回填表字段时,我们强制执行严格的速率限制,以防止过度的写入压力。
查询优化
挑战: 我们在 PostgreSQL 中发现了一些昂贵的查询。过去,这些查询的流量突然激增会消耗大量 CPU,从而减慢 ChatGPT 和 API 的请求速度。
解决方案: 一些昂贵的查询,例如连接多个表的查询,会严重降低甚至导致整个服务宕机。我们需要持续优化 PostgreSQL 查询,以确保其高效并避免常见的在线事务处理(OLTP)反模式。例如,我们曾经发现一个连接 12 个表的极其昂贵的查询,该查询的峰值是过去几次高严重性事件(SEV)的罪魁祸首。我们应尽可能避免复杂的多表连接。如果必须进行连接,我们学会了考虑分解查询并将复杂的连接逻辑移至应用层。许多此类有问题的查询是由对象关系映射(ORM)框架生成的,因此仔细审查它们生成的 SQL 并确保其行为符合预期非常重要。在 PostgreSQL 中也经常会发现长时间运行的空闲查询。配置 idle_in_transaction_session_timeout 等超时对于防止它们阻塞自动清理至关重要。
单点故障缓解
挑战: 如果一个只读副本宕机,流量仍然可以被路由到其他副本。然而,依赖单个写入节点意味着存在单点故障——如果它宕机,整个服务都会受到影响。
解决方案: 大多数关键请求只涉及读取查询。为了缓解主库的单点故障问题,我们将这些读取从写入节点分流到副本,确保即使主库宕机,这些请求也能继续提供服务。虽然写入操作仍然会失败,但影响已经减小;由于读取仍然可用,这不再是 SEV0 级别的事件。
为了缓解主库故障,我们在高可用性(HA)模式下运行主库,并配备一个热备库,这是一个持续同步的副本,随时准备接管流量。如果主库宕机或需要下线进行维护,我们可以迅速将备用库提升为主库,以最大限度地减少停机时间。Azure PostgreSQL 团队在确保这些故障转移即使在非常高的负载下也能保持安全可靠方面做了大量工作。为了处理只读副本故障,我们在每个区域部署了多个具有足够容量余量的副本,确保单个副本故障不会导致区域性中断。
工作负载隔离
挑战: 我们经常遇到某些请求在 PostgreSQL 实例上消耗过多资源的情况。这可能导致在相同实例上运行的其他工作负载性能下降。例如,一个新功能的发布可能会引入低效的查询,大量消耗 PostgreSQL 的 CPU,从而减慢其他关键功能的请求速度。
解决方案: 为了缓解“邻居噪音”问题,我们将工作负载隔离到专用的实例上,以确保资源密集型请求的突然激增不会影响其他流量。具体来说,我们将请求分为低优先级和高优先级,并将它们路由到不同的实例。这样,即使低优先级的工作负载变得资源密集,也不会降低高优先级请求的性能。我们也在不同的产品和服务之间应用相同的策略,以便一个产品的活动不会影响另一个产品的性能或可靠性。
连接池
挑战: 每个实例都有最大连接数限制(在 Azure PostgreSQL 中为 5,000)。很容易耗尽连接或累积过多的空闲连接。我们之前曾发生过因连接风暴耗尽所有可用连接而导致的事件。
解决方案: 我们部署了 PgBouncer 作为代理层来池化数据库连接。在语句或事务池化模式下运行它,使我们能够高效地重用连接,从而大大减少了活动客户端连接的数量。这也减少了连接建立的延迟:在我们的基准测试中,平均连接时间从 50 毫秒(ms)降至 5 毫秒。跨区域的连接和请求可能成本高昂,因此我们将代理、客户端和副本共同部署在同一区域,以最小化网络开销和连接使用时间。此外,PgBouncer 必须仔细配置。像空闲超时这样的设置对于防止连接耗尽至关重要。
每个只读副本都有自己的 Kubernetes 部署,运行多个 PgBouncer Pod。我们在同一个 Kubernetes 服务后面运行多个 Kubernetes 部署,该服务在 Pod 之间进行流量负载均衡。
缓存
挑战: 缓存未命中的突然激增会引发对 PostgreSQL 数据库的读取浪潮,使 CPU 饱和并减慢用户请求。
解决方案: 为了减少对 PostgreSQL 的读取压力,我们使用缓存层来处理大部分读取流量。然而,当缓存命中率意外下降时,缓存未命中的突发可能会将大量请求直接推向 PostgreSQL。数据库读取的突然增加会消耗大量资源,从而减慢服务速度。为了防止在缓存未命中风暴期间出现过载,我们实现了一种缓存锁定(和租用)机制,以便只有一个针对特定键未命中的读取器从 PostgreSQL 获取数据。当多个请求在同一个缓存键上未命中时,只有一个请求获取锁并继续检索数据并重新填充缓存。所有其他请求都等待缓存更新,而不是同时冲击 PostgreSQL。这显著减少了冗余的数据库读取,并保护系统免受级联的负载峰值影响。
扩展只读副本
挑战: 主库将预写日志(WAL)数据流式传输到每个只读副本。随着副本数量的增加,主库必须将 WAL 发送到更多实例,从而增加了网络带宽和 CPU 的压力。这会导致更高且更不稳定的副本延迟,使系统更难可靠地扩展。
解决方案: 我们在多个地理区域运营近 50 个只读副本,以最大限度地减少延迟。然而,在当前的架构下,主库必须将 WAL 流式传输到每个副本。尽管目前它可以通过非常大的实例类型和高网络带宽很好地扩展,但我们不能无限期地增加副本,否则最终会使主库过载。为了解决这个问题,我们正在与 Azure PostgreSQL 团队合作开发级联复制,其中中间副本将 WAL 中继到下游副本。这种方法使我们能够扩展到可能超过一百个副本,而不会使主库不堪重负。然而,它也引入了额外的操作复杂性,特别是在故障转移管理方面。该功能仍在测试中;我们将在将其推广到生产环境之前,确保其健壮且能够安全地进行故障转移。
速率限制
挑战: 特定端点的流量突然激增、昂贵查询的激增或重试风暴会迅速耗尽 CPU、I/O 和连接等关键资源,从而导致广泛的服务降级。
模式管理
挑战: 即使是微小的模式更改,例如更改列类型,也可能触发全表重写。因此,我们谨慎地应用模式更改——将它们限制在轻量级操作上,并避免任何重写整个表的操作。
解决方案: 只允许进行轻量级的模式更改,例如添加或删除不会触发全表重写的某些列。我们对模式更改强制执行严格的 5 秒超时。允许并发创建和删除索引。模式更改仅限于现有表。如果新功能需要额外的表,它们必须位于 Azure CosmosDB 等替代的分片系统中,而不是 PostgreSQL 中。在回填表字段时,我们应用严格的速率限制以防止写入峰值。尽管这个过程有时可能需要一周多的时间,但它确保了稳定性并避免了任何生产影响。
结果与未来展望
这项工作表明,通过正确的设计和优化,Azure PostgreSQL 可以扩展以处理最大的生产工作负载。PostgreSQL 为读取密集型工作负载处理数百万的 QPS,为 OpenAI 的 ChatGPT 和 API 平台等最关键产品提供支持。我们增加了近 50 个只读副本,同时将复制延迟保持在接近于零的水平,在地理分布的区域保持了低延迟读取,并建立了足够的容量余量以支持未来的增长。
这种扩展在最大限度地减少延迟和提高可靠性的同时仍然有效。我们在生产中始终如一地提供低两位数毫秒的 p99 客户端延迟和五个九的可用性。在过去的 12 个月中,我们只发生过一次 SEV-0 级别的 PostgreSQL 事件(它发生在 ChatGPT ImageGen 的病毒式发布期间,当时由于一周内有超过 1 亿新用户注册,写入流量突然激增了 10 倍以上。)
虽然我们对 PostgreSQL 取得的成就感到满意,但我们仍在继续挑战其极限,以确保我们有足够的空间来应对未来的增长。我们已经将可分片的写入密集型工作负载迁移到了像 CosmosDB 这样的分片系统中。剩下的写入密集型工作负载更难分片——我们也在积极迁移这些工作负载,以进一步从 PostgreSQL 主库中分流写入。我们还与 Azure 合作,以启用级联复制,这样我们就可以安全地扩展到更多的只读副本。
展望未来,随着我们基础设施需求的持续增长,我们将继续探索其他进一步扩展的方法,包括分片 PostgreSQL 或其他分布式系统。
致谢
特别感谢 Jon Lee、Sicheng Liu、Chaomin Yu 和 Chenglong Hao 对本文的贡献,以及帮助扩展 PostgreSQL 的整个团队。我们还要感谢 Azure PostgreSQL 团队的鼎力合作。
参考文献
[1] Azure PostgreSQL 灵活服务器实例. https://azure.microsoft.com/en-us/products/postgresql/
[2] The Part of PostgreSQL We Hate the Most. https://www.orioledata.com/blog/the-part-of-postgresql-we-hate-the-most/
[3] PostgreSQL - Wikipedia. https://en.wikipedia.org/wiki/PostgreSQL#MVCC_and_performance
[4] Cascading Replication. https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-CASCADE
[5] ALTER TABLE - PostgreSQL documentation. https://www.postgresql.org/docs/current/sql-altertable.html
[6] ChatGPT can now see, hear, and speak. https://openai.com/blog/chatgpt-can-now-see-hear-and-speak