本文是对 Uber's Rate Limiting System 的整理与翻译。
内容结构概览
本文会围绕 Uber 如何构建大规模全局限流系统展开,主要包括:
- 为什么 Uber 需要统一限流系统
- 早期微服务限流为什么碎片化、难维护、难扩展
- 为什么 Redis 这种中心化计数器模型无法支撑 Uber 的规模
- Uber 为什么把限流能力嵌入 Service Mesh
- GRL,也就是 Global Rate Limiter 的三层架构
- 数据面 client、zone aggregator、regional/global controller 分别做什么
- 为什么热路径必须本地决策,不能每个请求访问控制面
- 最初的 token bucket 方案如何工作
- token bucket 在大规模生产中暴露出的公平性和突发流量问题
- drop-by-ratio 概念:按全局超额比例概率丢弃请求
- 为什么 Uber 最终从 token bucket 迁移到统一概率丢弃模型
- GRL 如何配置 caller、procedure、global/regional/zonal 限流规则
- shadow mode 为什么是大规模限流上线的关键
- GRL 上线后的收益:延迟下降、Redis 依赖移除、可靠性提升
- 一次关键服务如何扛住 15 倍流量突增
- GRL 如何在 DDoS 和 retry storm 中保护内部系统
- 为什么手工 YAML 配置又成了新的问题
- RLC,也就是 Rate Limit Configurator,如何自动生成限流配置
- RLC 如何基于历史流量、预测模型和固定配额生成安全限额
- 自动化配置如何把 GRL 从"静态保护层"变成"自维护控制系统"
- Uber 下一步如何继续缩小 blast radius、提高更新频率并结合应用层 throttler
- 这套系统对普通后端工程师和平台团队有什么启发
一、Uber 为什么需要全局限流系统?
Uber 是一个典型的大规模微服务系统。
它的服务化架构每秒处理数亿级 RPC 请求,背后是数千个服务之间的复杂调用关系。任何一个服务、调用方、客户端或者异常流量源,如果没有控制,都可能把下游服务打爆,并进一步引发级联故障。
限流在这里不是一个"锦上添花"的功能,而是平台可靠性的基础设施。
它要解决的问题包括:
text
防止单个调用方压垮被调服务
防止异常客户端消耗过多共享资源
防止 retry storm 放大故障
防止突发流量穿透到核心服务
在服务降级时保护关键路径
在事故处理中快速压制特定流量
Uber 最终构建了 GRL,也就是 Global Rate Limiter。它直接集成到 Uber 的 service mesh 中,服务网格会转发 Uber 大多数服务之间的 RPC 请求,因此在这一层做限流可以覆盖绝大部分内部服务流量。
后来,GRL 又继续演进出 RLC,也就是 Rate Limit Configurator。RLC 会根据历史真实流量和策略,自动更新限流配置,让限流规则不再依赖大量人工维护。
所以这篇文章讲的是两个阶段:
text
第一阶段:用 GRL 统一限流执行
第二阶段:用 RLC 自动化限流配置
最终目标是:
让 Uber 的微服务平台在极大流量、突发峰值、重试风暴和攻击流量下,仍然能保持稳定、低延迟和可控。
二、为什么限流很重要?
在分布式系统里,限流是一种基础保护机制。
它的核心作用不是"拒绝用户",而是保护系统在压力下还能工作。
没有限流时,一个服务可能遇到这样的情况:
text
上游流量突然增加
下游延迟变高
上游开始超时
超时后上游重试
重试又带来更多请求
下游更慢
更多请求超时
系统进入雪崩
限流就是要在系统还没彻底崩掉之前,主动丢弃超出容量的请求,让核心服务保持可用。
普通限流系统通常会用一个分布式计数器,比如 Redis。
基本逻辑是:
text
每个请求到来
↓
根据 caller 或 endpoint 生成 key
↓
在 Redis 中对这个 key 计数
↓
如果计数超过阈值
↓
拒绝请求
对很多中小规模系统来说,这个方案简单有效。
但 Uber 的问题在于规模远远超过普通系统。
三、早期 Uber 的限流为什么碎片化?
在 Uber 微服务架构早期,并没有一个统一限流系统。
不同团队根据自己的需求实现自己的 throttling 逻辑。有些写在业务代码里,有些写在自定义 middleware 中,有些使用 Redis 作为中心计数器。
这些实现单独看都能解决局部问题,但放在整个公司规模下,就会出现系统性缺陷。
1. 配置不一致
不同团队对"限流"的理解不同。
同样是每秒 1000 请求,有的系统按 caller 限,有的按 endpoint 限,有的按实例限,有的按 Redis key 限。
结果就是:
text
同样的 quota,在不同服务里行为不同
同样的调用方,在不同服务里被保护方式不同
事故发生时,很难判断到底是谁在限流
2. 运维成本高
Redis-based limiter 需要维护 Redis 集群。
这意味着:
- Redis 本身要部署
- 要做分片
- 要做高可用
- 要做监控
- 要处理延迟
- 要处理跨区一致性
- 要处理 Redis 客户端库升级风险
限流本来是为了降低系统风险,但它自己又引入新的基础设施风险。
3. 修改限流规则需要发版或人工操作
很多早期限流逻辑和业务代码绑定。
如果要改限制值,可能需要重新部署服务。
这在事故处理中非常糟糕。
当某个服务正在被打爆时,你不可能慢慢改代码、走发布流程、等待部署完成。
限流系统应该能做到:
text
无需改业务代码
无需重新部署服务
几秒内改变限流行为
早期分散方案做不到这一点。
4. 维护风险高
碎片化限流逻辑最大的风险是:
事故发生时,没人知道它在哪里。
某个服务突然返回大量 429,到底是业务限流?middleware 限流?Redis 限流?网关限流?调用方限流?还是下游服务错误?
如果限流代码散落在不同仓库、不同框架、不同语言里,排障就会非常困难。
5. 覆盖不均匀
一些大型服务可能认真做了限流。
很多小服务可能完全没有限流。
但在微服务系统中,小服务也可能处在关键链路上。
一个小服务被打爆,也可能拖垮一条核心业务路径。
所以平台需要统一覆盖,而不是靠每个团队自觉实现。
6. 可观测性不统一
每个限流器上报指标方式不同。
有的报 drop count,有的不报;有的报 error,有的报 429;有的记录 caller,有的不记录 procedure。
这会导致一个问题:
当请求失败时,很难判断失败是不是由限流导致的。
统一限流系统必须提供统一指标、统一日志、统一错误语义和统一排障入口。
四、为什么 Redis 中心计数器无法支撑 Uber 规模?
Redis 作为限流计数器,在很多系统里很常见。
但 Uber 发现,它无法支撑自己的全局规模。
原因很直接:Uber 有数十万 hosts,每秒处理数亿级请求,并且跨多个 region。
如果每个请求都要访问 Redis 做计数,那么热路径就会变成:
text
请求到达服务网格
↓
访问 Redis
↓
读取/更新计数器
↓
判断是否超过 quota
↓
继续转发或拒绝
这有几个严重问题。
1. 每个请求多一次网络往返
限流本来应该是极轻量的热路径逻辑。
但 Redis 计数意味着每个请求都多一次远程访问。
在低延迟 RPC 系统里,这个成本非常高。
2. Redis 集群数量会爆炸
如果要支撑数亿请求每秒的实时计数,就需要大量 Redis 集群。
还要按 region、zone、caller、endpoint 分片。
这会带来巨大的运维复杂度。
3. 跨 region 一致性几乎不可接受
限流经常需要"全局视角"。
比如某个 caller 在多个 region 发流量,系统希望限制它的全局请求量。
但 Redis 计数器如果分布在多个 region,就会遇到跨地域一致性问题。
实时一致太贵,不实时又不准。
4. 定期同步计数也不够
一种折中方案是:
每个地方本地计数,然后定期同步。
但 Uber 也排除了这个方案。
因为定期同步会导致数据过时。
如果某个 caller 突然暴涨,系统可能几秒甚至更久后才感知到。
这对突发流量和 retry storm 不够快。
最终 Uber 得到结论:
中心化计数器无法同时满足低延迟、全局规模和实时性。唯一可行方向是完全分布式架构:本地代理在热路径做决策,控制面只负责聚合和下发策略。
五、愿景:统一的基础设施级限流器
Uber 最终选择把限流能力集成到 service mesh。
这个选择非常关键。
Service mesh 位于服务间 RPC 流量的基础设施层。
绝大多数服务间请求都会经过它。
把限流放在这一层,带来几个好处:
text
语言无关:不管服务用什么语言,都能被保护
框架无关:不依赖具体业务框架
无需业务改代码:服务只需要配置,不需要改逻辑
覆盖面广:所有经过 mesh 的 RPC 都可以被评估
热路径可控:可以在 proxy 内本地做快速决策
统一观测:指标、错误、drop ratio 都能统一上报
Uber 的目标是:
为任意服务提供统一限流能力,让团队可以按 caller、procedure 或两者组合配置 quota,而且不需要改业务代码。
但这个目标必须满足巨大规模:
text
每秒数亿请求
数万个 service pair
跨多个地理区域
对延迟影响极小
控制面故障时不能自毁
这就是 GRL 的设计背景。
六、GRL 的三层反馈架构
GRL 的核心是一个三层反馈循环:
text
Rate-limit clients
↓
Zone-level aggregators
↓
Regional / global controllers
可以理解成:
text
数据面负责本地执行
聚合层负责区域汇总
控制层负责计算全局限流策略
1. Rate-limit clients:服务网格数据面中的本地执行者
Rate-limit client 运行在 service mesh data plane 中。
它直接面对每个请求,负责在热路径上做本地判断。
它会:
- 根据请求匹配限流 bucket
- 根据控制面下发的 drop ratio 决定是否丢弃
- 本地快速放行或拒绝请求
- 上报本机每秒请求计数给 zone aggregator
关键点是:
每个请求的决策都是本地完成的,不需要同步访问控制面。
这保证了热路径低延迟。
2. Aggregators:每个 zone 的流量聚合器
Aggregator 按 zone 部署。
它负责收集本 zone 内所有 clients 上报的请求计数。
然后计算 zone-level usage,并把聚合结果继续发送给 controllers。
为什么要有 aggregator?
因为如果所有 clients 直接向 global controller 上报,控制面会被海量连接和指标打爆。
Aggregator 相当于中间汇总层,减少控制面的压力。
3. Controllers:region/global 控制器
Controller 位于 regional 或 global 层。
它负责:
- 聚合各个 zone 的使用量
- 与配置的限额比较
- 计算当前 drop ratio
- 把新的 drop directive 下发给 aggregators 和 clients
也就是说,复杂计算不在请求热路径上,而是在控制面异步完成。
七、为什么这种架构能同时兼顾低延迟和全局协调?
GRL 的核心取舍是:
text
请求热路径只做本地轻量判断
全局视角通过异步聚合形成
这样带来两个效果。
第一,低延迟。
每个请求不需要访问 Redis,不需要访问控制面,只需要本地随机采样和配置匹配。
第二,全局协调。
虽然请求本地决策,但 drop ratio 来自全局聚合结果,因此系统仍然可以按 global/regional/zonal scope 控制整体流量。
控制面更新可能有几秒延迟。
但 Uber 认为这比每个请求访问中心计数器更可接受。
另外,如果控制面不可用,clients 会 fail open。
这点非常重要。
fail open 的意思是:
text
控制面挂了
↓
数据面继续放行请求
↓
避免限流系统自己导致大面积故障
限流系统是保护系统的,不能因为控制面故障就把所有流量都拒绝掉。
这就是基础设施系统里常见的设计原则:
控制面可以短暂不可用,但数据面必须尽量继续服务。
八、最初的执行算法:Token Bucket
GRL 项目一开始实现的是 token bucket。
Token bucket 是经典限流算法。
它有一个桶,桶里有 tokens。
系统按一定速率往桶里补 token。
每来一个请求,消耗一个 token。
如果桶里有 token,请求通过。
如果没有 token,请求被限流。
在 GRL 早期方案里,每个 proxy 本地维护 token 状态。
token rate 根据 proxy 本地负载与全局 limit 的关系计算。
大致逻辑是:
text
token refill rate = ratio × limitRPS
如果请求到来时 token 可用,就放行并消耗一个 token。
否则标记为 rate-limited。
为了处理短暂 burst,client 会把未使用 token 存在 circular buffer 中,让它们在短时间突发流量里使用。
默认情况下,token 可以保留最多 10 秒。
对于更突发的服务,可以配置到 20 秒。
这个方案在早期测试中能工作,但到了生产环境,它暴露出几个问题。
九、Token Bucket 的生产问题
1. caller 数量超过限制时,不够公平
如果一个 caller 有很多实例,或者某个限流 bucket 下有很多请求来源,token bucket 在每个 proxy 本地独立执行,就很难做到全局公平分配。
某些实例可能 token 充足,某些实例却已经开始 drop。
系统整体看可能还没超过全局限额,但局部 proxy 已经发生过早拒绝。
2. 实例级 burst 会导致提前丢弃
假设某个 caller 的整体流量没超过全局 limit,但它某些实例短时间流量很高。
本地 token bucket 可能认为这个实例超了,于是开始丢请求。
但从全局看,它其实还没超。
这会导致合法流量被误杀。
3. 大规模网关类服务场景不友好
一些 gateway-style service 有成百上千个 caller instances。
每个实例本地 token accounting 无法准确反映全局分布。
在这种情况下,基于全局聚合结果的统一 drop ratio 更适合。
所以 Uber 开始引入 drop-by-ratio。
十、Drop-by-ratio:根据超额比例概率丢弃
drop-by-ratio 的思想很简单:
如果全局流量超过 limit,就按超出的比例概率丢弃请求。
公式是:
text
drop_ratio = (actual_rps - limit_rps) / actual_rps
举个例子。
某个 caller 的 limit 是 1000 RPS。
实际全局流量是 1500 RPS。
那么:
text
drop_ratio = (1500 - 1000) / 1500
= 500 / 1500
= 0.333...
也就是说,所有该 caller 的实例大约丢弃 33% 请求。
这样一来,系统不再依赖每个 proxy 本地 token 状态,而是用全局超额比例统一控制所有实例。
这个方案有几个优点。
第一,超额流量被均匀地分布式丢弃。
不是某个实例先被打爆,而是所有实例根据同一个 drop ratio 共同执行。
第二,更适合大型 gateway-style 服务。
当一个 caller 有大量实例时,drop ratio 比本地 token bucket 更能反映全局状态。
第三,热路径更简单。
每个请求只需要根据配置做一次概率判断。
十一、从 Token Bucket 迁移到统一概率模型
随着 GRL 成熟,Uber 最终完全废弃 token bucket,转向 control-plane-driven probabilistic dropping。
原因是,同时维护两套机制成本太高。
Token bucket 需要维护本地 token、补充速率、burst buffer。
drop-by-ratio 又需要控制面聚合和下发 drop 指令。
两套机制并存会带来:
text
配置复杂
行为难解释
控制面带宽增加
客户端状态复杂
不同场景下决策不一致
最终,Uber 选择统一为概率丢弃模型。
这意味着:
text
所有限流决策都基于控制面聚合出来的全局负载
客户端只执行概率丢弃
它的代价是:
限流决策依赖全局聚合数据,而这些数据每秒更新一次,所以可能滞后 2 到 3 秒。
但 Uber 认为这个延迟在绝大多数 workload 下可以接受。
只有极短、极高的瞬时 burst 可能不够精确。
相比之下,避免本地 token bucket 误杀合法流量,以及简化系统模型,更重要。
十二、最终设计:控制面指导的概率丢弃
GRL 当前模型可以这样理解:
text
控制面负责算 drop ratio
数据面负责按 drop ratio 丢请求
当请求到达 client proxy 时,会执行以下步骤:
text
1. 根据 caller、procedure 或两者组合匹配限流 bucket
2. 如果该 bucket 当前有 drop ratio 指令,就按该比例概率丢弃
3. 如果没有 drop 指令,就正常转发
所有复杂计算都在请求热路径之外完成。
Aggregators 和 controllers 负责:
text
收集请求计数
聚合全局流量
和配置 limit 比较
计算 drop ratio
每秒下发新指令
Client 热路径只做:
text
匹配 bucket
读取当前 drop ratio
随机采样
放行或拒绝
这非常轻量。
它不需要本地 token accounting。
不需要 per-request 访问控制面。
不需要 per-request 访问 Redis。
也没有中心瓶颈。
这就是它能支撑 Uber 数亿请求每秒的关键。
十三、限流规则如何配置?
GRL 的限流规则由 service owners 通过配置文件定义。
每个 rate-limit bucket 可以指定几个维度。
1. Scope:生效范围
可以是:
text
global
regional
zonal
global 表示全局限流。
regional 表示区域级限流。
zonal 表示可用区级限流。
不同 scope 对应不同 blast radius。
比如某个配置如果作用在 global 层,影响范围很大。
如果作用在 regional 或 zonal 层,影响会更局部。
后来 Uber 也进一步引入 regional rate limits,目的就是降低配置变更的影响范围。
2. Matching rule:匹配规则
可以按:
text
caller names
procedures
caller + procedure
也就是说,可以限制:
text
某个调用方访问某服务的总量
某个 procedure 的总量
某个调用方访问某个 procedure 的总量
这种组合很有用。
比如某个 caller 对大多数接口都正常,但只对某个昂贵 procedure 访问过高,就可以只限制这个组合,而不是把 caller 全部打掉。
3. Behavior:执行或影子模式
配置可以是:
text
deny:真正执行限流
allow / shadow mode:只观察,不实际拒绝
shadow mode 非常关键。
限流配置一旦错误,可能直接影响线上流量。
因此在真正 enforce 前,应该先观察:
text
如果这条规则生效,会丢多少请求?
会不会误伤正常流量?
哪些 caller 会被影响?
哪些 procedure 会被影响?
shadow mode 让团队可以先看假想 drop 行为,再决定是否正式启用。
十四、GRL 上线后的第一类收益:降低延迟和开销
GRL 一个非常直接的收益,是移除了 Redis-backed limiter 的网络往返。
以前很多服务做限流时,每个请求要访问 Redis。
迁移到 GRL 后,限流决策在 service mesh data plane 本地完成。
这带来明显延迟改善。
原文给了一个关键服务迁移后的请求延迟分布变化:
text
2--3 ms:从 2.5K RPS 增加到 30K RPS,提升 1100%
3--4 ms:从 50K RPS 增加到 170K RPS,提升 240%
4--5 ms:保持约 120K RPS
5--6 ms:从 80K RPS 降到 34K RPS,下降 57.5%
>6 ms:从 100K RPS 降到 20K RPS,下降 80%
这说明迁移后,大量请求从更高延迟区间移动到了更低延迟区间。
在关键 API endpoint 上,改善也很明显:
text
P50 延迟下降约 1ms
P90 延迟下降几十毫秒
P99.5 从几百毫秒下降到几十毫秒
最慢响应最多改善约 90%
这里的核心原因不是概率丢弃本身,而是:
把 per-request Redis 网络访问从热路径里拿掉了。
对于低延迟 RPC 系统来说,一次额外网络 hop 的成本非常明显。
十五、GRL 上线后的第二类收益:简化运维和节省资源
统一 GRL 之后,服务不再需要自己维护 Redis-based limiter。
这带来几个好处:
text
不需要为 quota 维护独立 Redis 集群
不需要每个团队维护不同 limiter
不需要处理 Redis 客户端库升级风险
不需要排查各种自定义限流行为
不需要在业务代码里嵌入复杂限流逻辑
原文提到,迁移带来了明显的计算效率收益,释放了大量原本用于限流工作负载的 Redis instances。
这类收益很容易被低估。
很多基础设施优化不只是请求延迟下降,也包括:
- 少维护一批集群
- 少处理一类故障
- 少一层依赖
- 少一套监控
- 少一批运维 runbook
- 降低 incident 期间排障复杂度
统一平台能力的价值,往往就在这里。
十六、GRL 上线后的第三类收益:可靠性和事故响应
GRL 部署后,多次在 spike、failover、retry storm 中保护服务。
原文给了一个例子:
一个关键服务经历了 15 倍流量突增:
text
22K RPS → 367K RPS
在 GRL 保护下,服务没有发生性能退化。
这说明 GRL 不只是平时省延迟,而是在极端情况下能保护系统不被打爆。
原文还提到,GRL 能在 DDoS 攻击到达内部系统之前吸收掉流量。
这点非常关键。
在大型系统里,最怕异常流量穿透到内部核心服务。
如果能在 service mesh 层就进行概率 shedding,就可以把异常流量挡在更靠前的位置。
同时,GRL 也成为 Uber Production Engineering 团队的事故处理工具。
在 incident mitigation 期间,工程团队可以快速对高流量 caller 或 procedure 应用 targeted rate limit。
因为控制面更新每秒传播,GRL 可以在几秒内响应过载情况,不需要服务重新部署。
这对事故处理非常重要。
事故中最宝贵的是时间。
几秒内压制特定流量,和等一次发布完成,是完全不同的恢复能力。
十七、GRL 的规模
原文提到,GRL 在 full scale 下已经处理:
text
约 8000 万 requests per second
覆盖超过 1100 个服务
它动态执行 quota,同时保持端到端低延迟。
这个数字非常重要,因为它说明 GRL 不是实验性系统,而是 Uber 可靠性平台的核心组件。
不过,统一执行只是第一步。
当 GRL 覆盖越来越多服务后,新的问题出现了:
限流配置仍然需要人维护。
这就引出 RLC。
十八、问题二:静态配置会过期
GRL 统一了限流执行,但早期限流配置仍然依赖人工 YAML。
Service owners 需要定义 per-caller、per-procedure quota。
当流量变化时,还要手动调整。
在 Uber 这种规模下,这很快成为问题。
1. 太严格
如果配置太低,健康流量峰值也会被限流。
这会误伤正常业务。
比如某个服务增长了,但 limit 没跟着增长。
结果业务正常高峰时被 GRL 丢请求。
2. 太宽松
如果配置太高,限流就失去保护作用。
服务真正过载时,limit 远高于实际承载能力,GRL 不会及时触发。
这种配置看起来"安全",其实只是摆设。
3. 太依赖人工
工程师要看 dashboard,分析流量,修改 YAML,走 review,发布配置。
但服务流量不断变化:
- 新功能上线
- 城市扩张
- 节假日
- 促销活动
- 调用关系改变
- 服务拆分或合并
- failover 后流量转移
人工配置很难长期保持准确。
所以 Uber 需要把"限流配置管理"本身也自动化。
十九、RLC:Rate Limit Configurator
RLC 的目标是:
自动让 GRL 的限流配置保持准确、及时和安全。
它会周期性分析真实流量指标,并根据观察到的需求重写 rate-limit settings。
RLC 的基本循环是:
text
1. 从 Uber observability platform 收集过去几周流量指标
2. 根据历史峰值和 buffer headroom 计算安全 limit
3. 将更新后的配置写入共享配置仓库
4. 通过现有控制面把新 limit 推送给 GRL
这形成一个闭环:
text
真实流量
↓
观测指标
↓
自动计算限额
↓
更新配置
↓
GRL 执行
↓
继续观测
这样,限流规则会随着流量演进自动变化,而不是停留在某个手工设定的旧值。
二十、RLC 的策略不是单一公式
RLC 从一开始就被设计成可扩展的。
默认策略基于历史 RPS 数据。
也就是说,根据过去几周的历史峰值,再加上安全 buffer,计算一个合理 limit。
这种策略适合大量普通服务。
但不是所有服务都适合只看历史。
原文提到,某些服务,比如地图和位置数据相关服务,会使用预测模型。
这些模型会结合:
text
traffic forecasts
pre-planned capacity
也就是流量预测和预先规划的容量。
这种策略不是简单对历史峰值加 buffer,而是提前预测未来负载。
比如某个地区即将有活动,或者某个业务将上线新功能,历史流量不能完全代表未来。
预测模型就更合适。
还有一些情况需要固定 quota。
比如某些配额来自合同、组织约定或长期运营安排。
这些 quota 不频繁变化,需要稳定可预期。
所以 RLC 支持多种 policy:
text
历史流量型
预测模型型
固定配额型
未来还可以扩展更多类型
这比一个统一公式更现实。
不同服务域的流量模型不同,限流策略也应该不同。
二十一、Shadow Mode 和验证
自动生成限流配置非常强大,但也很危险。
如果 RLC 算错了 limit,直接 enforce,可能造成大面积误伤。
所以 shadow mode 非常重要。
在 shadow mode 下,限流规则会被生成和监控,但不会真正丢请求。
系统会展示:
text
观察到的真实流量
如果规则生效,会丢多少请求
哪些 caller 会被影响
哪些 procedure 会被影响
是否会在正常峰值误伤
团队可以先通过 dashboards 和 alerts 验证行为,再正式启用。
这是一种非常重要的上线方式。
在大型基础设施里,自动化不等于直接放飞。
正确做法是:
text
先自动生成
再影子验证
再逐步 enforce
这样既减少人工,又保证安全。
二十二、RLC 带来的影响
RLC 将 GRL 从静态保护层,变成了自适应、自维护的控制系统。
原文总结了几个收益。
1. 操作简单
数千条 rate-limit rules 可以自动更新,不再依赖人工频繁修改。
这减少了大量运维工作。
2. 一致性
所有服务使用相同公式和数据源生成配置。
这避免了不同团队根据经验手工调参导致的不一致。
3. 灵活性
不同策略类型可以适配不同 workload。
普通服务用历史流量模型。
预测性服务用 forecast。
合同或长期约束用固定 quota。
4. 安全性
shadow mode 让配置在执行前先被验证。
这减少了误伤正常流量的风险。
二十三、GRL 和 RLC 的关系
可以把 GRL 和 RLC 看成一套系统的两个层面。
GRL 负责执行:
text
每个请求来了
↓
是否匹配限流 bucket
↓
当前 drop ratio 是多少
↓
按概率放行或丢弃
RLC 负责配置生成:
text
过去几周流量是什么样
↓
应该给 caller/procedure 设置多大 limit
↓
需要多少 buffer
↓
是否用预测模型或固定 quota
↓
写入配置并推送
GRL 是执行系统。
RLC 是配置智能系统。
没有 GRL,RLC 只是生成配置。
没有 RLC,GRL 仍然依赖人工配置。
两者结合,才形成了 Uber 当前的自动化限流体系。
二十四、为什么概率丢弃比精确计数更适合 Uber?
很多人第一次看到概率丢弃,会觉得它不如精确计数。
直觉上:
text
精确计数:更准确
概率丢弃:有随机性
但在 Uber 这种规模下,精确计数反而不现实。
要精确,就要维护全局实时计数。
这意味着巨大的网络通信、中心依赖、跨 region 一致性和控制面压力。
概率丢弃的优势是:
text
每个请求热路径极轻
无中心计数器
没有 per-request 远程调用
天然分布式
能根据全局聚合结果协调
误差在大流量下会被平滑掉
当流量规模足够大时,概率模型非常有效。
比如 drop ratio 是 33%。
每个 proxy 独立随机丢弃约 33% 请求。
在海量请求下,总体丢弃比例会非常接近目标。
这种方式牺牲的是短时间内的绝对精确性,换来的是全局可扩展性和低延迟。
这正是大规模系统常见取舍:
不追求每个请求级别的精确控制,而追求整体统计意义上的稳定控制。
二十五、为什么控制面允许 2 到 3 秒滞后?
GRL 的 drop ratio 来自全局聚合数据,每秒更新。
因此从真实流量变化到客户端执行新 drop ratio,可能有 2 到 3 秒滞后。
这听起来不完美。
但 Uber 认为可以接受。
原因是:
- 大多数流量变化不是毫秒级完成的
- 多数服务不需要 sub-second 精确限流
- 避免 per-request 中心计数更重要
- 本地 token bucket 反而可能误杀合法流量
- 几秒滞后只在极短、极高 burst 下影响较大
- 事故响应中几秒级传播已经足够快
这体现了工程系统里一个重要原则:
不是所有控制都需要强实时。只要反馈延迟相对于系统动态足够小,就可以接受。
对 Uber 的 RPC 限流来说,几秒级全局协调已经能防止绝大多数过载。
二十六、为什么 fail open 很重要?
GRL 的设计里,如果控制面不可用,clients 会 fail open。
也就是说,控制面挂了,数据面继续放行流量。
有人可能会问:
限流控制面挂了,不应该 fail closed,保护下游吗?
在这种基础设施系统里,fail closed 非常危险。
如果控制面出现故障,而所有服务网格代理都开始拒绝请求,那限流系统本身就会造成大面积故障。
GRL 的设计原则是:
text
控制面帮助系统更安全
但控制面故障不能让系统立刻不可用
所以 fail open 是合理选择。
当然,fail open 意味着在控制面故障期间,限流精度会下降。
但这比自我制造 outage 更可接受。
这和很多基础设施系统类似:
- 配置服务挂了,使用本地缓存
- 服务发现挂了,继续使用旧 endpoint
- 认证策略更新失败,保留旧策略
- 限流控制面挂了,继续按已有或放行策略执行
核心思想是:
数据面要尽量独立存活。
二十七、GRL 适合解决什么问题?
GRL 主要解决的是服务间 RPC 流量限速和过载保护。
它适合:
text
限制某个 caller 对某个 service 的流量
限制某个 caller 对某个 procedure 的流量
防止异常 caller 压垮下游
防止 retry storm 放大故障
在事故中快速压制特定流量
保护服务免受突发高流量冲击
通过 shadow mode 验证限流规则
但它不是所有过载问题的唯一解。
原文最后也提到,Uber 还有 throttler layer,用于提供更靠近应用层的额外 overload protection。
这说明 Uber 的可靠性体系是多层的:
text
服务网格层 GRL:统一服务间 RPC 限流
配置自动化 RLC:自动保持 limit 新鲜
应用层 throttler:更靠近业务语义的保护
其他系统:数据库层、存储层、队列层等各自保护
这点很重要。
在大规模系统中,没有一个限流器能解决所有问题。
不同层有不同上下文。
Service mesh 层适合做通用 caller/procedure 限流。
应用层适合做业务语义限流。
存储层适合根据真实资源状态做 admission control。
边缘网关适合做外部流量和安全防护。
二十八、和数据库负载管理的区别
Uber 另一篇文章讲的是从静态限流到智能数据库负载管理。
那篇文章主要关注 Docstore / Schemaless 存储层,强调 overload management 要靠近状态所在的 storage node。
而这篇 GRL 文章关注的是 service mesh 中的 RPC 入口控制。
两者相似,但关注点不同。
GRL 主要回答:
text
服务 A 调服务 B,A 的流量是否超过 B 配置的限额?
某个 caller/procedure 是否应该被概率丢弃?
如何在全局范围内统一执行限流?
数据库 Load Manager 主要回答:
text
这个 storage node 是否真的过载?
这个请求优先级高不高?
是不是某个 tenant 或 partition key 导致过载?
是否应该基于内存、goroutine、commit lag 等信号 shedding?
所以可以这样理解:
text
GRL 是服务网格层的全局 RPC 限流
数据库 Load Manager 是存储层的智能负载管理
两者都是 Uber 可靠性体系的一部分,但处理的问题层次不同。
二十九、普通后端系统可以怎么借鉴?
即使你的系统没有 Uber 这么大,也能从这篇文章里拿走很多经验。
1. 不要让限流散落在业务代码里
业务代码里的限流最容易碎片化。
如果多个服务都需要限流,最好抽象到统一 middleware、gateway、service mesh 或平台组件里。
2. Redis 限流不是银弹
Redis 计数器简单,但它不是免费午餐。
它会增加网络延迟,引入中心依赖,并在大规模下遇到一致性和扩展性问题。
对中小系统,Redis 可能足够好。
对超大规模系统,要考虑本地执行和异步聚合。
3. 热路径一定要轻
限流逻辑在每个请求上执行。
如果每次限流都需要远程调用、复杂计算、锁竞争或大对象分配,就会拖慢所有请求。
Uber 最终选择概率丢弃,就是为了让热路径极轻。
4. 限流规则要支持 shadow mode
限流配置错误会直接影响线上业务。
上线前先 shadow,观察 hypothetical drops,是非常必要的。
5. 配置会过期,要考虑自动化
手工 YAML 在早期可用,但会随着系统规模变成负担。
如果流量模式变化频繁,限流配置应该根据真实指标自动调整。
6. 限流要有 scope
global、regional、zonal 的区别很重要。
全局配置影响最大,风险也最大。
区域级和可用区级限流可以降低 blast radius。
7. 事故响应需要快速、定向限流
限流系统不只是平时自动保护,也应该是 incident playbook 的一部分。
事故中能几秒内压制某个 caller/procedure,比改代码发布强得多。
8. 可观测性必须统一
要能回答:
text
谁被限流了?
为什么被限流?
drop ratio 是多少?
实际丢了多少请求?
是否处于 shadow mode?
限流规则来自哪里?
配置何时更新?
控制面是否健康?
没有这些信息,限流系统很难被业务团队信任。
三十、从系统设计角度看:GRL 是一个分布式控制系统
GRL 不只是一个限流库。
它更像一个分布式控制系统。
控制目标是:
text
让每个 caller/procedure 的流量保持在配置 limit 附近
避免过载穿透到下游服务
保持请求热路径低延迟
系统结构是:
text
数据面 client 本地执行
zone aggregator 聚合局部指标
regional/global controller 计算控制信号
drop ratio 作为控制输出下发
反馈周期是秒级。
控制动作是概率丢弃。
这和很多控制系统类似:
text
观测当前状态
和目标状态比较
计算调整量
下发执行指令
继续观测结果
它的特别之处在于:
- 控制对象是服务间 RPC 流量
- 执行动作是概率 drop
- 热路径不能依赖中心状态
- 系统必须跨 region、zone 和海量 hosts 扩展
- 控制面故障时数据面必须 fail open
从这个角度看,GRL 的设计重点不是某个算法,而是整套控制循环如何在超大规模下稳定运行。
三十一、我的理解:Uber 从"限流功能"做成了"可靠性平台能力"
这篇文章最值得学习的地方,不是 token bucket,也不是 drop ratio 公式,而是 Uber 对限流的定位变化。
早期限流是:
text
某个服务自己写一点逻辑
或者用 Redis 记个 counter
超过就拒绝
后来变成:
text
服务网格统一执行
控制面全局聚合
概率丢弃保护下游
配置不需要改代码
事故中可以几秒生效
再后来变成:
text
RLC 自动分析历史流量
自动生成安全 limit
shadow mode 验证
不同策略支持不同服务域
限流配置自维护
这就是从功能到平台的演进。
功能解决的是一个点。
平台解决的是一类问题。
Uber 不是给某个服务加了一个限流器,而是让整个微服务体系具备统一、自动、可观测、可演进的流量保护能力。
这对后端平台团队非常有启发:
当某个问题在多个团队、多个服务、多个语言栈里重复出现时,不要让每个人各写一套。应该把它抽到基础设施层,统一执行,统一配置,统一观测,再逐步自动化。
三十二、总结
Uber 的限流系统经历了一个典型的大规模基础设施演进过程。
最初,Uber 内部服务各自实现限流。有的写在业务逻辑里,有的用 middleware,有的用 Redis-based counter。随着微服务规模增长,这种模式暴露出配置不一致、运维成本高、修改慢、排障难、覆盖不均匀、可观测性差等问题。
Redis 中心计数器方案在普通系统里可行,但无法支撑 Uber 的规模。Uber 有数十万 hosts、每秒数亿 RPC、跨多个 region。每个请求都访问 Redis 会带来不可接受的延迟、巨大的 Redis 集群成本和跨区一致性问题。定期同步计数也会因为数据过期而无法快速应对突发流量。
因此,Uber 选择将限流能力集成到 service mesh 中,构建 GRL,也就是 Global Rate Limiter。
GRL 使用三层架构:
text
Rate-limit clients:运行在 service mesh data plane,本地执行请求级决策
Aggregators:按 zone 聚合请求计数
Controllers:按 region/global 汇总流量,计算 drop ratio,并下发给 clients
早期 GRL 使用 token bucket,但在生产中遇到公平性和实例级突发问题。后来 Uber 引入 drop-by-ratio,当某个 caller 的全局流量超过 limit 时,根据公式计算概率丢弃比例:
text
drop_ratio = (actual_rps - limit_rps) / actual_rps
例如实际流量是 limit 的 1.5 倍,就丢弃约 33% 请求。
最终 Uber 废弃 token bucket,统一采用控制面指导的概率丢弃模型。请求热路径只做本地 bucket 匹配和随机采样,不访问 Redis,也不访问控制面。控制面每秒聚合请求计数、计算 drop ratio,并下发到 clients。虽然全局数据有 2 到 3 秒延迟,但这种取舍相比中心化计数更适合 Uber 的规模。
GRL 上线后效果明显:
text
移除 Redis-backed limiter 后,大量请求从高延迟区间移动到低延迟区间
P50 延迟下降约 1ms
P90 延迟下降几十毫秒
P99.5 从数百毫秒下降到数十毫秒,最慢响应最多改善约 90%
释放了大量用于限流的 Redis instances
关键服务成功扛住 15 倍流量突增,从 22K RPS 到 367K RPS 无性能退化
DDoS 流量能在进入内部系统前被吸收
GRL 在 full scale 下处理约 8000 万 RPS,覆盖超过 1100 个服务
但 GRL 统一执行后,配置管理又成为新问题。手工 YAML 限流规则会过期:太严格会误杀健康流量,太宽松又失去保护作用,频繁人工调整也不可持续。
于是 Uber 构建 RLC,也就是 Rate Limit Configurator。
RLC 会周期性从观测平台收集过去几周的流量数据,根据历史峰值和 buffer headroom 自动计算安全 limit,写入共享配置仓库,并通过控制面推送给 GRL。它还支持多种策略:默认历史 RPS 策略、基于预测模型的策略,以及固定 quota 策略。为了保证安全,RLC 支持 shadow mode,让团队先观察假想 drop 行为,再决定是否正式 enforce。
最终,GRL + RLC 将 Uber 的限流系统从零散、手工、依赖 Redis 的实现,演进成统一、自动、可扩展、低延迟的可靠性平台能力。
这篇文章给后端工程师的最大启发是:
限流不是简单的"超过 QPS 就返回 429"。
在大规模微服务系统中,限流是一个分布式控制系统,必须同时考虑热路径延迟、全局协调、配置自动化、可观测性、故障模式、事故响应和长期运维成本。
真正成熟的限流系统,不只是能丢请求。
它应该能在系统压力下,快速、准确、低成本地保护关键服务,同时让工程团队不再被手工配置和碎片化实现拖住。