快速扩展在线存储以服务超过10亿ChatGPT用户
什么是 Habitat?
每个 OpenAI 产品都依赖于快速、可靠的数据访问能力,无论用户是在登录、查看 Codex 设置,还是在 ChatGPT 中开启新对话。上述每一个操作在产品响应之前都可能需要进行多次独立的数据查询。如果这些请求变慢,产品就会感觉卡顿;如果请求失败,产品将完全无法工作。
Habitat 是我们构建的在线存储平台,旨在让 OpenAI 产品能够快速、可靠地访问所需信息。Habitat 现在每秒处理超过 7000 万次请求,为每周超过 10 亿人使用的产品提供支持,覆盖近 40 个地理区域。Habitat 最初于 2023 年 DevDay 为支持 GPTs 而推出,起初是一个简单的 Python 客户端库,连接到一个独立的数据库。如今,它已发展成为一个复杂的分布式系统,服务超过 500 PB 的数据。
- 在线存储平台
- 请求
- 响应
- 变更(CDC)
在这种规模下构建和运行基础设施并非易事,但也不特别困难。我们的独特之处在于:面对前所未有的用户增长和产品需求,我们必须在构建成熟平台的同时,以惊人的速度进行扩展。通常,系统工程师会按照 10 倍规模进行设计,并希望这套架构能支撑几年,同时为下一次 10 倍扩展做准备。在我们的案例中,过去三年每年都实现了超过 10 倍的增长。因此,构建和运行 Habitat 是一系列战术性决策和排序的过程:在最低层次理解每个组件,以从现有技术栈中榨取最大价值,同时抵御存储和算力紧张,为基础性投资争取时间。
- 每秒 7000 万+ 次请求
- 每周 10 亿+ 用户
- 500 PB+ 数据
随着 OpenAI 的发展,Habitat 也必须随之成长:首先变得足够可靠,以支撑关键产品流量;然后变得足够快,以服务全球用户;最后,能够灵活应对巨大规模。本文是关于如何扩展在线存储的两篇系列文章的第一篇。在这篇文章中,我们将分享 Habitat 是如何演变的、为什么我们把它从库变成服务,以及如何将一个用不常见的服务端编程语言——Python——编写的服务打造成可靠的存储平台层。
在后续文章中,我们将深入探讨如何实现大规模多租户可靠性、分层读取性能优化策略,以及如何与 Azure Cosmos DB 合作可靠地处理前所未有的需求。
什么是 Habitat?
Habitat 源于一个简单的理念:产品工程师不应该需要考虑数据库管理。Habitat 最初于 2023 年 DevDay 为支持 GPTs 而推出,作为一个小型 Python 库,与 ChatGPT 的主服务器交互。它支持少量操作,这些操作在底层映射到数据库应用 Azure Cosmos DB。
这个库的职责是为产品团队提供一种简单的方式来存储和检索数据,而无需掌握底层细节。Habitat 负责处理必要的工作:判断涉及什么类型的数据、数据应该从哪里来(或去哪里)、请求是否被允许等等。
产品工程师无需关心模式查找、路由、授权、加密、序列化、请求整形和连接池。他们甚至不需要考虑数据来自哪里:无论是 Azure Cosmos DB、缓存还是其他类型的存储。
通过将存储逻辑解耦为独立服务,我们建立了对部署、可观测性和平台增强的单一控制点。
- 请求
- 响应
尽管没有集中的推动力让团队放弃使用自服务的 Postgres 和 Azure Cosmos DB,这个 Python 库运行良好,OpenAI 的产品工程师们迅速采用了 Habitat。
随着产品需求的发展,产品开发者甚至可以轻松地为共享库添加客户端缓存、压缩或加密等功能支持。
构建服务以更好地支持多个复杂产品
到 2025 年中期,Habitat 作为客户端实现的局限性开始显现。随着 Habitat 层变得越来越复杂,OpenAI 的服务数量不断增加,向后兼容的协议变更变得不可行。
在一次案例中,我们希望通过将最关键的数据集迁移到一组区域分布的 Azure Cosmos DB 账户来减少任何单一区域故障的影响范围。做出这一更改需要在客户端中引入额外的路由逻辑,该逻辑被禁用并隐藏在功能开关后面,确保它能 rollout 到所有客户端,然后启用该功能开关。
跨数十个服务协调部署并与每个团队合作进行 rollout 花费了数天。在此之前,我们意识到需要引入一些影子机制来确保分片逻辑是正确的。这又花了几天的 rollout 时间。修复我们发现的错误的 bug?又花了几天。最终,当我们准备好启用该开关时,其中一个团队由于无关原因将其服务回滚到了之前有 bug 的客户端,导致了我们曾努力避免的故障。
对客户端库的更改需要在数十个服务之间进行复杂协调,这个过程变得越来越脆弱、低效,而且容易出现运维故障。为了减少未来部署的运维扇出,我们决定将 Habitat 剥离为独立的服务。
通过将存储逻辑解耦为独立服务,我们建立了对部署、可观测性和平台增强的单一控制点。我们不再管理碎片化的更新,而是可以集中实施改进,立即惠及每一个 OpenAI 产品。
集中式服务还为我们提供了单一的安全控制点,用于实施最强的数据安全和隐私原语。Habitat 服务是我们集中执行访问控制策略、执行审计日志以及限制对底层存储资源(如 Azure Cosmos DB)访问的地方。Habitat 在保护用户数据以及防止外部、内部和代理行为者的未授权访问方面发挥着关键作用。
大规模启动 Python 服务
我们知道需要一个服务,但我们还不想从 Python 迁移出去,尽管 Python 作为服务端有额外的开销。与本地库执行相比,在高吞吐量服务中使用 Python 会增加网络延迟,并带来显著的 CPU 和内存扩展成本。此外,我们认识到 Python 的低效在 100 倍规模下是不可接受的,因此最终几乎肯定会进行重写。
然而,我们将这视为技术债务的战略入侵。我们当时的主要目标不是成本或资源优化,而是为产品开发者解除阻碍,实现平台稳定。通过接受 Python 服务在短期的性能权衡,我们能够优先处理更紧迫的挑战,建立核心 API,并构建健壮的基础设施。
将 Habitat 作为 Python 服务运行在性能上不是最优的,但却是必要的选择。Python 让我们快速行动,但这不意味着我们可以不顾一切地接受显著更高的延迟。当平均用户请求导致数百次数据库调用时,用户感受到的是最慢的那次数据库调用。我们发现,在这种规模下运行 Python 服务的主要挑战在于管理这些尾部延迟。
追踪 asyncio 延迟
Asyncio 帮助 Python 并发执行 I/O 密集型工作负载,但不能帮助绕过 Python GIL 提供 CPU 并行。除了 I/O 密集型的请求代理,Habitat 还承担许多 CPU 密集型职责和后台任务:路由、压缩、加密、校验和、下游健康检查、请求影子和对冲。
由于我们的服务中有如此多的 CPU 密集型工作负载和后台任务,asyncio 调度延迟很容易主导请求的尾部延迟。在为初始服务发布进行调优之前,我们在延迟为 p99 及更高的请求跟踪中看到:虽然下游存储响应很快,但请求经常在等待负责解析响应的协程被重新调度时卡住。
并发不是 CPU 并行
Python asyncio 允许并发请求处理,但一次只有一个请求在 CPU 线程上执行。当需要大量 CPU 工作时,这对请求延迟影响很大。
| CPU 请求/响应处理 | Python 网络读/写 | 等待 Cosmos |
|---|---|---|
| 低 CPU 工作量 | ||
| 短暂的 Python 步骤;I/O 等待重叠 | ||
| 高 CPU 工作量 | ||
| 长的 Python 步骤使就绪响应等待 | ||
说明性时间:0.0 / 40 说明性单位
对于 OpenAI 的 Python 服务,我们发现除了测量内存、CPU、网络和磁盘使用的标准利用率和饱和度指标外,还必须监控 asyncio 循环及其繁忙程度,然后进行相应的调优。
通过定期调度后台任务并记录预期和实际执行时间之间的差异,我们能够实时经验性地测量事件循环调度延迟。在高利用率下,即使是每个进程大量并发请求中的 modest 数量,也足以产生显著的调度抖动,最高可达数百毫秒,在某些边缘情况下甚至达数秒。
因此,我们采用每个进程只服务少量并发请求的策略,而是大幅扩展 Python worker 进程的数量。
减少功能开关配置中的尾部延迟
在初始服务发布中,我们通过在线服务 CPU 分析发现了一个 asyncio 延迟高(以及随之而来的高尾部延迟)的根本原因:通过 Statsig(一个管理功能开关的工具,也可用于运行 A/B 测试等)定期解析 JSON 格式的功能开关配置。
默认情况下,Statsig 配置为每分钟轮询刷新配置,且没有抖动,该配置包含跨每个服务的所有生产规则。在其他地方,一个架构决策是每个 pod 最多运行 8 个 Python 进程,以提高 CPU 使用率并降低延迟。结合这意味着每分钟每个 pod 都会在某个时刻,其所有 worker 停止处理飞行中的请求,而是花费 CPU 周期解析一个巨大的配置文件。
一旦 CPU 分析帮助我们确定了问题根源,修复就很简单了:部署一个更小更有针对性的配置,延长刷新时间,并为这些后台任务添加一些抖动。
平衡负载和管理连接池
为了保持低 asyncio 延迟,在服务器进程之间保持良好的请求负载平衡也很重要;连接池如果不经过调优,实际上可能与此目标背道而驰。
通过客户端侧连接池,一个发出许多并发请求的单一客户端进程可能只建立少数几个服务器连接,结果是其所有负载只发送到少数几个进程。在调整负载平衡方式之前,我们服务的利用率差异很大,有些尾部进程服务的并发请求数是平均水平的 5-10 倍。
我们在一场意外事件中发现了这个问题:尽管我们停止了向服务部分过载的客户端,但部分进程的性能下降持续了很长时间。事实上,我们注意到那些进程经历了失控的退化,接收到的请求越来越多,直到我们重启它们。一旦 pod 过载,某些行为将更多流量绑定到已过载的 pod。这是我们一些队友在之前的工作中很熟悉的一类故障:元态故障(metastable failure)。
我们怀疑连接池是罪魁祸首,并通过限制最大连接重用时长来测试这一假设,这确实限制了退化,证实了我们的调查方向。进一步调查发现,Python 的 aiohttp TCPConnector 默认使用 LIFO 连接重用:最近返回的连接被选中用于下一个请求。这通常是一个合理的默认值:重用最近的连接允许处理突发流量的额外连接空闲超时,减少维护额外连接的开销。在这种情况下,这为我们创造了元态故障。在请求突发期间,发送到较慢过载服务器的请求稍后返回到连接池,因此随后请求更频繁地选择它们,逐渐将更多流量集中到已经不堪重负的 pod 上。将连接池修补为使用 FIFO 重用打破了这个反馈循环,甚至降低了我们稳态请求差异。
LIFO 将新工作发送回慢进程
在请求突发后,较慢的服务器最后返回连接。LIFO 鼓励将更多工作集中到那些相同的慢服务器上。
初始突发到达 A、B 和较慢的进程 C。
FIFO 打破连接重用反馈循环
FIFO 在突发后保持更多活动连接,但在所有服务器之间公平地平衡工作负载。
初始突发到达 A、B 和较慢的进程 C。
今天,我们主要依赖 Istio 和 Envoy 在整个 OpenAI 基础设施中提供连接池和更好的服务器负载感知平衡策略,完全避免了这个问题。
避免淹没下游资源
低 asyncio 延迟调优和拥有如此多的 Python 进程的一个副作用是,我们很容易用大量连接(称为「雷鸣般的羊群」)压垮下游依赖项。
如果不经调优而缓慢进行常规每日部署,可能会导致连接循环产生显著的 CPU 消耗。连接泄漏可能会通过饱和 NAT 网关而使网络瘫痪。这些对其他服务来说也是常见问题,但由于进程数量多出一个数量级,触发阈值显著降低,通常会饱和客户端在纯吞吐量稳态下不期望需要处理的网络相关资源。
我们还依赖 Envoy 来最大化我们的连接扇入。我们用它将 Python 的 HTTP/1 连接升级到 HTTP/2 以利用多路复用,然后池化这些连接并延长连接生命周期。Envoy 还为我们提供了一个实施速率限制和断路器的中心位置,这些在每个独立的 Python 进程中效果会较差。
相同的请求,更少的连接
连接池和 HTTP/2 连接多路复用有助于减少下游的连接负载。
| 请求 | 响应 | 空闲 keep-alive |
|---|
为什么 Habitat 做得更少
我们能够将 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 迁移到 Rust
将 Python 重写推迟一年,使我们能够在高速增长期间专注于更紧迫和更有影响力的挑战。随着平台成熟,我们的增长持续加速,Habitat 成为 OpenAI 核心数第二大服务(Envoy 足迹排名第四),是时候超越 Python 了。在其巅峰时期,Python 帮助我们每秒服务超过 2000 万次请求。
2026 年第二季度,仅凭 2 名工程师、Codex 和 GPT-5.5,我们能够用 Rust 重写整个服务。这个新的 Rust 服务现在处理 95% 的生产请求;我们将在未来几周内完全弃用 Python。我们的数据显示,Rust 服务比 Python 版本 CPU 效率高 6 倍,内存效率高 15 倍,平均延迟和尾部延迟都显著降低。我们计划在未来的博客中分享更多经验。
优化数据库层 Azure Cosmos DB
Python——现在是 Rust——服务只是 Habitat 的一个方面。在本系列文章的第二部分解释我们如何快速扩展在线存储以服务超过 10 亿 ChatGPT 用户时,我们将讨论存储层以及 Habitat 如何每秒服务超过 500 PB 数据和超过 7000 万次请求。