返回 文章 build CMS 文章

OpenAI 如何用 Python 将 Habitat 存储平台扩展至每秒 7000 万请求

OpenAI 在线存储平台 Habitat 的扩展历程:从 Python 库到服务,再到用 Rust 重写。

OpenAIHabitat在线存储Python
成长分 / 100 82 综合收获、行动、留存与影响

OpenAI 如何用 Python 将 Habitat 存储平台扩展至每秒 7000 万请求
为什么值得读了解 OpenAI 如何以前所未有的速度扩展在线存储基础设施,支撑每周超 10 亿用户。

学习在 Python 服务中管理 asyncio 调度延迟、连接池亚稳态故障等实际工程问题的解决方案。

关键洞察
  1. Habitat 从 2024 年中的简单 Python 客户端库演变为分布式服务,现每秒处理超 7000 万请求,存储超 500PB 数据,覆盖近 40 个地理区域。
  2. 将 Habitat 从库转变为独立服务,为部署、可观测性和平台增强建立了单一控制点,并集中执行数据安全与隐私策略。
  3. Python asyncio 提供并发但非 CPU 并行,高 CPU 工作负载下调度延迟可主导尾部延迟,需监控事件循环并限制每进程并发请求数。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:21826

快速扩展在线存储以服务超过10亿ChatGPT用户

我们如何用Python调整应用存储平台Habitat,以应对前所未有的增长。

作者:Jon Lee、Chaomin Yu和Ben Ries,技术团队成员

每个OpenAI产品都依赖于快速、可靠的数据访问,无论是有人登录、检查他们的Codex设置,还是在ChatGPT中开始新的对话。这些操作中的每一个都可能需要许多单独的数据查找,然后产品才能响应。如果这些请求很慢,产品就会感觉慢。如果这些请求失败,产品就会完全停止工作。

Habitat是我们构建的在线存储平台,以便OpenAI产品能够快速可靠地访问所需信息。Habitat现在每秒处理超过7000万次请求,支持每周超过10亿人使用的产品,覆盖近40个地理区域。两年前,Habitat最初只是一个连接到单个数据库的简单Python客户端库。如今,它是一个复杂的分布式系统,提供超过500PB的数据。

图01 · 什么是Habitat?

在线存储平台

Habitat是我们构建的在线存储平台,以便OpenAI产品能够快速可靠地访问所需信息。

  • 请求
  • 响应
  • 变更(CDC)

在这个规模上构建和运营基础设施绝非易事,但也并非特别具有挑战性。我们的情况独特之处在于,我们不得不以前所未有的速度扩展,以支持惊人的用户增长和产品需求,同时还要构建一个成熟的平台。通常,系统工程师会为10倍规模构建,并希望它能维持几年,同时为下一个10倍做准备。在我们的案例中,过去三年我们每年同比增长超过10倍。因此,构建和运营Habitat是一系列战术决策和排序:在最底层理解每个组件,以从现有技术栈中榨取尽可能多的汁液,同时抵御存储和计算容量的紧缩,为基础投资争取时间。

  • 每秒7000万+ 请求

  • 每周10亿+ 人

  • 500 PB+ 数据

随着OpenAI的增长,Habitat也必须随之增长:首先变得足够可靠以处理关键任务的产品流量,然后足够快以服务全球用户,最后,灵巧地大规模运营。这篇文章是关于我们如何扩展在线存储的两部分系列中的第一部分。在这篇文章中,我们将分享Habitat如何演变,为什么我们将其从库转变为服务,以及我们如何将一个用不常见的服务栈语言——Python——编写的服务扩展为可靠的存储平台层。

在未来的文章中,我们将详细讨论我们如何实现大规模多租户可靠性,我们优化读取性能的分层策略,以及我们如何扩展与Azure Cosmos DB的合作关系,以可靠地处理前所未有的需求。

Habitat始于一个简单的想法:产品工程师不应该需要考虑数据库管理。Habitat于2024年中期作为一个与ChatGPT主服务器交互的小型Python库开始。它支持一小组操作,这些操作在底层映射到数据库应用程序Azure Cosmos DB。

该库的职责是为产品团队提供一种简单的方式来存储和检索数据,而无需掌握底层细节。Habitat 负责处理必要的工作:确定涉及的数据类型、数据应来自(或去往)何处、请求是否被允许等等。

产品工程师无需关心模式查找、路由、授权、加密、序列化、请求整形和连接池。他们甚至不需要考虑数据来自哪里:Azure Cosmos DB、缓存或其他类型的存储。

图 02 · Habitat 服务

简化的 Habitat 请求流程

通过将存储逻辑解耦为独立服务,我们为部署、可观测性和平台增强建立了单一控制点。

  • 请求
  • 响应

这个 Python 库运行良好,Habitat 在 OpenAI 的产品工程师中迅速得到采用,尽管并没有集中的中央推动来弃用自助式 Postgres 和 Azure Cosmos DB。

随着产品需求的发展,产品开发者甚至可以轻松地向共享库添加对客户端缓存、压缩或加密等功能的支持。

到 2025 年中期,Habitat 作为客户端实现已达到了极限。随着 Habitat 层变得越来越复杂,OpenAI 的服务数量不断增加,向后兼容的协议变更已变得不可行。

在一个实例中,我们希望通过将最关键的数据集迁移到一组区域分布的 Azure Cosmos DB 账户,来减少任何单一区域中断的影响范围。进行此更改需要向客户端引入额外的路由逻辑,通过功能标志禁用,确保它推广到所有客户端,然后启用功能标志。

协调数十个服务的部署并与每个团队合作推广花费了数天时间。在启用此功能之前,我们意识到需要引入一些影子测试以确保分片逻辑正确。这又花了几天时间推广。修复我们发现的错误?又花了几天。最终,我们准备好启用标志,却有一个团队因无关原因将其服务回滚到之前有缺陷的客户端,导致我们努力避免的中断发生了。

客户端库的更改需要在数十个服务之间进行复杂的协调,这一过程被证明越来越脆弱、低效,且容易发生操作故障。为了减少未来部署的操作扩散,我们决定将 Habitat 提取到自己的服务中。

通过将存储逻辑解耦为独立服务,我们为部署、可观测性和平台增强建立了单一控制点。我们不再管理碎片化的更新,而是可以集中实施改进,为每个 OpenAI 产品带来即时好处。

集中式服务还为我们提供了一个单一的瓶颈点,以提供最强的数据安全和隐私原语。Habitat 服务是我们集中执行访问控制策略、执行审计日志记录以及限制对 Azure Cosmos DB 等底层存储资源访问的地方。Habitat 在保护用户数据和防止外部、内部和代理行为者的未经授权访问方面发挥着关键作用。

我们知道需要一个服务,但还不想完全从 Python 迁移,即使 Python 作为服务会带来额外的开销。与本地库执行相比,使用 Python 提供高吞吐量服务增加了网络延迟,并带来了大量的 CPU 和内存扩展成本。此外,我们认识到 Python 的低效在 100 倍规模下将无法接受,最终几乎肯定需要重写。

然而,我们将其视为技术债务的战略性引入。当时我们的首要目标不是成本或资源优化,而是解锁产品开发者并实现平台稳定性。通过短期内接受 Python 服务的性能权衡,我们得以优先处理更紧迫的挑战,建立核心 API,并构建强大的基础设施。

我们还做了一个经过计算的赌注:我们自己的编码模型的快速进步将简化未来的技术路径。我们押注当需要完全从 Python 迁移时,Codex 和 GPT 将使迁移成为可能。这个赌注最终被证明是正确的。

从性能角度看,将 Habitat 作为 Python 服务运行并非最优,但却是必要的选择。Python 让我们快速行动,但这并不意味着我们可以不顾一切,接受明显更差的延迟。当平均用户请求导致数百次数据库调用时,最慢的数据库调用就是用户能感受到的那个。我们发现,在这种规模下运行 Python 服务的主要挑战在于管理这些尾部延迟。

Asyncio 帮助 Python 并发执行 I/O 密集型工作负载,但无助于绕过 Python GIL 并提供 CPU 并行性。除了 I/O 密集型的请求代理外,Habitat 还处理许多 CPU 密集型职责和后台任务:路由、压缩、加密、校验和、下游健康检查、请求影子复制和对冲。

由于我们的服务中有如此多的 CPU 密集型工作负载和后台任务,asyncio 调度延迟很容易主导尾部请求延迟。在为初始服务发布进行调优之前,我们在 p99 及更高延迟的请求跟踪中看到,虽然下游存储响应迅速,但请求经常在等待负责的协程被重新调度以解析响应时停滞。

图 03 · 跟踪 asyncio 延迟

并发不是 CPU 并行

Python asyncio 允许并发请求处理,但一次只有一个请求在 CPU 线程上执行。当有大量 CPU 工作要做时,这对请求延迟有很大影响。

低 CPU 工作

简短的 Python 步骤;I/O 等待重叠Python:请求 A · CPU 请求处理

Python 线程

请求 ACPU 请求

请求 B等待第一个 Python 轮次

请求 C等待第一个 Python 轮次

Cosmos + 网络等待:两种场景下每个请求 6 个单位。

高 CPU 工作

长的 Python 步骤使就绪响应等待Python:请求 A · CPU 请求处理

Python 线程

请求 ACPU 请求

请求 B等待第一个 Python 轮次

请求 C等待第一个 Python 轮次

Cosmos + 网络等待:两种场景下每个请求 6 个单位。

对于 OpenAI 的 Python 服务,我们发现除了测量内存、CPU、网络和磁盘使用的标准利用率和饱和度指标外,监控 asyncio 循环及其繁忙程度并相应调优也至关重要。

通过定期调度后台任务并记录预期与实际执行时间之间的差值,我们能够实时地以经验方式测量事件循环调度延迟。在高利用率下,当存在许多高开销任务时,即使每个进程只有适度的并发请求数量,也足以产生显著的调度抖动,最高可达数百毫秒,在某些边缘情况下甚至可达数秒。

因此,我们选择让每个进程仅服务少量并发请求,转而大规模扩展 Python 工作进程的数量。

在服务初始上线时,我们通过实时服务 CPU 性能分析发现了一个导致高 asyncio 延迟(以及由此产生的高尾延迟)的根本原因:通过 Statsig(一种管理功能标志的工具,可用于运行 A/B 测试等)定期对功能标志配置进行 JSON 解析。

默认情况下,Statsig 被配置为每分钟轮询刷新配置且无抖动,并且该配置包含了每个服务的所有生产规则。此外,架构决策上每个 Pod 运行最多 8 个 Python 进程,以推高 CPU 使用率并提供更低的延迟。综合起来,这意味着每分钟每个 Pod 都会出现某个时刻,其所有工作进程都停止处理进行中的请求,转而将 CPU 周期用于解析一个巨大的配置文件。

一旦 CPU 性能分析帮助我们找到根本原因,修复就很简单了:部署更小的针对性配置、延长刷新间隔,并为这类后台任务添加一些抖动。

为了保持低 asyncio 延迟,维持服务器进程之间良好的请求负载均衡也至关重要;如果不进行调优,连接池最终可能会与此相悖。

在客户端连接池的情况下,一个发出许多并发请求的客户端进程可能只建立少量服务器连接,结果将其所有负载仅发送到少量进程。在调整负载均衡方式之前,我们的服务利用率差异很大,一些尾部进程服务的并发请求数量是平均值的 5-10 倍。

我们是在一次偶然事件中发现这一点的:尽管停止了使部分服务过载的客户端,一部分进程在突发流量过去很久后仍然处于降级状态。事实上,我们注意到这些进程经历了失控的降级,接收到的请求越来越多,直到我们重启它们。一旦某个 Pod 过载,某些行为会将更多流量固定到该过载的 Pod 上。这是一类我们一些队友从先前工作中非常熟悉的故障:亚稳态故障(在新窗口中打开)

我们怀疑连接池是罪魁祸首,并通过限制最大连接复用时长来验证这一猜测,这确实限制了性能退化,也证实了我们的调查方向。进一步调查发现,Python 的 aiohttp TCPConnector 默认采用 LIFO(后进先出)连接复用:最近归还的连接会被选用于下一个请求。这通常是一个合理的默认行为:复用较新的连接可以让为应对突发流量而创建的额外连接进入空闲超时,从而减少维护额外连接的开销。但在这种情况下,它给我们造成了一种亚稳态故障。在一波请求突发期间,发往较慢的过载服务器的请求会更晚将连接归还到池中,因此这些连接被后续请求选中的频率更高,逐渐将更多流量集中到那些本已吃力的 Pod 上。将连接池修补为使用 FIFO(先进先出)复用打破了这一反馈循环,甚至还降低了我们稳态下的请求方差。

图 04A · 客户端连接池

LIFO 将新工作送回慢进程

在一波请求突发之后,较慢的服务器最后将连接归还到池中。LIFO 促使更多工作集中到那些相同的较慢服务器上。

初始的一波请求到达 A、B 以及较慢的进程 C。

图 04B · 客户端连接池

FIFO 打破连接复用反馈循环

FIFO 在突发之后维持更多活跃连接,但在所有服务器之间公平地平衡工作负载。

初始的一波请求到达 A、B 以及较慢的进程 C。

如今,我们在整个 OpenAI 基础设施中主要依赖 Istio 和 Envoy 来提供连接池以及更好的服务器负载感知均衡策略,从而完全避免这个问题。

为降低 asyncio 延迟而进行调优,并拥有如此多的 Python 进程,其一个副作用是:海量的连接很容易压垮下游依赖(即所谓的“惊群效应”)。

一次常规的每日部署——如果没有调优得足够慢——可能会因连接循环而产生显著的 CPU 抖动。或者,连接泄漏可能通过占满 NAT 网关而使网络瘫痪。这些问题对其他服务来说也并不罕见,但由于进程数量多了一个数量级,触发阈值被显著降低,常常会占满网络相关资源,而客户端在仅基于纯吞吐量考虑时,并不预期在稳态下需要处理这些资源。

我们还依赖 Envoy 来最大化我们的连接扇入。我们用它把 Python 的 HTTP/1 连接升级为 HTTP/2,以利用多路复用,然后对这些连接进行池化并延长连接生命周期。Envoy 还为我们提供了一个集中位置来实现速率限制和熔断器,而这些如果在每个独立的 Python 进程中实现,效果会大打折扣。

图 05 · 连接扇入

相同的请求,更少的连接

连接池和 HTTP/2 连接多路复用有助于减少下游的连接负载。

直接从 Python 发起

独立的连接池保留了每个 Pod 的峰值。在所有 6 个 Pod 都达到 3 个请求的峰值后:18 个连接保持打开,但只有 6 个处于忙碌状态。

Pod 13 个活跃 · 峰值 3

Pod 20 个活跃 · 峰值 3

Pod 30 个活跃 · 峰值 3

Pod 40 个活跃 · 峰值 3

Pod 53 个活跃 · 峰值 3

Pod 60 个活跃 · 峰值 3

Azure Cosmos DB

6 个忙碌 · 12 个空闲 · 6 个并发上游请求

Envoy 池化 HTTP/1 连接

一个共享连接池增长到整个集群的峰值:6 个连接。稳定流量在响应返回后立即复用每个连接。

Pod 12 活跃 · 峰值 3

Pod 20 活跃 · 峰值 3

Pod 31 活跃 · 峰值 3

Pod 40 活跃 · 峰值 3

Pod 53 活跃 · 峰值 3

Pod 60 活跃 · 峰值 3

EnvoyHTTP/1 → HTTP/1

Azure Cosmos DB

6 忙碌 · 0 空闲 · 6 个并发上游请求

Envoy 升级到 HTTP/2

在稳定负载下,相同的 6 个请求共享 1 个保留的 HTTP/2 连接,每个请求在各自的并发流上。

Pod 12 活跃 · 峰值 3

Pod 20 活跃 · 峰值 3

Pod 31 活跃 · 峰值 3

Pod 40 活跃 · 峰值 3

Pod 53 活跃 · 峰值 3

Pod 60 活跃 · 峰值 3

EnvoyHTTP/1 → HTTP/2

Azure Cosmos DB

1 忙碌 · 0 空闲 · 6 个并发上游请求

我们能够将 Python 扩展到如此程度的一个原因是 Habitat 受限的 API,它使请求成本可预测。Habitat 没有允许客户端构造可能引发大表扫描或跨多表连接的任意 SQL 查询,而是暴露了一个简单的 NoSQL API。缺乏强大的 API 是 Habitat 设计中的一个明确权衡。

我们的目标是优化简单、可预测、恒定工作量的请求。根据我们的经验,这些系统本质上更容易扩展,且难以出错或误用。具有不可预测扇出的请求在操作上是危险的:它们使隔离、负载均衡复杂化,并引入难以扩展的延迟悬崖,对服务及其客户端都是如此。

在我们迁移到 Habitat 和 Azure Cosmos DB 之前,OpenAI 的大部分在线数据存储在 Postgres 上。那时,审查所有查询和模式更改以确保它们行为良好并在发布到生产环境之前针对索引数据操作是很容易的。随着团队和产品的增长,这很快变得难以管理,并且是频繁中断的常见原因,其中热路径上的单个昂贵新查询就会导致数据库瘫痪。

这里的问题在于成本不平衡:编写昂贵且难以运行的 SQL 查询既便宜又容易。在 Habitat 中,我们避免了这一点,并使昂贵的查询在客户端极其明显。没有可能使 Habitat 过载的无界查询,复杂的连接和图遍历要求产品团队承担一些繁重的工作,这有助于整体优化更高效的设计。

Habitat 暴露了一个围绕客户端定义的对象和边类型建模的 NoSQL API,灵感来自 TAO(在新窗口中打开)。客户端预定义对象和边以及它们之间的关系,但不定义每种类型的内容。由此产生的关系类似于图,但 Habitat 本身不支持典型的图遍历查询,除了查询特定对象的直接边之外。

我们对这个图进行分区,使得每个对象及其对应的边在存储级分区中并置,但我们没有在数据库级别做出协调努力来并置对象及其边指向的远程对象。结果是,该模型易于分区以实现水平可扩展性,但图遍历效率低下,因为对象之间的任何特定跳转可能需要从存储在不同区域的两个完全不同的 Azure Cosmos DB 账户中获取数据。

对于查询需求更复杂的客户,我们确实通过 Rockset 提供了一个离线的 Habitat 二级视图。我们使用变更数据捕获(CDC)将近乎实时地将在线存储中的变更流式传输到隔离的 Rockset 实例。每个客户团队负责为自己的复杂查询需求扩展各自的 Rockset 实例。

这种 Rockset 配置给我们的客户带来了额外的摩擦,但我们认为在当前这个特定时刻,这是正确的权衡:让简单查询成为默认选项,同时为需要复杂查询的用户提供一个逃生通道。这种设计将我们的在线存储与读取密集型的分析和搜索工作负载隔离开来。

将 Python 重写推迟一年,使我们能够在高速增长期间专注于更紧迫、更有影响力的挑战。随着平台日趋成熟、我们的增长持续加速,并且作为 OpenAI 按核心数计算的第二大服务(按 Envoy 占用计算为第四大),终于到了告别 Python 的时候了。在巅峰时期,Python 帮助我们每秒处理超过 2000 万个请求。

在 2026 年第二季度,仅凭 2 名工程师、Codex 和 GPT‑5.5,我们就能够用 Rust 重写整个服务。这个新的 Rust 服务目前处理着我们 95% 的生产请求;我们将在未来几周内完全弃用 Python。我们的数据显示,Rust 服务的 CPU 效率是 Python 版本的 6 倍,内存效率是 15 倍,且平均延迟和尾部延迟都显著更低。我们计划在未来的博客中分享更多经验。

Python——以及现在的 Rust——服务只是 Habitat 的一个方面。在本系列的第二部分中,我们将解释我们如何快速扩展在线存储以服务超过 10 亿 ChatGPT 用户,届时我们会讨论存储层,以及 Habitat 如何服务超过 500 PB 的数据和每秒超过 7000 万个请求。

如果你想在前沿规模的 OLTP 系统上工作,并对这类工程感兴趣,请查看我们团队的这个开放职位

继续阅读

查看全部