限流的三种实现:计数器、漏桶和令牌桶怎么选

米阳 2023-7-11 4 7/11

限流不是为了拒绝用户,而是为了让系统在超载时仍然能服务一部分请求。三种常见算法适用场景差别很大,选错了要么挡不住峰值,要么误伤正常流量。

一、固定窗口计数器

最简单的做法:每分钟一个 key,INCR 后判断值是否超过阈值,key 设 60 秒过期。

INCR rate:user:1024
EXPIRE rate:user:1024 60

优点是实现只要几行,缺点是窗口边界会放过双倍流量:用户在 59 秒发 100 次、00 秒再发 100 次,实际一秒内过了 200 次。滑动窗口可以缓解,但要存更多数据。

适合:登录失败次数、短信验证码这类对精度要求不高的粗粒度限制。

二、漏桶:恒定速率放行

请求先入桶,桶以固定速率漏水(放行),桶满则拒绝。无论上游来得多猛,下游收到的都是平滑流量。

优点是保护下游非常稳,缺点是不允许任何突发。哪怕系统此刻很闲,攒着的请求也得排队慢慢出,用户体感延迟会变高。

适合:调用第三方有明确 QPS 限制的接口、消息推送这类必须匀速的场景。

三、令牌桶:允许一定突发

以固定速率往桶里放令牌,请求来时取一个令牌,取不到才拒绝。桶里可以攒令牌,所以空闲一段时间后能承接一波突发。

rate      = 100 令牌/秒   # 长期平均速率
burst     = 200           # 桶容量,决定能承受多大突发

这是网关层最常用的算法,兼顾了保护和体验。Guava 的 RateLimiter、Nginx 的 limit_req(配合 burst)本质都是这个思路。

四、分布式环境别忘了一致性

单机内存计数在多实例部署下会失效:3 台机器各限 100 QPS,实际是 300。要用 Redis 集中计数,并且用 Lua 脚本保证「判断 + 自增」的原子性,否则并发下会超发。

local c = redis.call('INCR', KEYS[1])
if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return c

五、被限流之后给什么响应

返回 429,带上 Retry-After 头告诉客户端多久后重试;响应体里给一句人能看懂的提示。内部服务之间调用,则配合退避重试(指数退避 + 随机抖动),避免所有实例同时重试造成二次冲击。

小结

粗粒度用固定窗口,需要匀速用漏桶,网关和面向用户的接口用令牌桶。先明确要保护的是下游还是自己,再选算法。

- THE END -

米阳

7月11日10:25

最后修改:2023年7月11日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论