限流、熔断与降级

流量超过系统处理能力时,什么都不做只会整体崩掉:慢请求堆满线程池,数据库连接被打满,连原本正常的接口也一起超时。限流、熔断、降级是三道防线——限流控制“进来多少”,熔断防止“被下游拖死”,降级保证“坏的时候还活着”。

限流算法对比

算法原理优点缺点
固定窗口计数每单位时间一个计数器,超阈值拒绝实现最简单,内存小窗口临界处可能通过 2 倍流量
滑动窗口计数按更细粒度分桶滚动统计平滑,精度可调实现稍复杂,需存桶数据
令牌桶按速率生成令牌,请求取令牌允许一定突发,最常用需处理桶容量与预热
漏桶请求先入桶,按固定速率流出流出绝对平稳不允许突发,延迟不可控

固定窗口的临界问题很典型:阈值 100/秒时,第 1 秒最后 10ms 通过 100 个、第 2 秒前 10ms 又通过 100 个,20ms 内实际放行 200 个。

令牌桶的最小实现:

// 以下片段需放进对应类中运行
public class TokenBucket {
    private final double rate;                  // 每秒生成令牌数
    private final double capacity;              // 桶容量,即允许的突发量
    private double tokens;
    private long lastRefillNanos = System.nanoTime();

    public TokenBucket(double rate, double capacity) {
        this.rate = rate;
        this.capacity = capacity;
        this.tokens = capacity;
    }

    public synchronized boolean tryAcquire(int n) {
        long now = System.nanoTime();
        tokens = Math.min(capacity, tokens + (now - lastRefillNanos) / 1e9 * rate);
        lastRefillNanos = now;
        if (tokens >= n) {
            tokens -= n;
            return true;    // 输出:拿到令牌,放行
        }
        return false;       // 输出:令牌不足,拒绝或改为排队
    }
}

单机限流

单机限流最简单:网关层用 Nginx,应用内用本地计数器或令牌桶。

# Nginx 单机限流配置示意,参数含义以官方文档为准
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
server {
    location /api/ {
        limit_req zone=api burst=200 nodelay;  # 允许 200 的突发,nodelay 表示不排队直接处理
    }
}

单机阈值有硬上限:集群里每台机器各自限 100/s,10 台机器整体就是 1000/s。若阈值要按“整个服务”算,就必须用分布式限流。

分布式限流

常见做法是用 Redis + Lua 做滑动窗口计数,脚本在 Redis 内原子执行:

-- Lua 脚本示意(由客户端 eval 调用,KEYS[1]=限流 key)
local now    = tonumber(ARGV[1])   -- 当前毫秒时间戳
local window = tonumber(ARGV[2])   -- 窗口大小(毫秒)
local limit  = tonumber(ARGV[3])   -- 窗口内允许的请求数
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)  -- 清掉过期记录
if redis.call('ZCARD', KEYS[1]) >= limit then
    return 0                       -- 超限,拒绝
end
redis.call('ZADD', KEYS[1], now, ARGV[4])  -- ARGV[4] 为请求唯一 ID
redis.call('PEXPIRE', KEYS[1], window)
return 1                           -- 放行

分布式限流的注意事项:

  • 每次请求都要访问 Redis,Redis 成了热依赖,要做降级(Redis 不可用时退化为单机限流,而不是全部拒绝)。
  • 用 ZSET 存储每次请求代价较大,热点接口可改用“分桶计数”(把时间切成小格,每格一个计数器)。
  • 另一种思路是额度租借:实例从 Redis 一次性领取一批额度(如 100 个),本地扣减,用完后异步续借。既减少 Redis 压力,又只能做到近似限流。

熔断状态机

熔断器监控调用的失败率,触发后直接拒绝请求,给下游恢复时间:

       失败率超阈值
CLOSED ───────────────→ OPEN
  ↑                       │ 等待熔断时长
  │ 探测成功              ↓
  └────────────────── HALF_OPEN(放行少量请求探测)
            探测失败 → 回到 OPEN
参数含义常见起点
统计窗口计算失败率的时间跨度10s(滑动窗口)
最小请求数样本太少不熔断20 次
失败率阈值超过即熔断50%
慢调用阈值慢调用占比过高也熔断P99 > 1s 占 50%
熔断时长OPEN 持续多久10s,可逐次递增
半开放行数探测请求数5 次

要点:熔断是按下游依赖粒度配置的(如“查用户”“查库存”各一个熔断器),不是整个服务一个;触发熔断要打日志和告警,否则故障会被静默掩盖。具体实现可参考 Sentinel、Resilience4j 等组件的官方文档。

降级策略

熔断和限流之后,还要回答“被拒绝的请求怎么办”:

策略做法例子
返回兜底数据返回缓存快照、默认值、空列表推荐位降级为默认商品
异步化只写入队列/消息,稍后处理下单送积分改为异步发放
关闭非核心通过开关下线次要功能关闭评论、足迹、广告
限流保护对非核心接口优先限流大促时优先保下单

降级开关要能在配置中心一键切换,并且定期演练——没有演练过的开关,真出事时往往是失效的。

配合超时与重试

  • 超时必须设:连接超时、读超时都要显式配置,且调用链越上层超时越短,避免请求层层堆积。
  • 重试要有前提:只重试幂等操作,只对网络抖动、限流类错误重试,参数错误重试没有意义。
  • 重试要退避:固定间隔会形成重试风暴;用指数退避 + 随机抖动,并限制最大次数。
  • 控制重试放大:三层调用各重试 3 次,最底层实际承受 27 倍流量;上游重试时下游最好不要再重试。

配置化与压测验证

  1. 阈值必须来自压测:先测出单机 QPS/P99 拐点,再按 70%~80% 定限流阈值。
  2. 阈值放配置中心,支持秒级调整与灰度,不用发版。
  3. 上线前做一次“限流熔断演练”:人为把下游打挂,确认熔断生效、降级生效、告警到达。

小结:限流用令牌桶或滑动窗口控制在途流量,熔断用状态机切断故障依赖,降级用兜底与开关保住核心链路;三者再加上合理的超时与退避重试,才是一套完整的过载保护。阈值不要拍脑袋,靠压测确定并做成动态配置。

笔记加载中…