踩坑二:限流策略不当——从漏桶到令牌桶,真实压测下的雪崩教训
限流这东西,听起来简单——不就是控制流量吗?
嗯,我在项目初期也是这么想的。直到一次压测,系统直接雪崩,我才真正明白:选错限流算法,后果比不限流更可怕。
场景还原:一次“正常”的压测
那是个金融核心接口,日交易量几千万。业务方要求压测到正常峰值的3倍。我们用了漏桶算法,觉得挺稳妥——请求进来先排队,慢慢放行,很保守,对吧?
结果呢?
压测刚启动30秒,接口响应时间从20ms飙到12s。紧接着,下游数据库连接池爆满,CPU 100%。整个服务挂了。
为什么会这样?
当时我盯着监控屏,脑子一片空白。事后复盘才找到病根:漏桶算法在突发流量下,会形成“请求堆积—队列膨胀—资源耗尽”的恶性循环。
漏桶 vs 令牌桶:核心差异
先说结论:漏桶适合“平滑”,令牌桶适合“允许突发”。金融接口为啥必须用令牌桶?因为用户行为本身就是突发的——秒杀、转账高峰、批量查询,你无法强迫用户“匀速”访问。
| 特性 | 漏桶算法 | 令牌桶算法 |
|---|---|---|
| 流量形状 | 强制平滑,输出速率恒定 | 允许突发,短期可超阈值 |
| 处理机制 | 请求排队,溢出丢弃 | 令牌累积,突发时消耗库存 |
| 适用场景 | 网络流量整形、对抖动敏感的场景 | API限流、微服务保护、秒杀系统 |
| 主要风险 | 队列堆积导致延迟雪崩 | 突发可能突破下游阈值 |
| 我推荐指数 | ★★☆☆☆ | ★★★★★ |
关键教训:漏桶的“平滑”在金融接口里是伪命题。你想想看,用户不会因为限流算法就改变行为。与其强行削峰,不如主动拥抱突发,再用令牌桶的“蓄水池”消化掉。
那个坑到底是怎么踩的?
我们的漏桶配置是这样的:
// 伪代码,我当年就是这么写的
RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000个请求
// 每个请求进来时:
if (rateLimiter.tryAcquire()) {
// 处理请求
} else {
// 返回“限流中”
return HttpStatus.TOO_MANY_REQUESTS;
}
// 没设置排队时间,默认无限阻塞!
// 也没给队列设置最大长度,结果请求全堵在内存里
问题出在 RateLimiter.create(1000) 上。Guava 的 RateLimiter 默认使用 SmoothBursty(令牌桶变体),但我们业务代码把它当漏桶用了:tryAcquire 不设超时,导致请求无限等待。
压测时2000并发进来,1000个被处理,剩下1000个等着。等待的请求占着连接池、占着线程、占着内存。1秒后,新的1000个又来了……内存撑爆,线程池满,OOM。
⚠️ 真实教训:那次事故后,我们查日志发现,限流队列里最多堆积了8万个请求,每个请求对象平均12KB。光队列就用掉了近1GB内存。加上业务对象,整个进程在30秒内内存翻了三倍。
正确的做法:用令牌桶,并设置合理的突发容量
我个人习惯的做法是:先评估业务峰值,再设计限流参数。金融接口一般有明确的SLA,比如“单实例每秒1000请求,允许1秒内突发2000”。
// 正确配置——令牌桶
RateLimiter rateLimiter = RateLimiter.create(
1000, // 稳定速率:每秒1000
2000, // 突发容量:最多2000个令牌累积
TimeUnit.MILLISECONDS // 时间单位
);
// 请求处理
if (rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
// 处理请求——100ms内拿到令牌就处理
} else {
// 快速失败
return HttpStatus.TOO_MANY_REQUESTS;
}
嗯,这里要注意几点:
- 突发容量不能设太大。我见过有人设成10000,结果下游直接被打爆。一般建议是稳定速率的1~3倍。
- 超时时间必须有上限。不能无限等待,业务方宁愿收到429也不想等10秒然后超时。
- 限流必须在最外层。我习惯在API网关或Filter做,避免深入到业务逻辑里。
避坑指南:我曾经踩过的那些坑
这里分享几个我亲身踩过,或者帮别人排查过的典型坑:
- 限流和重试循环——客户端收到429后重试,结果重试又把限流撑爆。解决方案:客户端做指数退避,服务端记录重试次数并丢弃。
- 分布式限流不一致——多实例部署时,每个实例单独限流,导致总量超标。我当时的方案是:用Redis + Lua脚本做全局令牌桶,不过要注意Redis单点故障。
- 限流不区分接口优先级——所有接口共用同一个限流桶,导致核心交易被非核心查询挤占。后来我改成:核心接口独立桶,非核心接口共享桶。
- 压测时忘记关闭限流——这个说出来你可能不信,但真发生过。压测端模拟10000并发,限流只有1000,结果压了2个小时才发现数据不对。嗯,我现在每次压测前都会检查限流配置。
从漏桶到令牌桶的演进之路
说实在的,我早期做系统设计时也偏爱漏桶,觉得它简单、保守、不容易出事。但经历过压测雪崩后,我彻底转投令牌桶了。不是说漏桶没用——在网络层、硬件层,漏桶依然很香;但到了业务层、API层,令牌桶才是正解。
下面这张图,是我自己做限流设计时最喜欢贴出来的结构:
实用建议:如果你现在还在用漏桶做API限流,我建议赶紧换成令牌桶。不一定非要Guava的RateLimiter,Redis+Lua的分布式令牌桶也很成熟。关键是:允许突发,但设置上限;快速失败,不要排队。
最后说一句:压测不是用来验证系统有多强,而是用来找出系统哪里会死。那次雪崩后,我们团队把所有接口的限流策略都改成了令牌桶,并且加了压测自动化——每次上线前都要跑一遍限流压测。从那以后,类似的雪崩再没出现过。