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

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

图 01 · 什么是 Habitat?

在线存储平台

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

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

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

  • 7000 万+

每秒请求数

  • 10 亿+

每周人数

  • 500 PB+

数据

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

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

什么是 Habitat?

Habitat 始于一个简单的想法:产品工程师不应该需要思考数据库管理。Habitat 最初在 2023 年 DevDay 上为支持 GPTs 而推出,是一个与 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 作为服务会带来额外开销。与本地库执行相比,将 Python 用于高吞吐量服务会增加网络延迟,并增加大量 CPU 和内存扩展成本。此外,我们认识到 Python 的低效在 100 倍规模下将不可接受,这使得最终重写几乎不可避免。

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

我们还做了一个有根据的赌注,即我们自身编码模型的快速进步将在未来简化技术路径。我们押注于当完全迁移出Python成为必要时,Codex和GPT将使这一迁移变得可行。这一赌注最终被证明是正确的。

以Python服务运行Habitat在性能上并非最优,但却是必要的选择。Python让我们能够迅速行动,但这并不意味着我们可以不顾后果,接受明显更差的延迟。当平均用户请求导致数百次数据库调用时,用户感受到的是最慢的那次数据库调用。我们发现,在如此规模下运行Python服务的主要挑战在于管理这些尾部延迟。

跟踪asyncio延迟

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

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

图03 · 跟踪asyncio延迟

并发不等于CPU并行

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

CPU请求/响应处理 Python网络读写 等待Cosmos

低CPU工作

简短的Python步骤;I/O等待重叠

高CPU工作

长时间的Python步骤使已准备好的响应等待

示意时间 0.0 / 40 示意单位

对于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。

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

避免淹没下游资源

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

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

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

图 05 · 连接扇入

相同的请求,更少的连接

连接池和 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 重写推迟一年,使我们能够在超高速增长期间专注于更紧迫、更有影响力的挑战。随着平台日趋成熟、增长持续加速,并且它已成为 OpenAI 按核心数计算的第二大服务(按我们的 Envoy 占用计算为第四大),终于到了超越 Python 的时候。在巅峰时期,Python 帮助我们每秒服务超过 2000 万次请求。

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

优化我们的数据库层:Azure Cosmos DB

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

本文内容采集自官方网站,排版和翻译可能与原页面存在差异。

阅读官方全文