OpenAI存储平台:毫秒内响应40区域10亿用户的读请求
OpenAI的所有产品都需要快速、可靠地访问数据,这由我们的存储平台Habitat提供。它必须在毫秒内响应横跨40个地理区域的超过10亿用户。我们的基础设施面临一项非凡挑战:在持续提升可靠性、性能和产品开发者速度的同时,支持爆发式增长和新用例。解决这一挑战需要重新思考我们的在线存储平台的设计与运营方式。我们不断权衡取舍,遵循这样的模式:首先变得足够可靠以承载关键任务型产品流量,然后变得足够快速以满足全球用户,最后,在大规模下灵活运营。
本系列文章分为上下两篇,讲述我们如何将Habitat从一个简单的客户端库和数据库发展成一个服务超过500PB数据的全球分布式存储平台。上篇聚焦于Habitat从Python客户端到服务的演进,也介绍了让我们得以将基于Python的服务推向远超许多人认为可行程度的技术。
在这篇下篇中,我们介绍如何扩展底层存储组件:Azure Cosmos DB。通过与Azure团队的紧密合作,我们找到了一种可行的多租户方案——共同管理的容器——在全球大规模下实现了出色的读性能。
图01 · Habitat是什么?
在线存储平台
Habitat是我们构建的在线存储平台,使OpenAI产品能够快速可靠地访问所需信息。
请求
响应
变更(CDC)
大规模下多租户的可靠性
随着公司规模化,基础设施团队通常会从运行单租户系统演进到多租户系统。单租户模式下,每个产品面团队维护自己的系统,缩小了爆炸半径,提供了清晰的归属边界和工作负载隔离。但随着团队和产品面的增长,这种模式运行起来变得昂贵。单租户让每个产品团队都要负责容量规划,往往导致大量过度预置。
相反,多租户将控制权交给集中式的数据库团队,他们拥有更丰富的知识和经验来预置资源。但多租户也带来自身的问题,比如吵闹邻居问题:一个租户消耗异常多的资源份额,损害其他租户的利益。
在Habitat中,我们必须权衡所有这些取舍,以设计一个大规模下可靠、高效的存储系统。如今,Habitat是许多OpenAI产品的多租户数据库层,但我们经历了一段循环往复的旅程:从使用一个多租户Azure Cosmos DB容器,到预置单租户后端容器,再回到默认使用多租户容器。我们在Habitat上的多租户之路仍处于非常早期阶段:我们支持大量用例,但到目前为止,我们一直能够通过尽可能利用Azure Cosmos DB的内置原语来推迟对这一领域的投入。
Azure Cosmos DB容器简介
在介绍我们的Azure Cosmos DB之旅之前,先提供一些基本背景:
一个Azure Cosmos DB账户定义一组容器和一套复制策略。
每个容器按每秒请求单位(RU/s)预置一定量的吞吐量。
每个容器内的数据以典型的按指定逻辑分区键分片方式存储,每个逻辑分区键有文档化的存储和吞吐量上限。
Azure Cosmos DB负责将逻辑分区键分片到较少的后端物理分区上;当存储超过阈值,或容器需要更多物理分区来维持预置吞吐量时,会将其拆分为多个物理分区。
第一阶段:依赖Azure Cosmos DB原语
最初,我们允许任何客户端负载通过,依靠每个逻辑分区键10K RU/s的既定上限来提供背压。起步时,我们在每个账户中也只使用一个默认的Azure Cosmos DB容器。这套做法起初还算够用,但最终因几个原因暴露出问题:
逻辑分区键限制
由于早期的API选择,我们直接将逻辑分区键的选择暴露给客户端。结果,在不同产品用例之间,我们频繁在user_id或workspace_id等常用分区键上发生碰撞。由于这些用例在相同逻辑分区键上共享Azure Cosmos DB的上限,该设计限制了一个给定用户的总体吞吐量。
在迁移到Habitat和Azure Cosmos DB之前,我们扩展Postgres时遇到的一个常见问题是:来自某个超热组织或热键——一个产生不成比例高负载的租户——的大量请求可能拖垮整个数据库。我们投入了大量集体努力来缓存这些键,并在缓存未命中时构建租约机制,以免缓存未命中带来的负载冲击底层数据库。
图02a · 逻辑分区键限制
不同的客户端,同一个分区键
吵闹邻居
虽然文档化的数据大小和吞吐量上限适用于逻辑分区键,但实际上限是在物理分区层面执行的。实践中,这意味着逻辑分区无法达到10,000 RU/s。相反,它们与该物理分区上的所有其他逻辑分区键共享物理分区的上限。
这使吵闹邻居能够影响许多无关的逻辑分区键——运行在冷门产品功能数据上的工作负载会扰乱核心的登录和注册流程。
图02b · 吵闹邻居
不同的用户,同一个物理分区
弱分区
特定物理分区的吞吐量上限也是可变的,取决于分配给该Azure Cosmos DB容器的总吞吐量。容器的预置吞吐量大致均匀地分布在该容器的各物理分区之间。
随着数据量增长,物理分区会拆分,分区总数增加,而总预置吞吐量保持不变。结果是:每个分区的有效吞吐量上限随着数据规模增大而缩小。
对于我们存储量最大、类似blob的用例,这造成一种故障模式:仅存储增长本身就会降低活跃工作负载的吞吐能力,即便请求速率和使用量保持稳定。
图02C · 弱分区
上限降到需求以下
分区达到存储上限时发生拆分。每个子分区获得一半的吞吐能力。
第二阶段:尝试工作负载隔离与单租户
独立的容器有助于部分解决分区键限制和吵闹邻居问题。使用独立容器后,每个容器可以被分配自己独立的物理分区,只有该容器自身的变化工作负载才可能成为吵闹邻居或发生分区键空间重叠。缺点方面,预置新容器存在不可忽略的额外开销——每个容器都需要个性化的容量规划——因此我们希望只在有意义的服务或用例边界上创建容器。
我们开始为最需要强隔离的关键新用例使用独立的Azure Cosmos DB容器。再配合一个管理弱分区的脚本——将每个容器的总预置吞吐量与其物理分区数量同步——我们基本避免了此前遇到的所有问题。
然而,由于我们从默认容器起步,许多遗留的关键用例不幸困在那里,无法进行数据迁移。为了保护仍然依赖共享默认容器中数据的关键服务,我们经常建议团队为新的服务或大型产品功能创建独立容器。随着规模扩大,默认容器上的压力在增长,生产事故也在增多。我们开始为任何新服务或大型产品功能预置容器,仅仅是为了保护默认容器免受更多负载。
这让我们避免了许多全站性故障,将爆炸半径限制在功能或服务降级范围内。然而,预置新容器的开销最终还是找上了我们:
缺乏稳定的聚合增长
在旧的共享默认容器中,某个新用例从0 QPS增长到100万QPS不太可能显著改变总聚合用量。然而在独立容器上,一个新用例从0增长到100万QPS,可能意味着相比仅内部自测的基线水平高出1,000倍的负载。
有了专用容器后,团队现在需要记得在发布前将容器预置到预期的吞吐水平。产品团队经常将功能发布到生产环境后忘记预先扩容,于是我们在发布日坐在SEV作战室里,最后一刻手忙脚乱地扩容容器。
工作负载混合带来的聚合稳定是多租户系统的核心优势,而采用这种容器策略换取更好隔离的同时,我们失去了这一优势。
图03 · 缺乏稳定的聚合增长
相同的工作负载,不同的空间余量
负载测试时1K RU/s → 发布时1M RU/s
容量规划
随着时间推移,监控专用容器的用量还需要对产品有深入了解,知道流量何时会变化或增长。
我们可以提供一个基本的控制平面脚本,根据增长趋势为每个容器扩展预置吞吐量,但如果没有稳定的聚合增长,要正确扩容就意味着OpenAI几乎每个产品团队都要参与容量规划。
这在组织层面无法扩展,各个服务所属团队对自身Azure Cosmos DB预置投入的用心程度差异悬殊。
第三阶段:默认多租户,具备数据移动能力
随着组织壮大,这些运维问题变得难以扩展。没有单一的中央团队做规划,结果是许多团队规划不善。最终,我们转向回归多租户方案,并与Azure团队密切合作,通过共享租户设计来修复我们最初的多租户问题:
热键限流
优先限流最热的键,可以防止吵闹邻居耗尽某个物理分区的全部可用吞吐量。这削弱了吵闹邻居效应——一个热键可能产生全站级爆炸半径。
服务质量(QoS)
请求分级QoS同样防止批量插入或数据提取等离线工作负载耗尽某个物理分区的全部吞吐量。通过为这些工作负载的请求分配较低优先级,它们会先被限流,从而避免限流到服务终端用户的高优先级交互请求。
Serverless模式
Azure Cosmos DB原本已有serverless模式,本可以修复我们在预置模式下遇到的弱分区问题。但它有一个重大缺点:只基于存储用量进行扩展,并将每个物理分区的RU用量限制在5K RU/s。这能解决我们的弱分区问题,但会阻止我们扩展RU密集型用例。
Azure Cosmos DB团队为我们构建的一种改进版serverless模式,允许每个物理分区扩展到10K RU/s,并在基于存储用量的拆分之外引入了基于RU用量的拆分,让我们得以停止手动监控和扩容容器。
借助这些变更,我们得以回归使用一个共享容器,这大大简化了产品团队进行任何前期或持续容量规划的需求,同时保持了工作负载之间的隔离。此后数月,尽管产品开发在加速,我们再也没有发生过任何预置或吵闹邻居事故。
以共同管理服务模式提升可靠性
当公司成长到OpenAI的规模,托管服务就变得不那么“托管”了。许多工作负载成为云供应商基础设施中的离群值。随着OpenAI成为Azure Cosmos DB最大的用户之一,成功运营它需要一种不同的模式:共享测试、共享仪表板、直接工程沟通渠道,以及对仅在我们这种规模下才会出现的故障模式的共同负责。如今,我们最大的Azure Cosmos DB账户存储数十PB数据,服务超过2,000万QPS。据我们所知,我们是唯一一家将Azure Cosmos DB部署到如此规模的公司。
2024年底,一次区域性故障清楚地证明了这种协作的必要性。当时,停电袭击了我们最大的Azure Cosmos DB账户之一所在的Azure主区域。在随后的故障转移演练中,我们意识到最大的账户需要数小时才能故障转移到次要区域——这是我们团队和Azure Cosmos DB团队都没有在我们这种规模下测试过的场景。
大约一个月后,当我们将这些账户故障转移回原始区域时,又触发了一次严重故障。对原始区域的连接踩踏压垮了该区域的Azure Cosmos DB网关。gateway模式的Azure Cosmos DB客户端在处理故障转移时的一个怪癖,导致所有客户端立即建立连接并向原始区域发送请求,尽管当时只有一部分物理分区已移回该区域。
在每次故障中,OpenAI和Azure都执行了各自的事件管理流程,识别出多个根因并部署了修复。但这些故障是一个更大系统性问题的症状,问题出在我们团队与Azure Cosmos DB团队的运营和测试方式之间。起初,利用托管服务让我们能够快速行动、专注于业务目标。到了大规模阶段,托管服务变得不那么“托管”,需要在从设计到运营的各个层面与服务提供商深度集成。
如今,Habitat和Azure工程团队共同监控Habitat的每一层,从我们的服务层一直深入到Azure Cosmos DB的物理存储层。我们在一个生产规模但非生产的Azure Cosmos DB账户上运行和测试变更。我们一起测试和迭代SDK,共同确定如何扩展和规划Azure Cosmos DB账户规模的最佳实践。我们通过Teams可以直接联系到对方工程师,甚至建立了无需经过支持层级就能直接互相呼叫的途径。
过去一年里,双方展开了大量联合工作,以改进Azure Cosmos DB控制平面和网关的可扩展性:在网关中增加负载保护和各种Azure Cosmos DB元数据的缓存,以保护Azure Cosmos DB元数据分区;通过连接池和更高的连接复用来减轻我们Python habitat-service进程集群带来的压力;尽可能将工作负载放在一起;通过不同的VPC将关键工作负载与离线工作负载隔离。
这种级别的协同工程对过去一年我们在Azure Cosmos DB上的稳定性至关重要。任何足够大或足够关键的系统都需要这种团队协作:打破所有权壁垒,跨组织边界运营。
提升全球规模的性能
正如上篇所提到的,我们的核心哲学是:先让它跑起来,再让它变快,最后让它变便宜。可靠性优先,然后才是性能和效率。Habitat已经成为OpenAI的关键基础设施,因此首要任务是确保它的可靠性。随着系统在2025年趋于稳定、OpenAI在全球扩张,延迟成为用户和产品团队的下一个约束。
计算和存储容量往往位于不同区域,导致跨区域请求不可避免。
图04 · 提升全球规模的性能
全球流量
计算和存储容量往往位于不同区域,导致跨区域请求不可避免。
计算
存储
示意路径 · 路径越长耗时越久
计算和存储的地理位置仅为示意。
延迟差异巨大,取决于数据存储在哪里、请求在哪里被服务。为了让Habitat请求达到良好性能,产品开发者依赖额外的客户端缓存、请求批处理、最小化数据库读写,以及尽可能将工作移出关键路径。这虽然催生了一些对数据库问题更具韧性的有趣产品设计,但也让产品变得更加复杂并拖慢了产品迭代:这未能实现Habitat的目标——鼓励快速产品开发,且无需理解数据库。
这是我们在2026年的一个重点,我们制定了一个分层策略来优化读性能——读请求占Habitat请求的90%以上:
在容量允许时将数据存储在用户附近
我们在世界各地的不同Azure区域中有多个Azure Cosmos DB账户,用于满足有数据驻留要求的用户。即使对于没有硬性数据驻留要求的用户,将数据放在用户附近也能通过更低的延迟改善体验。
实际上,在数据驻留用例之外,我们无法广泛实现这一点,因为放置位置受限于物理云容量。由于我们的数据集规模庞大,许多区域无法容纳按用户流量比例所需分配的巨量存储空间。
在数据中心容量、电力或冷却有限的区域,我们有时面临直接取舍:预留容量用于就近存储、降低Habitat延迟,还是将该容量用于更多服务计算、将存储请求路由到更远的地方。
因此,放置决策往往是产品层面的取舍:A)为不同产品和用户子集优化延迟,还是B)原始存储和计算容量。
仅广泛复制最小且对延迟最敏感的数据集
对于延迟最敏感的工作负载,例如每次请求都需要的认证元数据,我们通过使用具有许多次要区域的高度复制Azure Cosmos DB账户来降低计算与Habitat之间的RTT。一些账户复制到我们服务流量的全部40个Azure区域;其他账户至少在每个大洲复制到一个区域。
我们不得不在比预期更早的时间在Habitat中构建透明的数据移动和路由层,以便我们能够在具有不同复制拓扑的账户之间移动数据集。
然而,这种方法最终也受容量约束,因此仅限于少数精心挑选的数据集。
其余一切依赖集成缓存
Habitat的工作负载绝大多数是读密集型的,应用程序不断获取设置、状态和其他属性;不成比例的流量来自一个相对较小的、频繁访问类型的工作集。这使得该系统特别适合分布式缓存。虽然缓存在单区域基础设施中能提升性能,但在跨区域场景中效果会叠加——缓存可以被用来“隐藏”跨区域延迟。
我们已将分布式旁路缓存层集成到Habitat中。它在我们基础设施中靠近用户请求处理位置部署的Valkey服务器上存储最近访问的对象和边的副本。通常,这类缓存使用TTL,在命中率和陈旧性之间权衡。短TTL能降低数据陈旧性,但会削弱缓存的正面效果,因为条目会很快过期并被更频繁地重新获取。长TTL能提高命中率,但会在较长时间内提供陈旧数据,导致较差的用户体验和不可预测的产品行为。
另一种方法是在应用程序对相同底层数据执行写操作时执行显式失效操作。这能大幅提升缓存数据的新鲜度,但很难做到正确。它仍然面临许多边缘情况,例如部分失败会导致缓存陈旧。多区域部署会加剧这些问题:跨区域网络可靠性更低,缓存还要与底层存储的复制赛跑。用TTL作为补充可以将最大陈旧性限制在一定范围内,但仍然存在上述TTL权衡的固有困境。
我们采用了更先进的失效方法,追随Meta TAO等系统的脚步。Habitat的集成缓存依靠通过Azure Cosmos DB变更源的变更数据捕获(CDC)在数秒内全球传播失效,实现高命中率同时保持低陈旧性。我们基础设施中的一个服务消费这个变更源,当底层数据更新时对本地缓存执行失效。这导致下次读取数据时缓存未命中,然后缓存被填入最新版本的数据。
用Rust降低代理开销
我们还通过将代码重写为Rust来消除Habitat服务自身的开销,从而显著降低延迟——Rust的性能优于原来的Python。
综合起来,这些变更将我们的全球读p50延迟从大约120毫秒降至大约10毫秒,自我们今年早些时候开始聚焦性能以来实现了12倍的提升。
图05 · 提升全球规模的性能
读请求p50延迟
三项重大设计变更改善了读延迟
虽然缓存大幅降低了中位延迟,但最慢的请求仍然需要从底层存储获取未缓存数据,而且往往跨越大距离区域。随着更多容量上线以及我们提升存储效率,我们计划解决一个更根本的挑战:从一开始就将数据放置在更靠近访问位置的地方。
扩展变更数据捕获
可靠且可扩展的CDC——将数据库变更传播到下游系统——不仅是Habitat缓存策略的关键组件。每秒数百万条捕获的变更,来自数千个容器和数百万个容器分区,流经一百多个Kafka主题,为最终一致的二级索引、数据移动管道、离线分析和其他实时产品功能提供动力。随着我们的后端基础设施扩展至全球,监听行变更变得越来越复杂。如果客户端想要在某种类型的行发生变更时注册一个回调,他们必须(除其他事项外)先确定哪些Cosmos账户和容器包含他们的数据、各自区域中哪个CDC服务部署正在监听这些容器,并正确扩展CDC服务工作线程集群、Kafka以及他们的消费者。我们的CDC平台旨在将这一切从产品开发者那里抽象掉,取而代之提供一个简单的界面:客户端注册一个希望在变更行上被调用的函数,我们处理所有管道和扩展工作。
一个内部控制平面持续确保所有必需的容器都在被捕获,选择从哪些Cosmos区域读取,并在跨多个地理区域的数千个工作线程之间高效分配工作。使用副本区域和较低优先级的feed读取,可以保护对延迟敏感的Habitat流量免受后台捕获工作负载的影响。在如此大规模的服务集群中,工作线程崩溃、容器问题、租约存储错误、无效配置和目标端故障持续发生。我们加固了CDC工作线程的生命周期和租约处理,隔离了流之间的故障,并为配置变更和发布构建了防护机制。我们利用死信队列防止永久性投递失败造成队头阻塞。我们通过将读取器切换到备用Cosmos区域来容忍区域故障。在我们这种规模下,我们甚至不得不修改Azure的CDC SDK,通过将检查点在内存中批量保存、仅定期刷新,使其检查点实现更加高效。
虽然Habitat对Azure Cosmos DB的内部使用很复杂,但它一贯的价值主张是向产品开发者抽象掉基础设施细节,以提升产品开发速度。我们的CDC平台也不例外。
展望未来
一个小团队之所以能够支撑一个快速增长的存储平台,靠的是刻意保持存储API的狭窄、持续扩展Python服务,以及优先处理最紧迫的工程挑战。我们首先聚焦关键任务产品流量的可靠性,然后聚焦全球用户的读性能,最后聚焦大规模下的运营效率。这种方法在提升可靠性、性能和生产开发速度的同时,实现了快速增长和新用例。
为了将Habitat和会话存储扩展到下一个十亿用户,我们需要解决在更大规模上跨基础设施复制数据的难题。为了提前应对这一问题,我们一直在构建一个自研的EB级在线存储系统,针对占用空间大的Habitat数据类型进行了优化。这个开发中的系统结合了来自Rockset的RocksDB Cloud核心与基于Raft的复制。它实现与Azure Cosmos DB相同的文档模型,但使用分离式存储和物理复制来减少所需的闪存容量,换取较低的峰值QPS。
如果你有兴趣在前沿规模上参与这类数据基础设施工作,欢迎查看我们的开放职位。