Python限流器引发的宕机:我们是如何被自己的限流策略反噬的(2026-09-06)
当“保护系统”变成“系统杀手”,这可能是今年最讽刺的技术事故。
一次诡异的“自我攻击”
上个月凌晨2:47,我们的支付网关监控突然炸了——错误率从0.2%飙升到73%,但流量却比平时低了40%。更诡异的是,数据库和下游服务都健康得可怕。
直到我打开应用日志,才发现罪魁祸首竟是我们在三天前上线的Python滑动窗口限流器。
限流器如何变成“自我DoS攻击”
案例:一段“优雅”的代码引发的雪崩
我们的限流器核心逻辑很简单(伪代码):
def check_rate_limit(user_id):
key = f"rl:{user_id}:{int(time.time()/60)}"
count = redis.incr(key)
if count > 100:
raise RateLimitExceeded
return True
问题出在哪? 在低流量时段,用户请求稀疏,滑动窗口内的旧key迟迟不清理。当我们清理过期key时,由于使用了 KEYS rl:* 命令(而非 SCAN),导致Redis阻塞了6秒。
而这6秒内积累的请求,全部触发限流——限流请求本身又占用了线程池,进而阻塞了健康请求的检测逻辑,形成死循环。
真实数据对比
| 指标 | 限流前 | 限流器失效时 |
|---|---|---|
| P99延迟 | 120ms | 8.4s |
| 线程池活跃度 | 35% | 100% |
| Redis CPU | 12% | 91% |
| 熔断器触发次数 | 0 | 247次/分钟 |
最致命的是: 我们为“保护系统”设计的限流器,自己却没有设置最大并发限制。当它进入“自我怀疑”状态时,每个请求都在等待Redis超时——而限流器本身的线程池被打满后,连“放行”请求的线程都拿不到。
三个必须知道的“反脆弱”设计原则
1. 限流器必须“零依赖”于被保护资源
如果限流器使用Redis,那么Redis故障时,默认应该放行(fail-open),而不是拒绝所有请求。我们当时错误地设置了 fail-closed——即Redis崩了,所有请求都认为是“超限”。
2. 给限流器加“断路器”断路器
你的熔断器需要保护熔断器自身。我们增加了信号量隔离:限流器最多占用50个线程,超过则直接放行,不再等待Redis响应。
3. 用“令牌桶”替代“固定窗口”
固定窗口在临界点会双倍突发(比如59秒和00秒各100个请求)。我们改成了令牌桶 + 本地预计算,每台机器独立判断,再通过Redis异步同步,避免每次请求都打Redis。
行动号召:今天审查你的限流器
打开你的代码,问自己三个问题:
- 如果Redis/数据库延迟3秒,我的限流器会怎样?
- 我的限流器线程池是否与业务线程池互相等待?
- 我有没有使用
SCAN而不是KEYS?
不要等到凌晨3点被叫醒才修复——我们的教训是:限流器不是服务的“盾牌”,而是另一个需要被保护的“士兵”。
免责声明:本文案例基于真实事故的艺术化改编,具体数据已做脱敏处理。技术方案仅供参考,实施前请结合你的系统架构进行压力测试。作者不对任何直接或间接损失负责。