限流熔断降级完全指南:Sentinel 与 Resilience4j 的高可用三件套

"流量突然涨 10 倍,系统怎么活下去?" —— 这是每个互联网工程师都会碰到的问题。限流、熔断、降级是高可用三件套,它们不是"可选优化",而是任何对外服务的必备保险。这篇文章把令牌桶、漏桶、滑动窗口讲透,然后讲熔断器模式、降级策略,以及 Sentinel / Resilience4j 等主流框架的工程实践。

问题:没有保护的系统怎么死

真实事故场景:

  • 某接口正常 QPS 1000。某天前端 bug 让客户端无限重试,QPS 飙到 50000。后端数据库被打满,所有请求超时,前端继续重试 —— 雪崩。
  • 下游支付服务慢了,上游订单服务的线程都在等。1000 个线程都阻塞,新请求来连队列都进不了,业务停摆。
  • 某个 hot key 缓存失效,所有请求穿透到数据库,数据库挂掉,缓存重建不出来,服务永远恢复不了。

这些事故的共性:局部问题被放大成全局崩溃。限流、熔断、降级就是阻止这种放大。

限流算法 1:计数器

最简单粗暴 —— 每秒最多 N 个请求。

class CounterLimiter:
    def __init__(self, limit):
        self.limit = limit
        self.count = 0
        self.reset_time = time.time() + 1

    def allow(self):
        now = time.time()
        if now >= self.reset_time:
            self.count = 0
            self.reset_time = now + 1
        if self.count < self.limit:
            self.count += 1
            return True
        return False

问题:临界突刺。一秒末 100 个请求 + 下一秒初 100 个请求,1 秒内挤 200 个请求。算法看起来"每秒最多 100",实际上短时间内能跑 2 倍。

限流算法 2:漏桶(Leaky Bucket)

请求像水流入桶,桶以固定速率往外漏(处理)。桶满了请求被丢:

class LeakyBucket:
    def __init__(self, capacity, leak_rate):
        self.capacity = capacity
        self.leak_rate = leak_rate    # 每秒漏多少
        self.water = 0
        self.last = time.time()

    def allow(self):
        now = time.time()
        # 计算从上次到现在漏了多少
        self.water = max(0, self.water - (now - self.last) * self.leak_rate)
        self.last = now
        if self.water < self.capacity:
            self.water += 1
            return True
        return False

特性:严格匀速处理,突发流量被吸收 / 拒绝。适合"必须平滑输出"的场景(下游处理能力固定)。

限流算法 3:令牌桶(Token Bucket)

最常用。桶里以固定速率产生令牌,请求来取走一个;没令牌就拒。允许一定突发:

class TokenBucket:
    def __init__(self, capacity, fill_rate):
        self.capacity = capacity
        self.fill_rate = fill_rate    # 每秒生成多少令牌
        self.tokens = capacity
        self.last = time.time()

    def allow(self, n=1):
        now = time.time()
        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.fill_rate)
        self.last = now
        if self.tokens >= n:
            self.tokens -= n
            return True
        return False

桶能蓄水到 capacity,所以瞬时突发能消化 capacity 个请求,之后回归 fill_rate。"平均限速 + 允许突发" 是绝大多数业务的需求。Guava RateLimiter、Spring Cloud Gateway、Nginx limit_req 都用令牌桶。

限流算法 4:滑动窗口

计数器的进化版,解决"临界突刺"。把一秒切成多个小窗口(如 10 个 100ms),滚动统计最近 1 秒:

class SlidingWindow:
    def __init__(self, limit, window_size=1.0, buckets=10):
        self.limit = limit
        self.window_size = window_size
        self.buckets = [0] * buckets
        self.timestamps = [0] * buckets
        self.bucket_size = window_size / buckets

    def allow(self):
        now = time.time()
        idx = int(now / self.bucket_size) % len(self.buckets)
        if now - self.timestamps[idx] >= self.window_size:
            self.buckets[idx] = 0
        self.timestamps[idx] = now

        # 计算近 1 秒总和
        total = sum(self.buckets[i] for i in range(len(self.buckets))
                    if now - self.timestamps[i] < self.window_size)
        if total < self.limit:
            self.buckets[idx] += 1
            return True
        return False

Redis 实现分布式滑动窗口:

# 用 Sorted Set,score = timestamp
ZADD rate:user:1234 {now_ms} {request_id}
ZREMRANGEBYSCORE rate:user:1234 0 {now_ms - 1000}    # 删 1 秒前的
ZCARD rate:user:1234                                  # 看当前窗口数量
# 如果 >= limit,拒绝

分布式限流:Redis + Lua

多实例服务的限流必须共享状态。Redis + Lua 原子脚本是标配:

-- 令牌桶 Lua 脚本
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local tokens_key = key .. ':tokens'
local timestamp_key = key .. ':ts'

local last_tokens = tonumber(redis.call('GET', tokens_key) or capacity)
local last_time = tonumber(redis.call('GET', timestamp_key) or 0)

local elapsed = math.max(0, now - last_time)
local filled = math.min(capacity, last_tokens + elapsed * rate)

if filled < requested then
    return 0    -- 拒绝
else
    redis.call('SET', tokens_key, filled - requested, 'EX', 60)
    redis.call('SET', timestamp_key, now, 'EX', 60)
    return 1    -- 允许
end

Lua 在 Redis 里原子执行,避免多客户端竞争。所有限流框架(包括 Spring Cloud Gateway、阿里 Sentinel)在分布式模式下都是这套思路。

熔断器模式:防止级联故障

限流防"流量太大",熔断防"下游已经崩了,你还在打它"。状态机:

Closed(正常)
  请求正常通过,统计失败率
  失败率超阈值 -> 切换到 Open

Open(熔断)
  立刻返回失败(快速失败),不调下游
  等几秒后 -> 切换到 Half-Open 试探

Half-Open(半开)
  放少量请求过去试探下游是否恢复
  成功 -> 回 Closed
  失败 -> 回 Open

用 Resilience4j 的实现:

import io.github.resilience4j.circuitbreaker.*;

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)                  // 失败率 > 50% 触发
    .waitDurationInOpenState(Duration.ofSeconds(10))
    .slidingWindowSize(100)                    // 用最近 100 个请求统计
    .minimumNumberOfCalls(20)                  // 至少 20 个请求才统计
    .permittedNumberOfCallsInHalfOpenState(5)  // 半开时试 5 个
    .build();

CircuitBreaker cb = CircuitBreaker.of("paymentService", config);

Supplier<PaymentResult> decorated = CircuitBreaker.decorateSupplier(cb, () -> paymentApi.pay(order));

try {
    PaymentResult r = decorated.get();
} catch (CallNotPermittedException e) {
    // 熔断器 Open 状态,直接走降级
    return fallbackResult();
}

熔断器和限流的关键差别:限流看"流量",熔断看"健康度"。下游服务即使流量不大,但 90% 都失败,熔断器也会断;限流不管这些。

降级:断之后做什么

熔断让请求快速失败,但用户体验需要"失败时给点什么"。降级策略:

  • 静态降级:返回默认值或缓存的旧数据。商品价格服务挂了,返回"暂时无法显示价格"。
  • 功能降级:关闭次要功能保主要功能。大促时关推荐 / 关评论,保下单。
  • 读多写少时降级写:写入暂时进消息队列,异步处理。
  • 动态降级:配置中心一键开关,运维或自动触发。Apollo / Nacos 都支持。
# Hystrix 风格降级
@HystrixCommand(fallbackMethod = "fallbackPrice")
public BigDecimal getPrice(Long skuId) {
    return priceService.getPrice(skuId);
}

private BigDecimal fallbackPrice(Long skuId) {
    return cache.getOrDefault(skuId, BigDecimal.valueOf(-1));
}

Sentinel:阿里开源的现代框架

Sentinel 是 2018 年开源的"流量哨兵",集限流 / 熔断 / 降级 / 系统保护于一体。和老 Hystrix 比的优势:更细粒度、有控制台、支持热点参数限流、动态规则。

// 注解风格
@SentinelResource(value = "getUserOrders",
    blockHandler = "handleBlock",
    fallback = "handleFallback")
public List<Order> getUserOrders(Long userId) {
    return orderService.getByUser(userId);
}

public List<Order> handleBlock(Long userId, BlockException e) {
    // 被限流/熔断时调用
    return Collections.emptyList();
}

public List<Order> handleFallback(Long userId, Throwable e) {
    // 业务异常时调用
    return cachedOrders(userId);
}

规则在控制台可视化配置,无需改代码 —— 这是 Sentinel 比 Hystrix 大的运维优势。

系统层保护:CPU / 内存 / 入口 QPS

除了接口级保护,还要有"系统不能被打挂"的兜底:

  • 系统负载保护:CPU > 70% 时全局限流。
  • RT(响应时间)保护:平均 RT > X ms 触发降级。
  • 线程数保护:某接口同时占用线程数过多,新请求拒绝。
  • OOM 预警:堆内存 > 阈值,主动 GC + 限流。

Sentinel 的"System Adaptive Protection"基于 BBR 算法(类似 TCP 拥塞控制),根据 CPU / RT / 并发自适应限流,效果好于固定阈值。

热点参数限流

普通限流是接口维度,但有时需要"同一接口下,某个 hot key 单独限"。例:抢购接口对每个商品 ID 独立限。

// Sentinel 热点限流
ParamFlowRule rule = new ParamFlowRule()
    .setResource("buyItem")
    .setParamIdx(0)              // 第 0 个参数(skuId)是 hot key 维度
    .setCount(100)               // 每个 skuId 每秒 100 次
    .setDurationInSec(1);

ParamFlowRuleManager.loadRules(List.of(rule));

@SentinelResource("buyItem")
public Result buy(Long skuId, Long userId) { ... }

客户端限流 vs 服务端限流

  • 服务端:接收方限。万一客户端不受控(如开放 API),只能服务端守门。
  • 客户端:发送方主动限。比如调下游前 SDK 内置令牌桶,避免一开始就把下游打挂。

实战:两端都做。客户端限流避免无谓的网络开销,服务端限流是最终保障。

429 状态码与 Retry-After

HTTP 标准里 429 Too Many Requests 专门用于限流。返回时务必带 Retry-After 头告诉客户端多久后再试:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"code": "rate_limit", "message": "请 60 秒后再试"}

好客户端会尊重这个头,避免立刻重试加剧问题。坏客户端不管 —— 这时只能靠 IP 封禁等手段。

常见坑

坑 1:熔断器太敏感。 失败阈值 30% 太低,偶尔的网络抖动就触发熔断,用户感受到大量"系统不可用"。常用 50% + 至少 20 次调用统计 + 10 秒窗口。

坑 2:熔断器不带 fallback。 熔断后立刻报错给用户,体验差。应该有合理的 fallback。

坑 3:限流单位错。 配置"每秒 1000",实际 QPS 是平均 1000 但有突发 5000 —— 客户端"有时被限有时不被限",非常困惑。明确"是平均还是峰值"的语义,文档化。

坑 4:分布式限流的"谁来限"。 10 个实例的服务总 QPS 限 10000,每个实例分 1000?还是用 Redis 共享计数?前者简单但不准确(各实例流量不均),后者准确但 Redis 是瓶颈。中间方案:本地令牌桶 + 周期性向中心拉取配额。

坑 5:限流规则脱离业务。 限流值是测试团队拍脑袋定的,业务量增长后没人调,造成"正常流量被限"。规则要可观测、可动态调整,理想是基于真实指标自适应。

自适应限流:让阈值自己变

静态阈值的麻烦:业务量增长后过保守,或者突发场景过激进。自适应限流根据实时指标自动调整阈值。Sentinel 的"BBR 类"算法基于 TCP 拥塞控制的思想:

// 实时计算允许的最大并发
maxQPS = (CPU 负载<某阈值时) 提升,否则下调
maxConcurrent = recent_max_QPS × recent_min_RT

// 当前并发超过 maxConcurrent 就限

这套机制不需要你定死"每秒 1000"这种数,而是"系统能承受多少就放多少"。下游慢了,自动减少进入;下游恢复了,自动放开。生产用得越来越多。

写在最后

限流熔断降级是分布式系统的"免疫系统"。它们不让系统永远不出问题(那不可能),而是让局部问题不变成全局崩溃。一个没有限流熔断的服务就像没有保险丝的电路 —— 一次过载就烧穿。

给一个工程心得:在系统上线前就接入限流框架,而不是等出事后再加。把"每个对外接口配置默认限流值"做成发布流程的强制项。值可以宽松,但要有 —— 至少在突发流量时给你一个干预的入口。Sentinel / Resilience4j 集成成本一两天,但能救你无数次。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

一致性哈希完全指南:从哈希环到 Jump Hash 与 Redis Cluster

2026-5-15 16:09:29

技术教程

幂等性与分布式锁完全指南:幂等 Token、Redis 锁与 Snowflake ID

2026-5-15 16:19:19

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索