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。

行动号召:今天审查你的限流器

打开你的代码,问自己三个问题:

  1. 如果Redis/数据库延迟3秒,我的限流器会怎样?
  2. 我的限流器线程池是否与业务线程池互相等待?
  3. 我有没有使用 SCAN 而不是 KEYS ?

不要等到凌晨3点被叫醒才修复——我们的教训是:限流器不是服务的“盾牌”,而是另一个需要被保护的“士兵”。


免责声明:本文案例基于真实事故的艺术化改编,具体数据已做脱敏处理。技术方案仅供参考,实施前请结合你的系统架构进行压力测试。作者不对任何直接或间接损失负责。