"流量突然涨 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