Habitat 存储层扩展
本文是本库基于 OpenAI 《Rapidly scaling online storage to serve over 1 billion ChatGPT users(Part II)》对 Habitat 存储层的归纳,不是一套另有专名的通用框架。核心问题是:共享存储如何既不被热点拖垮,也不把容量规划重新推给每个产品团队;数据分布全球后,又如何降低读取延迟。
原文覆盖超过 500 PB 数据、40 个地域的在线存储平台;其中最大的 Azure Cosmos DB 单个账户 存储数十 PB、承载超过 2,000 万 QPS。这是账户级指标,不等同于 Habitat 全平台请求量。
多租户:共享、独占,再回到默认共享
Cosmos DB 账户定义容器及复制策略;容器按 RU/s(每秒请求单位)配置吞吐。客户端选择逻辑分区键,底层再把多个逻辑分区映射到物理分区。原文提到的每逻辑键 10K RU/s 上限不是独享吞吐保证,实际限制在物理分区执行,多个键仍会争用预算。逻辑分区键不同,不代表物理资源已经隔离。
| 阶段 | 做法 | 解决的问题与代价 |
|---|---|---|
| I:默认共享容器 | 允许客户端流量进入,依赖 Cosmos DB 分区限制施加背压 | 接入简单,但键碰撞、物理分区上的邻居干扰和存储增长引发的吞吐下降会波及核心业务 |
| II:按服务或用例独占容器 | 为重要业务分配独立物理分区,并用脚本随分区数调整容器吞吐 | 缩小故障影响面,却要求各产品团队预测上线流量、提前扩容 |
| III:默认共享,保留数据迁移能力 | 热键限流、请求优先级、改进的 serverless 模式,配合数据移动与路由 | 把容量规划收回平台,同时限制热点与后台任务对在线请求的影响 |
第一阶段暴露了三种不同机制,不能都归为“热用户”:
- 同一个逻辑键被不同用例复用:Conversations、Auth、Memory / dreaming 都使用同一个
user_id值,竞争同一份分区预算。图 02a 用后台记忆任务占用大多数容量格、其他请求收到429来演示。 - 不同逻辑键落在同一个物理分区:即使用户不同,也可能争抢吞吐。图 02b 中
user-id-28104与user-id-10384共享物理分区,前者的热点影响后者。 - weak partitions:固定的容器吞吐大致均分给物理分区;存储增长触发分裂、分区数量增加后,每个分区分到的吞吐反而下降。请求量没变,也可能开始被限流。
图 02c 是最后一种失效的示意图,不是实测时间序列:目标键所在分区的容量从 10K 降为 5K RU/s,键整体仍位于一个物理分区;需求维持 6K RU/s,于是成功请求从 6K 降为 5K,限流部分从 0 升为 1K RU/s。图中容器总预算仍有 20K RU/s,说明“总量有余”不等于目标分区有余。
独占容器为什么增加上线风险
共享池中的新业务突增,可能只占总量很小一部分;在新建的小容器里,同一突增却可能远超容量。图 03 用以下示例分配,非生产测量 说明区别:
| 指标 | 共享容器 | 新独占容器 |
|---|---|---|
| 总容量 | 100M RU/s | 10K RU/s |
| 新业务进入前的使用量 | 80M RU/s | 0 |
| 剩余容量 | 20M RU/s | 10K RU/s |
| 压测需求 1K RU/s 占剩余容量 | 0.005% | 10% |
| 上线需求 1M RU/s 占剩余容量 | 5% | 10,000% |
两行需求都是相对新业务进入前的余量计算,上线需求替代压测需求,二者不相加。原文另用 QPS 描述业务流量增长,图中则以 RU/s 表示容量需求;二者不能直接换算。
独占容器缩小了事故半径,却丢失共享池混合负载的平滑效应。团队若忘记预扩容,就会在上线当天临时抢救。平台团队即使提供自动脚本,也很难替所有产品预测发布时间和流量跃迁。
回到共享的前提是补隔离机制
- 先限最热的键,防止单键吃掉物理分区的全部预算。
- 按请求做 QoS 分级:批量插入、数据导出等离线负载先被限流,交互请求优先。
- 让扩容同时响应存储与 RU 使用量:原有 serverless 模式只按存储扩展,每物理分区上限为 5K RU/s;Azure 团队为 OpenAI 改进的版本允许每物理分区达到 10K RU/s,并加入 RU 使用量触发分裂。不能据此断言所有公开 serverless 产品都具备相同能力。
OpenAI 报告,回到默认共享后的数月内未再出现容量配置或邻居干扰事故。这是该平台的观察窗口,不是零事故保证。
超大规模下,托管服务需要共同运营
2024 年末,主地域停电后,团队发现最大账户切换到备用地域需要数小时;双方此前都没在这个规模验证过。约一个月后回切,又发生连接风暴:gateway-mode 客户端立即向原地域建立连接并发送请求,尽管只有部分物理分区已经迁回,最终压垮网关。
改进不限于“供应商修 bug”,而是重建协作方式:
- 用生产规模的非生产账户共同测试变更与 SDK;共享从 Habitat 到物理存储层的监控。
- 直接建立工程沟通与互相呼叫值班人员的渠道,不再逐级经过支持工单。
- 为控制面、网关与元数据分区加入负载保护和元数据缓存;通过连接池与复用减少海量 Python 进程的连接压力。
- 尽量同置工作负载,并用不同 VPC 隔离关键在线业务与离线业务。
这里的经验是规模使客户成为云服务的异常负载,设计、测试、运行都需要跨组织承担责任,而不只是约定一个服务等级。
全球读取:数据放置、选择性复制、就近缓存
读取占 Habitat 请求的 90% 以上。延迟问题首先来自计算与存储跨地域,优化分四层:
- 容量允许时,把数据放在用户附近。数据驻留需求已有地区化账户,但并非所有地区都能容纳对应流量比例所需的数据;电力、制冷与机房容量可能迫使平台在本地存储和更多服务计算之间取舍。这与 AI 基础设施 的物理约束直接相连。
- 只广泛复制小而关键的数据集。例如每次请求都需要的认证元数据;部分账户复制到全部 40 个服务地域,另一些至少覆盖各大洲。为此 Habitat 提前建设了透明的数据移动与路由层,在不同复制拓扑的账户间迁移数据,而不是复制所有数据。
- 其余数据依赖集成缓存。在请求处理侧附近部署 Valkey,缓存最近访问的对象与边,以旁路读取方式隐藏跨地域延迟。
- 用 Rust 降低服务层代理开销。这是 Habitat 服务演进 的延续,不是数据库本身换成 Rust。
图 05 标出“关键数据复制到邻近地域 → 区域内缓存上线 → Rust 重写上线”三个里程碑。整体全球读取 p50 从约 120 ms 降至约 10 ms,约 12 倍改善。原文没有披露各措施的独立收益,不能把 12 倍全归于缓存或 Rust。慢请求仍可能跨地域读取未缓存数据,p50 改善也不能推导为同幅度的尾延迟改善。
缓存失效依赖 CDC,而非只调 TTL
短 TTL 降低陈旧程度,却频繁回源;长 TTL 提高命中率,却延长旧数据可见时间。写请求显式删除缓存也会遭遇部分失败,以及缓存填充与跨地域底层复制之间的竞争。
Habitat 参考 Meta TAO,用 Cosmos DB change feed 捕获变更,再在全球数秒内传播失效。本地缓存被删除后,下次读取回源并填入新值。这是异步新鲜度机制,不代表强一致读取,也没有消除所有失效竞争;原文没有给出完整的一致性协议或最大陈旧时间保证。
flowchart LR
W["写入 Cosmos DB"] --> F["Change feed"]
F --> C["CDC 服务"]
C --> I["传播到各地并删除对应缓存"]
R["本地读取"] --> V{"Valkey 命中?"}
V -->|"是"| H["返回缓存值"]
V -->|"否"| D["读取底层存储并回填"]
I -.-> V
图为机制示意,不表示每次读取都等待 CDC,也不规定 CDC 内部所有传输环节。
CDC:从变更流到产品侧回调
CDC 还支撑最终一致的二级索引、数据迁移、离线分析与实时产品功能。原文披露的规模是:每秒数百万条变更、数千容器、数百万容器分区、超过一百个 Kafka topic;控制面在多个地域给数千 worker 分配捕获任务。
产品侧只注册“某种类型的数据变更时调用哪个函数”,无需自行定位账户、容器、地域部署和 Kafka,也不必逐层扩容。平台背后负责:
- 持续检查所需容器都已进入捕获范围,选择读取地域,分配 worker 工作。
- 从副本地域读取,并降低 feed 读取优先级,保护在线请求。
- 加固 worker 生命周期和租约处理,隔离不同流的故障,为配置变更和发布设置保护。
- 用死信队列绕开永久投递失败,避免队首阻塞;地域故障时切换到其他 Cosmos 地域读取。
- 修改 Azure CDC SDK,将 checkpoint 先在内存批量累积、定期刷出,降低检查点开销。
原文没有承诺端到端 exactly-once;“回调注册简单”也不等于下游无需考虑重复消费或恢复语义。
下一步:以峰值吞吐换闪存效率
OpenAI 还在开发面向大体积 Habitat 数据类型的 EB 级在线存储:结合来自 Rockset 的 RocksDB Cloud 核心与 Raft 复制,保持与 Cosmos DB 相同的文档模型,采用存储解耦和物理复制,减少闪存容量需求,代价是较低的峰值 QPS。它仍是开发中系统,原文没有宣称已全面替代 Cosmos DB,也未给出成本或吞吐对比数字。