深蓝黑背景封面:左侧大标题「流量洪峰来了,系统怎么自保」,副标题「限流、熔断、降级的触发条件与配合」;右侧原创示意——一道闸门挡住汹涌而来的请求流,闸门后三枚盾牌分别标「限」「熔」「降」;左上标注「知识专栏 · 服务保护」,右上标注「Day 03 · 10-21」 图 1:本文封面——流量洪峰面前,三板斧各守一段防线。

「如何设计一个高并发系统」是面试常客,那道题的解析主轴是整体分层架构,本文不重复展开;今天换个角度,聊真正决定系统死活的三件事:限流、熔断、降级。

结论先行:三板斧的分工是「限流守入口、熔断守出口、降级兜全局」。限流挡住超过处理能力的流量,熔断切断对不稳定下游的调用,降级在资源紧张时牺牲次要功能保住核心链路。三者共用同一套指标与预案,构成完整的自我保护体系。

限流:入口的闸门,先讲算法。

限流的本质是回答「单位时间放多少请求进来」。四种主流算法各有性格:固定窗口计数最简单,但窗口边界会放进两倍流量;滑动窗口把统计区间切小格,摊平边界毛刺;漏桶按恒定速率出水,流量被强行整形,代价是突发被抹平;令牌桶按速率发令牌、允许积攒,桶内有余量时突发可以瞬间通过——既限速又容忍突发,是工程上最常用的形态。

以开源组件 Sentinel 的限流规则为参照:阈值类型分 QPS 与并发线程数两种,控制效果有直接拒绝、Warm Up(冷启动缓慢放量)、匀速排队(对应漏桶)三种。

熔断:出口的分界线,盯下游健康。

熔断解决另一个方向的问题:不是你被流量打垮,而是你的下游病了。下游变慢、错误率升高,你的线程堆积在等待上,最终把自己拖死,故障顺着调用链向上传导,这就是雪崩。熔断器是调用端的自我保护:监控下游调用的失败率,越过阈值就断开调用,请求直接快速失败。

熔断器是一个三态状态机。闭合状态正常放行并统计失败情况;失败率(或慢调用比例)超阈值跳到打开状态,请求快速失败;经过冷却窗口进入半开状态,放少量探针试探:成功回闭合,失败重新打开。以 Sentinel 的熔断规则为参照:策略有慢调用比例、异常比例、异常数三种,触发要求统计时长内请求数先超过最小样本量——样本不够即使全挂也不熔断,避免零星请求制造假警报。

降级:全局的取舍,保住核心。

降级是产品经理也得点头的那一个:资源不够时主动关掉次要功能,把容量让给核心链路。常见动作包括返回缓存旧数据、兜底静态内容、关闭非核心入口。降级的难点不在技术而在预案——哪些功能可降级、降级成什么样、谁拨开关,必须提前写成预案并演练。

三板斧怎么用代码落地?

下面这份代码实现了一个令牌桶和一个三态熔断器,实测洪峰拦截与「下游故障—熔断—恢复」的过程。用 Node.js 22.15.0 直接运行(node protection.js)即可:

'use strict';

// 令牌桶限流器:按速率发令牌,拿到令牌才放行
class TokenBucket {
  constructor(ratePerSec, capacity) {
    this.rate = ratePerSec;
    this.capacity = capacity;
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }
  tryAcquire() {
    const now = Date.now();
    this.tokens = Math.min(this.capacity, this.tokens + ((now - this.lastRefill) / 1000) * this.rate);
    this.lastRefill = now;
    if (this.tokens >= 1) { this.tokens -= 1; return true; }
    return false;
  }
}

// 熔断器:闭合 -> 打开 -> 半开 三态
class CircuitBreaker {
  constructor({ failureThreshold = 0.5, minRequests = 4, cooldownMs = 100 } = {}) {
    this.state = 'CLOSED';
    this.window = [];
    this.cooldownMs = cooldownMs;
    this.failureThreshold = failureThreshold;
    this.minRequests = minRequests;
    this.openedAt = 0;
  }
  call(fn, fallback) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.openedAt < this.cooldownMs) return fallback('熔断中,快速失败');
      this.state = 'HALF_OPEN'; // 冷却结束,放探针
    }
    try {
      const result = fn();
      this.record(true);
      if (this.state === 'HALF_OPEN') this.state = 'CLOSED'; // 探针成功
      return result;
    } catch (err) {
      this.record(false);
      if (this.state === 'HALF_OPEN') { this.trip(); } // 探针失败,重新熔断
      return fallback(err.message);
    }
  }
  record(ok) {
    this.window.push(ok);
    if (this.window.length > this.minRequests) this.window.shift();
    if (this.window.length >= this.minRequests) {
      const failures = this.window.filter((x) => !x).length;
      if (failures / this.window.length >= this.failureThreshold) this.trip();
    }
  }
  trip() { this.state = 'OPEN'; this.openedAt = Date.now(); this.window = []; }
}

// 演示 1:每秒 2 个令牌、容量 2,瞬间打 6 个请求
const bucket = new TokenBucket(2, 2);
const results = [];
for (let i = 1; i <= 6; i++) results.push(bucket.tryAcquire() ? '放行' : '拒绝');
console.log(`令牌桶(速率2/秒,容量2)连续 6 个请求:${results.join('、')}`);

// 演示 2:下游 4 次连续故障触发熔断,冷却后探针恢复
const breaker = new CircuitBreaker({ failureThreshold: 0.5, minRequests: 4, cooldownMs: 50 });
let downstreamHealthy = false;
const downstream = () => {
  if (!downstreamHealthy) throw new Error('下游超时');
  return '下游正常返回';
};
const fallback = (why) => `降级响应(${why})`;
for (let i = 1; i <= 4; i++) {
  console.log(`第 ${i} 次调用:${breaker.call(downstream, fallback)},熔断器=${breaker.state}`);
}
console.log(`熔断后立刻再调:${breaker.call(downstream, fallback)},熔断器=${breaker.state}`);
await new Promise((r) => setTimeout(r, 60)); // 等过冷却窗口
downstreamHealthy = true;
console.log(`冷却后探针调用:${breaker.call(downstream, fallback)},熔断器=${breaker.state}`);
console.log(`恢复后再调:${breaker.call(downstream, fallback)},熔断器=${breaker.state}`);

实测输出:

令牌桶(速率2/秒,容量2)连续 6 个请求:放行、放行、拒绝、拒绝、拒绝、拒绝
第 1 次调用:降级响应(下游超时),熔断器=CLOSED
第 2 次调用:降级响应(下游超时),熔断器=CLOSED
第 3 次调用:降级响应(下游超时),熔断器=CLOSED
第 4 次调用:降级响应(下游超时),熔断器=OPEN
熔断后立刻再调:降级响应(熔断中,快速失败),熔断器=OPEN
冷却后探针调用:下游正常返回,熔断器=CLOSED
恢复后再调:下游正常返回,熔断器=CLOSED

两段输出对应两个核心行为:令牌桶容量耗尽后立即拒绝,守住入口水位;熔断器攒够四次失败样本才打开,冷却期内请求不再打到下游,冷却后一个探针完成恢复。注意熔断打开期间走的就是降级路径(fallback 兜底响应)——熔断负责发现「该降级了」,降级负责回答「降级成什么样」。

分工加状态机图:上方三张分工卡——限流在入口挡过量请求,令牌桶按速率放令牌、没令牌就拒,滑动窗口统计单位时间请求数;熔断盯下游健康,错误率超阈值就断开调用;降级做取舍,保住核心链路、牺牲次要功能。下方熔断器状态机:闭合状态正常放行,失败率超阈值跳到打开、全部快速失败,经过冷却窗口进入半开、放少量探针请求,探针成功回闭合、失败回打开。右侧配合关系:限流守入口、熔断守出口、降级兜全局,三者共用同一套指标与预案 图 2:三板斧的分工与熔断器状态机全貌。

常见误区有三个。 一是熔断阈值过于敏感,零星抖动就熔断——所以成熟组件都带最小样本量门槛。二是降级没有预案,洪峰时临时讨论「关哪个功能」,错过最佳窗口。三是限流阈值拍脑袋:不压测直接写个整数,要么没拦住、要么误杀正常流量,阈值应来自压测水位再打折扣。

行动建议:给核心接口压测定水位,限流阈值按水位打折;给所有下游调用挂熔断与兜底;把可降级功能列成清单并预演。你的系统里三板斧配齐了吗,降级预案演练过吗?欢迎聊聊。