雪崩预防:熔断 / 限流 / 降级

一句话速记

限流:控制进入系统的请求速率(令牌桶/漏桶),超限快速拒绝,保护后端;熔断:监控下游错误率,达到阈值则”断路”(停止调用下游,直接返回默认值),等待恢复后再试探;降级:主流程失败时,用低质量但可用的替代方案响应用户(返回缓存、默认值、静态页面)。三者组合是雪崩防御的标准套路:限流挡外部冲击,熔断保护内部依赖,降级保证系统可用

关键细节

1)限流

令牌桶(Token Bucket)— 允许突发流量

# 令牌桶:以固定速率往桶里放令牌,请求来了取令牌
# 有令牌 → 处理;无令牌 → 拒绝(或等待)
 
class TokenBucket:
    def __init__(self, rate: float, capacity: int):
        self.rate = rate          # 令牌生成速率(个/秒)
        self.capacity = capacity  # 桶容量(允许的突发量)
        self.tokens = capacity    # 当前令牌数
        self.last_refill = time.time()
    
    def acquire(self) -> bool:
        now = time.time()
        elapsed = now - self.last_refill
        # 补充令牌
        self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
        self.last_refill = now
        
        if self.tokens >= 1:
            self.tokens -= 1
            return True  # 允许
        return False     # 拒绝
 
# 优点:允许短暂突发(桶满时可以消费积累的令牌)
# 适用:API 限流,允许偶发的流量突增

漏桶(Leaky Bucket)— 平滑输出

请求进入漏桶 → 以固定速率流出(处理)
桶满 → 拒绝新请求

特点:输出速率恒定(无突发),适合保护下游服务(数据库等)
缺点:不允许突发,可能浪费下游的处理能力

Redis 实现限流(滑动窗口)

-- Lua 脚本(原子执行)
-- KEYS[1]: 限流 key(如 "rate:user:1001")
-- ARGV[1]: 窗口大小(秒)
-- ARGV[2]: 限流阈值
-- ARGV[3]: 当前时间戳(毫秒)
 
local key = KEYS[1]
local window = tonumber(ARGV[1]) * 1000  -- 转毫秒
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
 
-- 移除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)
 
if count < limit then
    redis.call('ZADD', key, now, now)  -- 记录本次请求
    redis.call('EXPIRE', key, ARGV[1])
    return 1  -- 允许
else
    return 0  -- 拒绝
end

限流框架

// Sentinel(阿里开源,推荐)
@SentinelResource(value = "getUserInfo",
    blockHandler = "handleBlock",     // 被限流时调用
    fallback = "handleFallback")      // 业务异常时降级
public UserInfo getUserInfo(Long userId) {
    return userService.query(userId);
}
 
public UserInfo handleBlock(Long userId, BlockException ex) {
    return UserInfo.empty();  // 限流返回空对象(快速失败)
}
 
// 配置限流规则
FlowRule rule = new FlowRule("getUserInfo");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1000);  // QPS 上限 1000
FlowRuleManager.loadRules(List.of(rule));

2)熔断(Circuit Breaker)

三态状态机

CLOSED(正常):
  正常调用下游
  统计错误率 / 响应时间
  达到阈值 → 转 OPEN

OPEN(断路):
  停止调用下游(直接返回错误/降级结果)
  等待 N 秒(冷却时间)→ 转 HALF_OPEN

HALF_OPEN(半开):
  放行少量请求试探下游
  成功 → 转 CLOSED(恢复)
  失败 → 转 OPEN(继续断路)

Resilience4j(Java 熔断器)

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .slidingWindowType(SLIDING_WINDOW_TYPE.COUNT_BASED)
    .slidingWindowSize(10)           // 最近 10 次调用
    .failureRateThreshold(50f)       // 失败率 50% → 开路
    .waitDurationInOpenState(Duration.ofSeconds(30))  // 开路等待 30s
    .permittedNumberOfCallsInHalfOpenState(3)         // 半开放 3 次试探
    .build();
 
CircuitBreaker cb = CircuitBreaker.of("paymentService", config);
 
Supplier<PaymentResult> decoratedSupplier = 
    CircuitBreaker.decorateSupplier(cb, () -> paymentService.pay(request));
 
try {
    return decoratedSupplier.get();
} catch (CallNotPermittedException e) {
    // 熔断器开路,直接走降级
    return PaymentResult.degraded("服务暂时不可用,请稍后重试");
}

熔断的触发条件设计

条件 1:错误率(Error Rate)
  最近 N 次请求中,失败率 > 50%

条件 2:慢调用率(Slow Call Rate)
  响应时间 > 1s 的请求占比 > 50%
  → 下游"看起来成功但实际很慢"也要熔断

条件 3:异常次数(Consecutive Failures)
  连续失败 5 次 → 立即熔断

推荐:条件 1 + 条件 2 组合(兼顾速度和成功率)

3)降级

降级的层次

Level 1:返回缓存数据(最优)
  "商品详情"接口失败 → 返回 Redis 缓存的旧数据(可能 5 分钟前的)
  用户几乎感知不到

Level 2:返回默认值 / 空数据
  推荐系统失败 → 返回热榜商品(固定列表,预写入代码)
  用户感知到"不个性化",但不崩溃

Level 3:返回友好提示
  "当前服务繁忙,请稍后重试"
  关键业务不能这样(如支付),非关键可以

Level 4:功能降级(关闭非核心功能)
  评论功能挂了 → 商品详情页仍然显示,评论区显示"暂时不可用"
  不影响购买主流程

多档位降级(T-k 日桶)

场景:每日跑批依赖昨日数据(T-1),但数据可能延迟

降级档位:
  档位 0:使用 T-1 数据(最新,正常流程)
  档位 1:T-1 数据未就绪 → 使用 T-2 数据(降一档)
  档位 2:T-2 也不可用 → 使用 T-7 数据(降两档)
  档位 3:所有历史数据不可用 → 使用固定基线数据(最保守)
  
配置化:
  bucket.level=0 → 正常
  bucket.level=1 → 告警 + 使用 T-2
  
触发方式:
  自动:监控 T-1 数据就绪状态,自动切换档位
  手动:紧急时运营同学在管控平台手动切档位

4)组合使用(雪崩防御体系)

外部流量 → 网关限流(Nginx/API Gateway)
         → 服务限流(Sentinel,业务粒度)
         → 熔断保护(Resilience4j,下游粒度)
         → 降级兜底(返回缓存/默认值)

监控告警体系:
  限流次数 > 阈值 → 告警(可能被攻击/流量异常)
  熔断次数 > N → 告警(下游服务故障)
  降级触发 → 告警(影响数据质量)

延伸追问

  • Q:限流和熔断的触发点有什么区别? → 限流在入口控制(控制进来多少请求),超限直接拒绝,保护本服务;熔断在出口控制(控制对下游的调用),下游异常时停止调用,保护下游和本服务(避免等待积累)。两者互补:限流防外部冲击,熔断防依赖故障传播。
  • Q:Sentinel 和 Resilience4j 怎么选? → Sentinel:Java + Spring Cloud,功能更完整(限流+熔断+降级+系统保护+热点参数),有控制台(实时调整规则),国内互联网主流;Resilience4j:轻量级(纯函数式,无侵入),适合 Spring Boot 微服务,国外更流行。简单说:国内 Java 服务用 Sentinel,轻量化选 Resilience4j。
  • Q:如何判断需要降级而不是报错? → 看业务影响:核心链路不可降级(支付、下单核心步骤);边缘功能可降级(推荐、评论、统计)。判断标准:降级后用户是否还能完成主要目标。降级设计是产品 + 技术共同决定的,需要定义”核心功能”和”非核心功能”的边界。

我的记法

  • 限流:令牌桶(允许突发),漏桶(平滑),Redis 滑动窗口
  • 熔断:CLOSED → OPEN(错误率达阈值)→ HALF_OPEN(试探)→ CLOSED
  • 降级:缓存 → 默认值 → 友好提示 → 关闭非核心功能
  • 组合:网关限流 + 服务限流 + 熔断 + 降级 = 雪崩防御四层
  • 一句话:「限流挡洪水,熔断隔故障,降级保可用」

状态

  • 已背速记
  • 能画熔断器三态状态机
  • 能说降级的四个层次