2. KMS限流基本原理:令牌桶 vs 漏桶归纳总结

说到KMS限流,大多数人第一反应就是「令牌桶」。嗯,这确实是最经典的算法。但我在实际排查过几次故障后发现,很多同学对它的理解其实停留在「知道名字」的程度。今日咱们就把两种算法彻底掰扯清楚,你想想看,以后遇到限流失效或抖动,心里就有底了。

2.1 令牌桶算法

先说说令牌桶。我习惯把它想象成一个蓄水池 + 定时投币机:系统以固定速率往桶里扔令牌,每个请求进来必须拿走一个令牌才能通过。如果桶空了,请求就被拒绝或排队。

这里有两个关键参数:rate(令牌生成速率)和capacity(桶容量)。

  • rate:决定了平均能承受的请求频率。比如每秒50个令牌,意味着长期来看每秒最多处理50个请求。
  • capacity:决定了瞬时突发处理能力。比如桶能存200个令牌,那么短时间可以一次性放走200个请求。

我之前遇到过一个线上事故:某个服务平时请求量很稳,结果活动开始时流量暴增3倍,限流却完全没有生效,cpu直接被打满。查到最后发现什么?桶容量设得太大,而且令牌生成速率和qps上限没拉开差距,桶一直有剩余令牌。说白了,突发处理能力被错误放大了。

个人经验:对于正常流量加突发峰值的场景,我习惯把capacity设为rate * 2到rate * 3。既能抗住瞬时的流量尖刺,又不会让桶长期处于满员状态。

2.2 漏桶算法

漏桶跟我们平时用的漏斗完全一样——水(请求)以任意速度往漏斗里倒,但底部出口以固定速率流出。如果进水太快,桶装满后会溢出,溢出的请求直接拒绝。

漏桶的核心思想是强制平滑,它不在乎你什么时候进来,反正出来是均匀的。这和令牌桶的本质区别:令牌桶允许短期波,漏桶完全削平波峰。

举个例子:你有一个API,客户要求调用频率不能超过20次/秒。用漏桶的话,哪怕在0.1秒内打进来50个请求,也只有前2个能在第1秒内出去(因为每秒20个),其余48个要么排队要么丢弃。而令牌桶呢?桶里有容量的话,这50个请求可以秒过,但后面就会饿死。

注意:网上很多资料说漏桶就是「先入先出队列+匀速消费」,这是对的。但要注意,漏桶的桶满丢弃策略通常采用丢弃最早的请求(类似tail drop),这在网络协议里常见,但在业务限流中可能会导致客户端连续超时重试,造成雪崩。我曾经踩过这个坑,后来改成丢弃最新(head drop)才稳住系统。

2.3 核心差异对比表

对比维度令牌桶漏桶
允许突发是,上限取决于容量否,严格平滑
对突发流量的容忍度高低
队列溢出策略通常丢弃新令牌或拒绝请求丢弃最早请求或新请求(需配置)
适用场景API网关、微服务限流、允许一定峰值的业务网络/磁盘IO限速、严格保证出口速率
实现难度中等(需定时补充令牌)简单(计数器+队列)
常用开源实现Guava RateLimiter, Redis+LuaNginx limit_req, Linux tc

这个表我建议你截图保存。每次选型拿不准时,先看场景允许不允许突发。比如KMS接口,有时候客户端会突然批量重试,你想想是不是该用令牌桶?对。

2.4 选型心法

我做过的KMS限流项目里,最终全用了令牌桶。不是漏桶不好,而是业务场景天然需要容忍突发:客户端拿到初始时要批量请求,日常则是平稳流量。漏桶会把初始批量请求卡死在队列里,导致用户等半天。当然,如果你是做日志上传或CDN节点出口限速,那漏桶反而是首选。

这里有个简单的决策流程:

  1. 先问:业务是否允许短期流量尖峰?是→令牌桶;否→漏桶。
  2. 再问:对请求延迟有多敏感?如果允许排队,两者都可;如果担心排队导致超时积累,令牌桶更合适。
  3. 最后问:后端资源是否能承担突发?如果数据库或下游服务扛不住突发,那你必须用漏桶强行削峰。
我个人习惯:KMS类接口我直接用令牌桶 + 小容量(容量约等于1秒的rate)。既能抗住客户端瞬间重试(比如5个请求并发),又不会让桶里攒着几百个令牌,避免一次性压垮后端。

2.5 流程图:令牌桶 vs 漏桶核心逻辑

下面是两种算法的执行对比图。请注意左侧令牌桶分「正常请求」和「突发请求」两条路径,右侧漏桶只有一个出口速率,所有请求必须排队。

令牌桶 vs 漏桶核心逻辑对比 令牌桶 vs 漏桶逻辑对比 令牌桶 令牌生成器 (rate) 令牌桶(容量) 桶中有N个令牌 请求进入 有令牌 突发? 通过✅ 拒绝❌ 漏桶 请求 漏桶(队列) 请求排队中... 匀速流出 溢出丢弃 说明:令牌桶允许突发通过(虚线路径),但会消耗桶内令牌;漏桶强制所有请求排队,流出速率恒定。 实际使用中,令牌桶配合小容量可同时满足「」突发容忍「」和「」后端保护「」。

2.6 避坑指南

最后分享几个我亲眼见过的坑:

  • 桶容量设为0:有的同学误以为容量=0就能禁止所有突发,结果桶一直空着,任何请求都过不了。容量至少为1,否则永远拒接。
  • 令牌补充频率太低:比如rate=100/s,但每10s补充一次,每次补充1000个。这会导致前9.9秒桶是空的,第10秒突然涌出1000个请求,限流形同虚设。应该均匀补充。
  • 漏桶队列无限大:我见过一个同学用无界队列实现漏桶,结果请求堆积到内存溢出。漏桶一定要设上限,而且要考虑丢弃策略。
  • 不区分用户:全局限流容易误伤所有用户。建议对每个调用方(appId/ak)分别维护独立的令牌桶。
我曾经在KMS限流里用漏桶,队列设了1000,客户端连续重试,结果队列里全是旧请求,新请求等到超时都被踢了。后来改成令牌桶 + 小容量(50),且每个ak单独一个桶,问题才解决。所以你想想看,选对算法比调参数重要得多。

好了,这一章的核心就这些。你只要记住:令牌桶适合业务限流,漏桶适合出口限速。一旦选错,调参也救不回来。