摘要:限流处理"流量太大",降级处理"功能太弱"。限流在入口按阈值挡掉超额请求(令牌桶/漏桶/滑动窗口),降级在压力或故障下提供"次一级但可接受"的响应。两者常配合使用,让系统在过载时还能偏向地保住核心体验,而不是一视同仁地全崩。
熔断管的是"下游坏了",限流管的是"流量太冲",降级管的是"压力太大时别硬扛"。三个老兄弟常被一起提,但干的活不一样——本节先讲清限流怎么做,再讲降级怎么选,然后把"限流+降级"拼起来看。
限流的核心是给系统定一个放行上限:每秒最多处理 N 个请求,超过的直接拦截(返回 429 或排队),防止后端被瞬时洪峰打穿。它保护的是"系统资源本身"。
一张表选型:
| 算法 | 心智 | 突发容忍 | 实现成本 |
|---|---|---|---|
| 固定窗口 | 每段限额 | 窗口边界会爆 | 最低 |
| 滑动窗口 | 连续统计 | 好 | 中 |
| 令牌桶 | 攒令牌冲突发 | 好(允许攒) | 中 |
| 漏桶 | 恒定速率削峰 | 无突发 | 中 |
一块常见的令牌桶读写示意(表达意图,示意成分大于完整实现):
// 令牌桶:桶里有令牌就放行,没有就拦截(语义示意) type TokenBucket struct{ tokens int; rate int; mu sync.Mutex } func (b *TokenBucket) Allow() bool { b.mu.Lock(); defer b.mu.Unlock() if b.tokens > 0 { b.tokens--; return true } return false // 超限,返回 429 }
限流放哪个层次,也有讲究:可以在网关层做(面向整个系统/按 IP 限)、在服务内做(按用户/按业务维度限)、还可以对特定下游做。别只看总量——按用户维度的限流,比按总量限流更能防单个大户把别人挤死。
降级回答的是另一个问题:压不住了或依赖坏了,我怎么"省着办"还能给用户个交代? 它不是回到"什么都没了",而是主动提供一种"更省资源但依然可接受"的响应。
几类常见降级:
把两个拼起来看一次秒杀:下单瞬间请求量冲爆。限流在网关先把超额的请求拦下(有人排队、有人干脆 429),把进入后端的量压在它能扛的水平;后端在下单主流程稳住的同时,对"猜你想买""相似推荐"这类非核心功能打开降级开关,直接返回静态列表——于是核心下单流程稳住,次要体验受损但整体可用。这个"伤次要保核心"的取舍,就是限流+降级的合力价值。它和降级的核心思想完全一致:面对超载,关键不是每条请求都成功,而是把最重要的那部分保住。
限流最怕误伤真实用户。如果阈值拍脑袋定得比真实峰还要低,平时就会把好好的用户给 429 掉。所以限流的阈值要建立在历史真实峰值 + 扩容后的余量上,并且要按维度分化(IP、用户、token 各不相同),不能一刀切。"挡错的"比"全崩"更伤口碑——因为全崩能查,挡错了查不清又天天误伤。
降级的错误做法是把返回内容换成"出错啦"或者干脆让前端报错。降级的真意是返回一个"用户能接受、系统负担小"的结果——哪怕是"推荐暂时不可用,稍后再试"这种诚实的话,也比一坨报错好。别为了让监控"看起来没报错"而硬编一个张冠李戴的假数据,那才真是把降级做成了造假。
把第 6 章的几个兄弟放回一张部署图上,位置感比抽象定义更记得住:限流站在最外面(网关/入口),先按量把超额流量挡在外;熔断跟着某个具体下游调用走,那一调和哪段坏了它就在那段下;舱壁管的是每个下游各自占的资源,连接池/线程池都按下游切开。三者就像三道设在不同环节的闸,各管一段,最后一起把"雪崩"这条最大的系统性风险给拆成小风险。
之所以强调位置,是因为现实中很多"容错失效"不是没装,而是装错了地方——把限流塞进了一个具体服务内部当成万能入口,或者把熔断设在网关却拦不住下游真正坏死的那一路。放对位置,每条机制才能各司其职:总量闸在入口、坏路闸在下游、资源闸在连接池。
一句话:限流挡"太多",降级给"次级的可用",它们合力保住最重要的那部分
限流防"流量太大",降级治"压力太大",熔断管"下游坏了",三者分工不同
令牌桶容忍突发,漏桶削峰,滑动窗口修正窗口边界,按场景选
别只看总量,按用户维度的限流更能防大户挤死小户
降级是"给次级的可接受响应",静态降级/关非核心/切只读都是手段
阈值建立在真实峰值上,别误伤正常用户;降级别变成造假响应