Skip to content

限流、熔断与降级:微服务的三道防线

本文是微服务系统学习系列的 L2 核心篇。前置:29. 服务通信:REST、gRPC 与序列化。 学完可以配合面试题食用:05-service-fault-tolerance-hystrix-sentinel-circuit-breaker-rate-limit-retry-timeout11-circuit-breaker-hystrix-threadpool-sentinel-semaphore-half-open

为什么需要三道防线:从雪崩说起

服务 A 调用 B,B 调用 C,C 调用 D。某天 C 一台机器的磁盘满了,CPU 卡在 100% 上,响应的平均耗时从 10ms 飙到 10s。B 的 HTTP 连接池会怎么样?它把可用线程全占着等 C 回包,A 的请求开始排队,然后 A 的 CPU 也满了。一直蔓延到网关,用户端全员 502。

这就是雪崩:一个慢服务拖死整条链上的所有服务。代码里没有防御机制的话,上线就是拼运气。

三道防线各司其职:

  • 限流:不让自己的服务被过量请求冲垮
  • 熔断:不让下游的故障拖死自己
  • 降级:扛不住的时候主动放弃次要功能,保核心链路

限流算法四件套:选对场景比实现更重要

限流解决的是"过量请求"问题。四个主流算法,各有缺陷。

固定窗口计数器

java
// 每分钟最多 100 次请求
long currentMinute = System.currentTimeMillis() / 60000;
if (counter[currentMinute] >= 100) reject();

最直观,但临界问题致命:第 59 秒涌进 100 个请求,第 60 秒又涌进 100 个,1 秒内实际通过了 200 个。固定窗口的边界不是均匀的。

滑动窗口

把一分钟切成 6 个 10 秒小格,每格计数,窗口移动时减去过期格子的计数。精度由格子数决定,但内存消耗也线性增长。能解决临界问题,代价是维护滑动队列。

漏桶

请求先进桶,桶以恒定速率漏出(处理)。桶满直接丢弃。优点是输出完全平滑,缺点是一旦流量突增,大量请求被直接丢掉,即使后端有空闲能力。

令牌桶

按固定速率往桶里加令牌,请求来了拿令牌才放行。桶有上限,允许短时突发。这是使用最广的——Sentinel 默认的匀速排队模式就是令牌桶变体,而 Guava 的 RateLimiter 也是令牌桶实现。

一句话选型:需要严格平滑→漏桶;需要容忍突发→令牌桶;精度要求不高+实现简单→固定窗口(配合适当阈值余量)。

熔断器状态机:不是保险丝

很多人把熔断类比成"保险丝",其实有本质区别:

  • 保险丝熔断后需要人工换
  • 熔断器 half-open 状态会自动试活
mermaid
stateDiagram-v2
    [*] --> Closed
    Closed --> Open : 错误率/慢调用率 > 阈值
    Open --> HalfOpen : 等待超时(熔断时长)
    HalfOpen --> Closed : 探针请求成功
    HalfOpen --> Open : 探针请求失败

Closed 状态下正常调用,按滑动窗口统计错误率或慢调用率。超过阈值(比如 50% 请求耗时 > 1s)就转到 Open,直接抛异常或返回 fallback,不再去调用下游。

Open 持续一段时间(通常是秒级,比如 5 秒),然后进 HalfOpen:放一个探针请求过去。成功了就认为下游恢复了,状态切回 Closed,恢复正常调用。失败了就回到 Open,再等一个周期。

这个自愈机制是微服务容错的核心——不需要人工介入,系统自己判断上下游是否健康。

降级设计:有损服务的取舍

降级不是"系统崩了再想办法",而是预先设计好哪些功能可以在压力下放弃

典型做法:

  • 返回兜底数据:比如推荐系统挂了,返回默认的热门缓存列表,不返回空
  • 异步化:非核心操作的写操作(如操作日志、埋点)改为异步写入,不阻塞主流程
  • 砍功能:大促时关闭"猜你喜欢"这种非核心接口,把机器资源让给支付和下单

降级的关键是开关要预先埋。上线后再加开关,等于改代码重新部署,黄花菜都凉了。

Sentinel 实操:规则与动态推送

Sentinel 是阿里的流量防卫组件,资源粒度的限流/熔断/降级。核心思路是:给每个受保护的方法或代码块贴 @SentinelResource 注解,然后外部配置规则。

java
@SentinelResource(
    value = "createOrder",
    fallback = "createOrderFallback",
    blockHandler = "createOrderBlockHandler"
)
public Order createOrder(OrderDTO dto) {
    // 实际业务逻辑
    return orderService.doCreate(dto);
}

public Order createOrderFallback(Throwable t) {
    // 熔断/降级时的兜底:返回一个"排队中"的占位对象
    log.warn("createOrder fallback due to: {}", t.getMessage());
    return Order.placeholder("排队中,请稍后查询结果");
}

public Order createOrderBlockHandler(BlockException e) {
    // 限流时的兜底(与 fallback 区分)
    return Order.placeholder("请求过载,请稍后重试");
}

规则配置写到 Nacos 里,这样改规则不用重启:

yaml
# Nacos 配置中心 dataId: sentinel-rules-createOrder.json
[
  {
    "resource": "createOrder",
    "controlBehavior": 2,         # 0 快速失败,1 Warm Up,2 匀速排队
    "count": 500.0,               # QPS 阈值
    "grade": 1,                   # 0 线程数,1 QPS,2 关联,3 链路
    "strategy": 0,
    "limitApp": "default"
  },
  {
    "resource": "createOrder",
    "grade": 0,                   # 熔断规则:0 慢调用比例,1 异常比例,2 异常数
    "count": 1000.0,              # 慢调用阈值(ms)
    "timeWindow": 10,             # 熔断时长(秒)
    "minRequestAmount": 5,
    "statIntervalMs": 1000,
    "slowRatioThreshold": 0.5     # 慢调用比例阈值
  }
]

Sentinel 的 DataSource 监听 Nacos 配置变更,规则更新后秒级生效。这就是"熔断阈值调多少不需要重启"的实现方式。

手写一个滑动窗口限流器(30 行)

java
public class SlidingWindowRateLimiter {
    private final long windowSizeMs;       // 窗口总时长
    private final int bucketCount;         // 分多少格
    private final long bucketSizeMs;       // 每格时长
    private final long maxRequests;        // 窗口内总请求上限
    private final AtomicReferenceArray<Bucket> buckets;

    public SlidingWindowRateLimiter(long windowSizeMs, int bucketCount, long maxRequests) {
        this.windowSizeMs = windowSizeMs;
        this.bucketCount = bucketCount;
        this.bucketSizeMs = windowSizeMs / bucketCount;
        this.maxRequests = maxRequests;
        this.buckets = new AtomicReferenceArray<>(bucketCount);
        for (int i = 0; i < bucketCount; i++) {
            buckets.set(i, new Bucket(0, 0));
        }
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        int idx = (int)((now / bucketSizeMs) % bucketCount);
        Bucket cur = buckets.get(idx);
        if (now - cur.time >= bucketSizeMs) {
            buckets.set(idx, new Bucket(now, 0));
        }
        buckets.get(idx).count++;
        return totalCount(now) <= maxRequests;
    }

    private long totalCount(long now) {
        long count = 0;
        for (int i = 0; i < bucketCount; i++) {
            Bucket b = buckets.get(i);
            if (now - b.time < windowSizeMs) count += b.count;
        }
        return count;
    }

    static class Bucket {
        long time; long count;
        Bucket(long t, long c) { this.time = t; this.count = c; }
    }
}

常见误区与小结

  • 熔断超时设太长:熔断时长 5-10 秒就够,设 60 秒意味着服务挂了 5 秒,熔断器硬等 60 秒才试活,浪费了 55 秒
  • 限流阈值拍脑袋:上线前压测拿到真实 TPS 上限,再留 20-30% 余量;拍脑袋容易设太高(没效果)或太低(误伤正常请求)
  • 降级 fallback 和业务代码混在一起:fallback 应该独立,不在业务代码里写 if (degraded) ... 这种分支,改用 Sentinel 注解或 AOP 切面管理
  • 限流和熔断只用一种:两件事不冲突。限流管入口流量,熔断管下游故障,缺一不可
  • 规则配在本地文件里:规则必须动态推送(Nacos/APollo),否则调阈值要重启,等重启完流量高峰已经过了

小结:限流、熔断、降级是微服务容错的三个独立工具,不是同一种东西换个说法。限流挡在自己门口,熔断保护自己不被下游拖死,降级是在扛不住的时候做权衡取舍。Sentinel 把这三种统一成一套注解+规则体系,配好动态推送后,调阈值跟改配置一样简单。

下一篇预告:31. API 网关设计:路由、鉴权与插件化 —— 限流和熔断规则在网关层该怎么落地,以及网关自身的容错设计。

参考

参考:Sentinel 官方文档Guava RateLimiter 源码Martin Fowler 原文 CircuitBreaker

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1