主题
限流、熔断与降级:微服务的三道防线
本文是微服务系统学习系列的 L2 核心篇。前置:29. 服务通信:REST、gRPC 与序列化。 学完可以配合面试题食用:05-service-fault-tolerance-hystrix-sentinel-circuit-breaker-rate-limit-retry-timeout、11-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