Files
nexus/sreweekly/markdown/532/06-how-uber-conquered-database-overload-the-journey-from-static-rate-limi-zh.md

211 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Uber 如何征服数据库过载:从静态限流到智能负载管理
- **期号**: SRE Weekly Issue #532(2026-08-31)
- **作者**: Dhyanam Vaidya, Prathamesh Deshpande, and Mike Ma — Uber
- **链接**: https://www.uber.com/us/en/blog/from-static-rate-limiting-to-intelligent-load-management/
## 简介
这篇有大量精彩细节,讲他们的配额管理方案如何失败、又如何一步步迭代。
## 正文
# Uber 如何征服数据库过载:从静态限流到智能负载管理
# 引言
Uber 的数千个微服务为超过 1.7 亿月活用户处理流量:乘客、Uber Eats 用户、司机和骑手。这些基础设施的核心是 [Docstore](https://www.uber.com/us/en/blog/schemaless-sql-database/?uclick_id=0ef55d57-e714-4820-8ab3-c3f30875480f) 和 [Schemaless](https://www.uber.com/us/en/blog/schemaless-part-one-mysql-datastore/?uclick_id=0ef55d57-e714-4820-8ab3-c3f30875480f)——Uber 基于 MySQL® 自研的分布式数据库。这些数据库横跨数千个集群,存储数十 PB 的运营数据,每秒服务数千万个请求,读写数十亿行。它们支撑着 Uber 对时延最敏感、最关键的任务负载,驱动着 Uber 的每一个业务线:从出行、配送、地图到支付,不一而足。
在这个规模上,即使轻微的过载也不是孤立事件,它们会级联。系统某一部分的短暂尖峰可以向外扩散:下游服务超时、重试堆积、性能退化放大成更大范围的故障。在多租户环境中,确保公平、防止某个租户独占全部资源同样至关重要。由于工作负载在流量形态、时延画像和对系统的影响上千差万别,构建有效的过载保护是一个极具挑战性的问题。
过载保护搞错的代价是高昂的。这篇博客分享我们如何构建了一个智能负载管理器:从多个信号检测过载,让数据库在压力下保持稳定与公平。
## Docstore 与 Schemaless
在进入保护 Uber 数据库的负载管理器之前,先快速过一遍它们的架构。
虽然 [Docstore](https://www.uber.com/us/en/blog/schemaless-sql-database/?uclick_id=0ef55d57-e714-4820-8ab3-c3f30875480f) 支持带完整 CRUD 操作的事务,[Schemaless](https://www.uber.com/us/en/blog/schemaless-part-one-mysql-datastore/?uclick_id=0ef55d57-e714-4820-8ab3-c3f30875480f) 针对追加型(append-only)工作负载做了优化,但两者共享共同的架构基础。它由三个主要层组成:无状态查询引擎、有状态存储引擎和控制面。限于本文范围,我们聚焦查询引擎和存储引擎两层。
无状态查询引擎负责查询规划、请求路由、分片、模式管理、授权、请求解析与校验。它充当路由层:在把客户端请求交给存储层之前进行协调和校验。
有状态存储引擎处理事务管理、连接池、共识与复制。数据被分片到多个分区,每个分区由一个 leader 和两个 follower 组成,通过 [Raft](https://www.scs.stanford.edu/~zyedidia/docs/papers/raft.pdf) 协调以保证强一致性。每个分区由挂载本地 NVMe SSD 的 MySQL 节点支撑,为大规模的高吞吐、低时延工作负载而构建。
## 挑战
### 查询引擎层的基于配额限流
最初,我们探索了在无状态查询引擎层做基于配额的限流。概念很简单:根据处理的字节数给每个读和写请求分配一个容量单位成本,给用户发放固定配额,配额用尽时返回 429。由于路由节点是无状态的,我们把配额用量存在一个集中式 Redis® 缓存里。概念上合理,但在生产中撑不住。
首先,它增加了不必要的复杂性。每个请求都要一次 Redis 调用,引入了新的故障点和一次额外的网络跳数开销。
其次,无状态路由层要准确地为过载的存储分区丢弃请求,就需要维护系统中数千个分区的实时健康与负载信息。这带来了大量跟踪开销,削弱了架构的可扩展性。
成本模型也太粗糙。在 Docstore 和 Schemaless 中,由于 MySQL 处理扫描和过滤的方式,一个执行全表扫描却只返回一行的查询,与只读一行的查询被分配了相同的容量成本。这个计量上的根本缺陷意味着轻量操作和重量操作被一视同仁,使配额执行变得不可靠。
最后,配额是静态定义的,导致干系人频繁要求调整配额,在多租户环境中形同虚设。
尽管初衷美好,这个方案还是失败了。但它给了我们一个关键洞见:过载管理必须尽可能贴近存储节点。这个认识成为最终设计的基石——把过载保护放在有状态存储层。
### 识别正确的过载信号
设计一个健壮负载管理器的核心挑战,是选择可靠的过载信号。简单的基于 QPS 的限流太粗。它无法适应工作负载的波动,往往丢得太晚或太早。更有效的是并发度:当前在途的操作数。它直接反映系统负载,遵循 Little 定律:*并发度 = 吞吐量 × 时延*。在有状态系统中,它与资源使用高度吻合,是更可靠的指标。
### 平衡韧性与公平
在多租户系统中平衡韧性与公平是核心挑战。面临全局性压力时,我们希望按优先级丢弃流量,先丢低优先级请求。但当单个吵闹租户独占资源、却还没触发全局过载时,我们也需要独立于系统负载的按租户限流。这个双重要求促使我们把动态过载检测器与公平性执行机制并行组合。
## 构建统一负载管理器的基础
### 受控时延:压力下的智能排队
负载卸载之旅始于 [CoDel](https://queue.acm.org/detail.cfm?id=2209336)(受控时延,Controlled Delay),一个从网络领域借鉴来对抗"缓冲膨胀"(bufferbloat)的概念。CoDel 不按队列长度丢弃,而是看请求在队列里等了多久:以响应性为先,而不是以数量为先。
我们为每种操作类型实现了独立的 CoDel 队列:
- **读队列**:点查和轻量查询
- **写队列**:insert、update 和 upsert 操作
- **慢队列**:长时运行和后台操作,如扫描、删除或复制
每个队列独立管理,让我们在不同工作负载之间获得更好的隔离。
FIFO 排队还不够,因为纯 FIFO 按到达顺序处理请求,这在流量稳定时表现良好。但过载时,FIFO 会形成一个陷阱:旧请求不断堆积、等待过久,往往被客户端丢弃或重试,造成浪费。与此同时,仍然相关、很可能成功的新请求却在队尾闲置。
CoDel 引入自适应 LIFO 来解决这个问题。正常负载下,队列表现为 FIFO;压力之下,它切换为 LIFO,优先处理还有成功机会的新请求。这个简单的转变通过快速失败、丢弃过期工作、给新请求优先权,改善了响应性。
### Scorecard 引擎
Scorecard 引擎是一个基于规则的准入控制组件和轻量配额系统,用于在多租户环境中执行按租户的并发度限制。负载卸载在过载时保护系统,Scorecard 则确保即使在正常条件下,也没有单个租户能主导共享基础设施。
配置简单且确定。
Scorecard 的主要价值在于事故遏制。它帮助在宕机或流量尖峰期间定位干扰源头。它隔离并限制行为不当的租户而不影响其他租户,在正常负载下平衡稳定性、压力下实施严格限制,并通过快速、确定性地执行边界来缩小过载事件的爆炸半径。
Scorecard 提供了可预测的公平性和爆炸半径控制,尤其是当多个租户争夺共享资源时。
### 调节器(Regulators)
Scorecard 防范的是基于并发度的过度使用,但它没有覆盖有状态数据库系统可能过载的所有方式。有些倾斜行为很隐蔽,不会体现在并发度饱和上,但任其发展仍会拖累系统性能。
例如,一个低 QPS 的调用方可以通过发送大的写入负载搞垮系统。或者,流量倾斜到某个分区键,能让单个集群过载而其他集群闲置。
为防范这些倾斜行为,我们引入了插件式调节器:节点本地的过载检测器,执行系统不允许违反的不变量。健康运行期间它们几乎不触发,这是设计使然。与此同时,当用户意外制造热点或大规模数据摄取时,调节器介入以防止级联故障。
我们用这些调节器:
- **写字节调节器**:限制并发写入量,防止 I/O 饱和
- **分区键调节器**:节流指向热点分区键的流量
- **内存调节器**:跟踪进程空闲内存,内存不足时节流
- **Goroutine 调节器**:跟踪 goroutine 总数,超过阈值时节流
### 哪些做得好
通过丢弃多余请求,我们的 CoDel 队列阻止了失控的资源耗尽,带来了更高的稳定性和更高的被接受请求成功率。这种方法在确保过载期间核心系统功能仍然可用方面特别有效。
Scorecard 引擎通过执行按租户的并发限制,成功隔离了行为不当的租户,让我们能快速遏制吵闹邻居造成的干扰而不用惩罚其他用户,确保共享资源被公平使用。
### 局限
虽然这套初始设计为过载保护和公平性打下了基础,但它有几个局限。首先,CoDel 对所有请求一视同仁,丢弃低优先级流量和面向用户流量时没有差别,导致糟糕的客户体验和更高的值班负担。
CoDel 还依赖固定的队列超时和静态的在途并发上限,对动态系统来说是低保真方案,需要频繁手动调优,带来运维辛劳。
CoDel 中固定、静态的等待时间导致了惊群效应。请求最终被拒绝时,它们会同时重试,触发一轮又一轮过载与拒绝的循环。这些时期缺乏流量差异化,连高优先级请求也被丢弃,造成用户可见的错误,放大了爆炸半径。
归根结底,它没有让系统崩掉,但缺少高品质用户体验所需的细腻与动态。这凸显了对动态、优先级感知队列的需求。
## 架构演进
### Cinnamon 取代 CoDel
我们观察到许多过载源于低优先级的异步任务:管道、聚合器和内部垃圾回收流程。它们不应该和乘车请求或实时定价查询拥有相同的存活优先级。
为解决这个问题,我们用 [Cinnamon](https://www.uber.com/us/en/blog/cinnamon-using-century-old-tech-to-build-a-mean-load-shedder/) 取代了 CoDel——一个由 Uber Delivery 团队开发的优先级感知负载卸载器。Cinnamon 通过考虑请求等级、动态系统状态和工作负载的相对重要性,做出更聪明的卸载决策。
请求等级来自请求附带的优先级;如果没有显式优先级,Cinnamon 会根据调用方服务分配一个默认值。优先级用分层模型定义:tier 0(t0)是最关键的流量,tier 5(t5)是最不关键的。t0 预留给一小部分关键基础设施服务,t1 代表最重要的面向用户的在线流量——过载时我们核心要保护的工作负载。这套系统让 Cinnamon 在过载时优先卸载低优先级流量。
有了请求优先级感知,我们把队列结构简化为只有读和写队列。长时运行和后台操作改为标记低优先级,而不是单独一个队列。
Cinnamon 之前:CoDel 队列卸载器与优先级无关,过载时卸载不分青红皂白。
Cinnamon 之后:队列卸载器具备优先级感知,过载时按优先级顺序卸载。
### 性能与稳定性收益
基于 Cinnamon 的设计带来了性能和稳定性收益。请求被分级,Cinnamon 可以先丢低优先级流量,保护面向用户的链路。过载期间,关键用户请求得到更好保护,影响最小。
Cinnamon 还使用 P90 时延指标自适应调整队列超时阈值,免去手动调优。此外,它的 [Auto Tuner](https://www.uber.com/blog/cinnamon-auto-tuner-adaptive-concurrency-in-the-wild/) 动态调整在途上限(图 10 蓝框中显示的可用槽位),以最大化吞吐量。它持续监控并响应实时时延和错误率信号,确保稳定有效的负载卸载。
与 CoDel 的静态方法不同——固定等待时间(比如 5 毫秒)后激进地拒绝所有请求——Cinnamon 基于 [PID 的控制](https://www.uber.com/us/en/blog/pid-controller-for-cinnamon/) 让系统吸收压力而不过度反应。它根据实时时延和错误信号动态调整队列超时和在途上限,只在必要时卸载。这避免了一大类过早卸载——否则会导致不必要的拒绝、重试和惊群效应。结果是更平滑的恢复、更少的 429,以及不损害系统健康的前提下更稳定的可用性。
### 待改进之处
尽管 Cinnamon 带来了收益,仍有一些关键挑战,凸显了对统一平台的需求。
负载管理器基于服务器的本地健康采取行动,跟踪在途并发、写字节、内存使用等信号。但在分布式系统中,过载不总是局部的。leader 节点可能因为 follower 滞后而需要卸载流量,即使它自己很健康。我们称之为提交索引滞后(commit index lag)。传统上,外部组件用基于令牌桶的限流器处理这种远端卸载决策。它们容易构建,但在规模上被证明无效,会引入脑裂行为和全局次优的卸载决策。
初始设计擅长基于并发度的卸载,但它不是为了成为可复用平台而建的,无法承载一个不断增长的系统未来必然涌现的新过载信号。
这些洞见引领我们走向系统的最终演进:把 Cinnamon 从纯并发度卸载器,转变为一个真正通用的过载控制引擎。通过把所有信号整合进一个模块化的决策回路,我们实现了整体、一致的过载管理。
## 统一负载卸载引擎
### 集中过载决策
我们增强了 Cinnamon,支持可插拔的外部信号,比如 follower 提交滞后,让系统能在同一条准入控制路径内做出全局知情、优先级感知的卸载决策。这一转变把本地和远程过载逻辑统一到单一控制回路中,弥合了此前造成不稳定的缺口。
但卸载并不总是一刀切的决策,这正是负载管理器架构大放异彩之处。它建立在 BYOS(Bring Your Own Signal,自带信号)理念之上,提供了一个可插拔框架,让团队嵌入新的过载信号并把它们路由到正确的控制路径。无论压力是系统性的还是针对某个调用方的,负载管理器都根据信号按优先级广泛卸载,或按调用方精确卸载。
### 回报:统一控制,简化负载管理
转向集中、可插拔的架构让系统更稳定、更可预测,带来了实实在在的成果。
Cinnamon 用 PID 控制器立即卸载多余请求,避免了令牌桶限流器造成的内存和 goroutine 堆积。这带来了更低的尾部时延和更精简的资源用量画像,即使在重负载下也是如此。我们看到:
- 过载下吞吐量提升 80%(QPS 均值 5,400 对比 3,000)
- P99 时延降低约 70%(upsert 均值 1.0 秒对比 3.1 秒)
- 过载期间 goroutine 减少约 93%(峰值 10,000 对比 150,000)
- 堆内存占用降低约 60%(峰值 1GB 对比 5-6GB 尖峰)
我们还看到更平滑、更可预测的卸载行为。没有 PID 调节,卸载像一把锤子:反应式、突兀。有了它,更像一个调光开关:平滑、稳定。对比提交滞后在令牌桶限流器下与 Cinnamon 的 PID 控制器下的稳定方式,差别一目了然。
## 经验教训
- **优先级至上。** 有效的负载卸载始于决定什么最重要。先保护关键的、面向用户的流量。其他一切都是次要的。
- **快速失败,不要阻塞。** 尽早拒绝几乎总是好过把请求留在内存里直到过期。它减少浪费的工作,保持时延可预测,防止 OOM,让系统在压力下更有韧性。
- **用 PID 调节实现稳定卸载。** 仅基于当前错误率的简单反应式卸载往往导致不稳定,纠正得太晚、太猛。PID 调节通过纳入系统历史与趋势方向带来平衡,是平滑、持续、有韧性的过载控制的关键工具。
- **把控制放在靠近真相源的地方。** 最好的卸载决策发生在状态所在之处。保护应该放在拥有完整上下文的层——在有状态系统中通常是存储层。
- **拥抱动态性。** 尽可能避免静态配置。你的系统应该足够聪明,能根据上下文适应不同场景。
- **投资可观测性。** 良好的可观测性是一切调优与信任的基础。跟踪什么被卸载、为什么被卸载、每个组件如何促成系统压力。
- **简单胜过复杂。** 这是一条指导所有其他决策的元原则。
# 结论
我们通往健壮负载管理器的旅程,是由大规模、有状态、分布式环境的独特复杂性定义的。通过把零散组件统一进一个决策大脑、采用 Bring Your Own Signal 模型,我们获得了同时精准处理系统性过载和局部吵闹邻居问题的灵活性。结果是:一个按优先级更聪明地卸载、尾部时延更低、运维辛劳大幅减少的负载管理系统。
如果你喜欢分布式系统、数据库、存储和缓存相关的挑战,欢迎[在这里](https://www.uber.com/us/en/careers/list/?query=storage&department=Engineering)申请我们的开放职位。
## 致谢
这样规模的项目很少能靠一个人完成。我们衷心感谢 Rich Porter、Jesper Nielsen、Piyush Patel,以及 Storage 和 Delivery 两个团队的工程师们在整个旅程中的指导与协作。从设计评审到值班洞见,他们的贡献对构建这个现在守护着 Uber 最关键基础设施的系统至关重要。
*封面图片署名:"[Heavy Traffic Jam in Urban City Center](https://www.pexels.com/photo/heavy-traffic-jam-in-urban-city-center-32487428/)",作者 [Dapur Melodi](https://www.pexels.com/@dapur-melodi-192125/)*
*MySQL 是 Oracle 和/或其关联公司的注册商标。其他名称可能是其各自所有者的商标。*
*Redis 是 Redis Labs Ltd. 的商标。其所有权利归 Redis Labs Ltd. 所有。本文中的使用仅出于指代目的,不表示 Redis 与 Uber 之间存在任何赞助、背书或关联。*
Dhyanam Vaidya
Dhyanam Vaidya 是 Uber 存储平台团队的软件工程师。他参与了许多 Docstore 功能的设计与实现。他的工作聚焦于提升 Uber 分布式数据库在规模上的可靠性、韧性与运维效率。
Prathamesh Deshpande
Prathamesh Deshpande 是 Uber 存储平台团队的高级工程师(Staff Engineer),构建满足 Uber 全球可靠性与性能要求的数据库功能和分布式存储系统。他的工作聚焦于大规模数据管理、分布式数据库存储系统和平台可靠性。
Mike Ma
Mike Ma 是 Uber 存储平台团队的高级软件工程师(Staff Software Engineer),为 Schemaless 和 Docstore 的多个核心组件做出了贡献。他的工作聚焦于 Uber 大规模分布式数据库的可扩展性、可靠性、性能与卓越运维。
Chaitanya Yalamanchili
Chaitanya Yalamanchili 是 Uber 存储平台团队的高级经理兼技术负责人。他领导在线分布式存储系统的开发,致力于提供一个支撑 Uber 所有关键业务功能与业务线的一流平台。该平台每秒服务数千万 QPS,存储数十 PB 的运营数据。