2026年9月11日
工程实践
快速扩展在线存储以服务于超过10亿ChatGPT用户
我们如何将应用存储平台Habitat从Python适配,以应对前所未有的增长。
作者:Jon Lee、Chaomin Yu 和 Ben Ries,技术团队成员
收听文章
17:57
分享
什么是Habitat?
构建服务以更好地支持多个复杂产品
大规模部署Python服务
追踪asyncio延迟
降低功能标志配置中的尾部延迟
平衡负载与管理连接池
避免淹没下游资源
为什么Habitat做得更少
从Python迁移到Rust
优化数据库层,即Azure Cosmos DB
每个OpenAI产品都依赖于快速、可靠的数据访问,无论是用户登录、检查Codex设置,还是在ChatGPT中开始新对话。这些操作中的每一个在产品展示响应之前,可能都需要多次单独的数据查询。如果这些请求缓慢,产品就会感觉迟缓;如果这些请求失败,产品将完全无法工作。
Habitat是我们构建的在线存储平台,旨在让OpenAI产品能够快速、可靠地访问所需信息。Habitat目前每秒处理超过7000万次请求,支持每周被超过10亿人使用的产品,覆盖近40个地理区域。Habitat最初于2023年DevDay上发布,用于支持GPTs,起初只是一个连接到单个数据库的简单Python客户端库。如今,它已成为一个复杂的分布式系统,服务于超过500PB的数据。
图01 · 什么是Habitat?
在线存储平台
Habitat是我们构建的在线存储平台,旨在让OpenAI产品能够快速、可靠地访问所需信息。
暂停
请求
响应
变更(CDC)
客户端
在线存储平台
存储资源
ChatGPT
API
Codex
内部服务
以及更多
Habitat
缓存
缓存
ACL策略
授权
放置与数据驻留
数据驻留
加密
数据安全
隔离
多租户
速率限制
请求整形
路由
模式查找 · 数据驻留
Azure Cosmos DB
在线存储
Nanobase
在线存储
Valkey
缓存
Blob存储
存储资源
CDC服务
变更数据捕获
Databricks
Rockset
Kafka
以及更多
以这种规模构建和运营基础设施并非易事,但也并非特别具有挑战性。使我们的情况独特的是,为了支持惊人的用户增长和产品需求,同时构建一个成熟的平台,我们不得不以前所未有的速度进行扩展。通常,系统工程师会为10倍规模的扩展而构建,并希望其在未来几年内保持稳定,同时为下一个10倍规模做准备。而在我们的案例中,过去三年我们每年的增长都超过了10倍。因此,构建和运营Habitat是一系列战术决策和排序的过程:从最低层面理解每个组件,以榨取现有栈的最大潜力,同时抵御存储和计算容量的瓶颈,为基础性投资争取时间。
70M+
每秒请求数
1B+
每周用户数
500 PB+
数据量
随着OpenAI的成长,Habitat也随之成长:首先变得足够可靠以支持关键产品流量,然后足够快速以满足全球用户的需求,最后,能够灵活地在巨大规模下运营。这篇文章是两篇文章系列的第一篇,介绍我们如何扩展在线存储。在本文中,我们将分享Habitat的演变过程、我们为何将其从库转变为服务,以及我们如何将一种非主流服务栈语言——Python——编写的服务拉伸为一个可靠的存储平台层。
在未来的文章中,我们将详细介绍如何实现大规模下的多租户可靠性、优化读取性能的分层策略,以及如何扩展我们与Azure Cosmos DB的合作关系,以可靠地应对前所未有的需求。
什么是Habitat?
Habitat 源于一个简单的理念:产品工程师无需操心数据库管理。Habitat 最初在 DevDay 2023 上发布,旨在支持 GPTs,当时它只是一个与 ChatGPT 主服务器交互的小型 Python 库。它支持一组有限的操作,这些操作在底层映射到数据库应用程序 Azure Cosmos DB。
该库的任务是为产品团队提供一种简单的方法来存储和检索数据,而无需掌握底层细节。Habitat 负责处理必要的工作:确定涉及的数据类型、数据来源(或去向)、请求是否被允许等。
产品工程师无需关心模式查找、路由、授权、加密、序列化、请求整形和连接池。他们甚至不需要考虑数据来自何处:是 Azure Cosmos DB、缓存还是其他类型的存储。
图 02 · HABITAT 服务
简化的 Habitat 请求流程
暂停
请求
响应
客户端
OPENAI
AZURE COSMOS DB
Habitat 客户端 SDK
Envoy
habitat-service
进程 1
habitat-service
进程 2
habitat-service
进程 3
habitat-envoy
habitat-cosmos-db-us0
habitat-cosmos-db-us1
habitat-cosmos-db-eu0
这个 Python 库运行良好,尽管没有集中推动放弃使用自助式 Postgres 和 Azure Cosmos DB,Habitat 在 OpenAI 的产品工程师中仍迅速普及。
随着产品需求的发展,产品开发者甚至很容易向共享库中添加对客户端缓存、压缩或加密等功能的支持。
构建服务以更好地支持多个复杂产品
到 2025 年中,Habitat 作为客户端实现已达到其极限。随着 Habitat 层变得越来越复杂,且 OpenAI 的服务数量增加,向后兼容的协议更改变得不可行。
在一例中,我们希望通过将最关键的数据集迁移到一组区域分布式的 Azure Cosmos DB 账户,来减少任何单个区域故障的影响范围。做出这一更改需要在客户端引入额外的路由逻辑,该逻辑通过功能标志禁用,确保其部署到所有客户端,然后启用该功能标志。
协调数十个服务的部署并与每个团队合作进行部署花费了数天时间。在启用此功能之前,我们意识到需要引入一些影子测试(shadowing),以确保分片逻辑正确无误。这又花了几天时间来部署。修复我们发现有误的漏洞?又是几天时间。最终,我们准备好启用该标志时,其中一个团队却因无关原因将其服务回滚到之前有缺陷的客户端版本,导致了我们竭力避免的故障。
对客户端库的更改需要在数十个服务之间进行复杂的协调,这一过程被证明越来越脆弱、低效且容易受到操作故障的影响。为了减少未来部署的操作扩散,我们决定将 Habitat 整合为独立的服务。
通过将存储逻辑解耦为独立服务,我们为部署、可观测性和平台增强建立了单一的控制点。与其管理碎片化的更新,我们可以集中实施改进,从而立即惠及所有 OpenAI 产品。
集中式服务还为我们提供了提供最强数据安全和隐私原语的单一瓶颈点。Habitat 服务是我们能够集中执行访问控制策略、执行审计日志记录并限制对 Azure Cosmos DB 等底层存储资源访问的地方。Habitat 在保护用户数据和防止来自外部、内部以及智能体行为者的未授权访问方面发挥着关键作用。
大规模部署 Python 服务
我们清楚需要一项服务,但即便作为服务会带来 Python 的额外开销,我们也并不想现在就迁移出 Python。与本地库执行相比,使用 Python 构建高吞吐量服务会增加网络延迟,并带来显著更高的 CPU 和内存扩展成本。此外,我们认识到,在 100 倍规模下,Python 的低效性将不可接受,因此最终的重写几乎不可避免。
然而,我们将此视为技术债务的战略介入。当时我们的主要目标并非成本或资源优化,而是解除产品开发的阻塞并实现平台稳定性。通过接受短期内的 Python 服务性能折衷,我们能够优先处理更紧迫的挑战,建立核心 API,并构建稳健的基础设施。
我们还做出了一项经过计算的赌注:我们自身编码模型的快速进步将使未来的技术路径变得简单。我们押注,当需要完全迁移出 Python 时,Codex 和 GPT 将使这种迁移成为可能。这一赌注最终被证明是正确的。
以 Python 服务运行 Habitat 在性能方面并非最优,但却是必要的选择。Python 让我们能够快速行动,但这并不意味着我们可以抛却谨慎,接受明显更差的延迟。当平均用户请求导致数百次数据库调用时,最慢的那次数据库调用就是用户感知到的延迟。我们发现,在这个规模下运行 Python 服务的主要挑战在于管理这些尾部延迟。
追踪 asyncio 延迟
Asyncio 有助于 Python 并发执行 I/O 密集型工作负载,但并不能帮助绕过 Python GIL(全局解释器锁)并提供 CPU 并行性。除了繁重的 I/O 请求代理外,Habitat 还处理许多 CPU 密集型职责和后台任务:路由、压缩、加密、校验和计算、下游健康检查、请求影子测试(shadowing)以及对冲(hedging)。
由于服务中存在如此多的 CPU 密集型工作负载和后台任务,asyncio 调度延迟很容易主导尾部请求延迟。在针对初始服务发布进行调优之前,我们在 p99 及更高延迟的请求追踪中看到,尽管下游存储响应迅速,但请求经常因等待负责协程被重新调度以解析响应而停滞。
图 03 · 追踪 asyncio 延迟
并发不等于 CPU 并行性
Python asyncio 允许并发处理请求,但在任何给定时刻,CPU 线程上仅执行一个请求。当有大量 CPU 工作需要完成时,这对请求延迟影响巨大。
重播
暂停
CPU 请求/响应处理
Python 网络读写
等待 Cosmos
低 CPU 工作
短暂的 Python 步骤;I/O 等待重叠
Python:请求 A · CPU 请求处理
时间 →
0
10
20
30
40
Python 线程
A
A
B
B
C
C
请求 A
CPU 请求
COSMOS
请求 B
等待第一个 Python 周期
COSMOS
请求 C
等待第一个 Python 周期
COSMOS
Cosmos + 网络等待:两种场景下每个请求均为 6 个单位。
高 CPU 工作
漫长的 Python 步骤使就绪响应处于等待状态
Python:请求 A · CPU 请求处理
时间 →
0
10
20
30
40
Python 线程
A
A
B
B
C
C
请求 A
CPU 请求
CPU 请求
COSMOS
就绪 · 阻塞
CPU 响应
请求 B
等待第一个 Python 周期
CPU 请求
COSMOS
就绪 · 阻塞
CPU 响应
请求 C
等待第一个 Python 周期
CPU 请求
COSMOS
就绪 · 阻塞
CPU 响应
Cosmos + 网络等待:两种场景下每个请求均为 6 个单位。
示意时间
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 上。这是我们一些队友从以往工作中非常熟悉的一类故障:亚稳态故障(metastable failure)。
我们怀疑是连接池的问题,并通过限制最大连接重用时长来验证这一猜测,结果确实限制了降级情况,并证实了我们的调查方向。进一步调查发现,Python 的 aiohttp TCPConnector 默认采用 LIFO(后进先出)连接重用策略:即选择最近返回的连接用于下一个请求。这通常是一个合理的默认设置:重用最近的连接可以让为处理突发流量而创建的额外连接空闲超时,从而减少维护额外连接的开销。在这种情况下,它却导致了我们的亚稳态故障。在请求激增期间,对较慢的过载服务器的请求会更晚地返回连接池,因此被后续请求更频繁地选中,逐渐将更多流量集中在已经挣扎的 Pod 上。修补连接池以使用 FIFO(先进先出)重用以打破这一反馈循环,甚至也降低了我们稳态下的请求方差。
图 04A · 客户端侧连接池
LIFO 将新工作发送回慢速进程
在请求激增之后,较慢的服务器最后将连接返回到连接池。后进先出(LIFO)策略会导致更多的工作集中在这些相同的慢速服务器上。
重播
暂停
初始的请求突发到达 A、B 以及较慢的处理进程 C。
01
初始突发
02
重用连接
03
结果
客户端进程
B3 = 到服务器进程 B 的连接 3。每个进程都有 t