高并发系统的限流与熔断策略:从算法原理到生产实战

发布日期: 2026-10-04 主题: 系统架构/分布式

一、为什么需要限流与熔断

在现代分布式系统架构中,服务器面临着各种各样的流量冲击。熔断器(Circuit Breaker)可以保护系统不被海量的请求压垮,限流则确保在高负载下系统依然能够稳定运行。

典型的场景包括:大促活动时的突发流量、热门商品抢购时的尖峰请求,或者某个下游服务宕机导致的重试风暴(Retry Storm)。这些场景都需要明确的策略来应对,否则可能出现级联故障,最终导致整个系统不可用。

限流与熔断的核心目标不同:限流是对"流量整形",保护系统免受过载;熔断是"快速失败",防止故障扩散。两者结合使用,才能构建真正健壮的高并发系统。

二、限流算法详解

2.1 计数器窗口(Counter Window)

最直观最简单的限流算法。统计在固定时间窗口内的请求数量,超过阈值则拒绝新请求。优点是实现简单、及时性强;缺点是存在明显的窗口边界效应——在两个窗口交界处可能放行两倍的流量。

// 简化版计数器窗口限流
class RateLimiter {
  constructor(maxRequests, windowSizeMs) {
    this.maxRequests = maxRequests;
    this.windowSizeMs = windowSizeMs;
    this.requests = [];
  }

  allowRequest() {
    const now = Date.now();
    // 清理过期请求
    this.requests = this.requests.filter(t => now - t < this.windowSizeMs);
    if (this.requests.length < this.maxRequests) {
      this.requests.push(now);
      return true;
    }
    return false;
  }
}

2.2 固定窗口 vs 滑动窗口

固定窗口的边界效应可以通过滑动窗口避免。滑动窗口将时间窗口细分为多个子窗口,记录每个子窗口的请求数。当时间滑动时,统计覆盖当前时间的所有子窗口请求数总和,从而平滑细粒度控制。

// 滑动窗口限流实现
class SlidingWindowLimiter {
  constructor(maxRequests, windowSizeMs, subWindowCount) {
    this.maxRequests = maxRequests;
    this.subWindowSize = windowSizeMs / subWindowCount;
    this.windows = new Array(subWindowCount).fill(0);
    this.currentIndex = 0;
    this.lastTime = Date.now();
  }

  allowRequest() {
    this.rotateWindows();
    const total = this.windows.reduce((a, b) => a + b, 0);
    if (total < this.maxRequests) {
      this.windows[this.currentIndex]++;
      return true;
    }
    return false;
  }

  rotateWindows() {
    const now = Date.now();
    const elapsed = now - this.lastTime;
    const steps = Math.floor(elapsed / this.subWindowSize);
    if (steps > 0) {
      for (let i = 0; i < Math.min(steps, this.windows.length); i++) {
        this.currentIndex = (this.currentIndex + 1) % this.windows.length;
        this.windows[this.currentIndex] = 0;
      }
      this.lastTime += steps * this.subWindowSize;
    }
  }
}

2.3 漏桶算法(Leaky Bucket)

漏桶算法的核心思想:请求以任意速率流入"桶"中,处理逻辑以恒定速率从"桶底"漏出。如果流入速度持续大于流出速度,桶会满,此时新请求将被丢弃或排队等待。优点是输出速率恒定,天然地对下游友好;缺点是无法应对短时突发流量。

// 漏桶算法实现
class LeakyBucket {
  constructor(capacity, leakRatePerSecond) {
    this.capacity = capacity;           // 漏桶容量
    this.leakRate = leakRatePerSecond;  // 每秒流出数量
    this.water = 0;                     // 当前水位
    this.lastLeakTime = Date.now();
  }

  allowRequest() {
    const now = Date.now();
    // 计算经过的流出量
    const elapsed = (now - this.lastLeakTime) / 1000;
    this.water = Math.max(0, this.water - elapsed * this.leakRate);
    this.lastLeakTime = now;

    if (this.water < this.capacity) {
      this.water += 1;
      return true; // 允许请求
    }
    return false; // 桶已满,拒绝
  }
}

2.4 令牌桶算法(Token Bucket)

令牌桶是漏桶的改进版:系统以固定速率往桶里放入令牌,每个请求到达时需要消耗一个令牌。如果桶中有足够令牌,请求通过;如果令牌不足,请求被拒绝或等待。令牌桶允许在桶满时利用积累的令牌应对短时突发流量,兼具限流和一定的弹性能力。Guava RateLimiter 就是经典的令牌桶实现。

// 令牌桶算法实现
class TokenBucket {
  constructor(capacity, refillRatePerSecond) {
    this.capacity = capacity;             // 最大令牌数
    this.refillRate = refillRatePerSecond; // 每秒新增令牌数
    this.tokens = capacity;
    this.lastRefillTime = Date.now();
  }

  allowRequest(tokensRequired = 1) {
    this.refill();
    if (this.tokens >= tokensRequired) {
      this.tokens -= tokensRequired;
      return true;
    }
    return false;
  }

  refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefillTime) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
    this.lastRefillTime = now;
  }
}

2.5 四大算法对比

计数器窗口简单但有边界问题;滑动窗口解决边界但实现更复杂;漏桶保证输出恒定但缺乏弹性;令牌桶在保证限流的同时支持突发流量,是最均衡的选择。在实际生产环境中,令牌桶和滑动窗口是最常用的方案。

三、熔断器模式(Circuit Breaker)

3.1 三种状态机

熔断器模式由 Michael Nygard 在《Release It!》中提出。核心思路是通过监控下游服务的健康状态,自动切断故障通路的请求,防止故障蔓延。

  • Closed(闭合状态):正常工作,记录失败次数。当失败次数超过阈值,切换到 Open 状态。
  • Open(断开状态):熔断器打开,所有请求直接返回失败(快速失败),不实际调用下游。持续一段时间(如30秒)后切换到 Half-Open。
  • Half-Open(半开状态):放行少量请求尝试探测下游恢复情况。如果探测成功,回到 Closed;如果仍然失败,回到 Open。
enum CircuitState { CLOSED, OPEN, HALF_OPEN }

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;    // 失败次数阈值
    this.resetTimeout = options.resetTimeout || 30000;        // 重试超时(ms)
    this.halfOpenMaxRequests = options.halfOpenMaxRequests || 1;
    this.state = CircuitState.CLOSED;
    this.failureCount = 0;
    this.firstFailureTime = 0;
    this.halfOpenRequests = 0;
  }

  async invoke(fn) {
    if (this.state === CircuitState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitState.HALF_OPEN;
        this.halfOpenRequests = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    if (this.state === CircuitState.HALF_OPEN && this.halfOpenRequests >= this.halfOpenMaxRequests) {
      throw new Error('Circuit breaker is HALF_OPEN, limit reached');
    }

    if (this.state === CircuitState.HALF_OPEN) {
      this.halfOpenRequests++;
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    if (this.state === CircuitState.HALF_OPEN) {
      this.state = CircuitState.CLOSED;
    }
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.state = CircuitState.OPEN;
      this.firstFailureTime = Date.now();
    }
  }

  shouldAttemptReset() {
    return Date.now() - this.firstFailureTime >= this.resetTimeout;
  }
}

3.2 主流熔断器框架

  • Resilience4j:Java 生态中最流行的容错库,支持熔断、限流、重试、隔离舱、时间限制器等多种策略。配合 Spring Boot 使用非常方便。
  • Sentinel:阿里巴巴开源的流量防护组件,提供流量控制、熔断降级、系统负载保护、热点参数限流等丰富的功能,并带有实时监控大盘。
  • istio/Envoy:在 Service Mesh 层面通过 Sidecar 代理实现熔断,对业务代码零侵入。
  • Polly:.NET 平台的容错和瞬态故障处理库,支持熔断、重试、超时、舱壁隔离和回退策略。

四、自适应限流:从静态到动态

传统限流依赖人工设置阈值,但生产环境的负载是动态变化的。自适应限流(Adaptive Rate Limiting)能根据系统指标自动调整限流阈值,避免"阈值设低了浪费资源、设高了保护不住"的困境。

4.1 基于 CPU 使用率的自适应限流

// 基于 CPU 负载的自适应限流
class CpuAdaptiveLimiter {
  constructor(maxRps, cpuThreshold = 0.75) {
    this.maxRps = maxRps;            // 最大上限
    this.cpuThreshold = cpuThreshold; // CPU 阈值
    this.currentRps = maxRps;        // 当前允许的 RPS
    this.minRps = Math.floor(maxRps * 0.1); // 最小保障
  }

  async adjust() {
    const cpuUsage = await this.getCpuUsage();
    if (cpuUsage > this.cpuThreshold) {
      this.currentRps = Math.max(this.minRps, Math.floor(this.currentRps * 0.9));
    } else {
      this.currentRps = Math.min(this.maxRps, Math.ceil(this.currentRps * 1.05));
    }
  }

  allowRequest() {
    // 使用令牌桶算法,用 currentRps 作为 refillRate
    return this.tokenBucket.allowRequest();
  }
}

4.2 基于响应时间(P99)的自适应限流

另一种思路是监控系统的 P99 或 P95 响应时间。如果响应时间超过目标值,说明系统过载,需要收紧限流;如果响应时间良好,可以适度放宽。

// 基于 P99 响应时间的自适应限流
class ResponseTimeAdaptiveLimiter {
  constructor(targetP99, initialLimit) {
    this.targetP99 = targetP99;       // 目标 P99 延迟(ms)
    this.limit = initialLimit;         // 当前并发限制
    this.inFlight = 0;
    this.responseTimes = [];
  }

  async invoke(fn) {
    if (this.inFlight >= this.limit) throw new Error('Overloaded');
    this.inFlight++;
    const start = Date.now();
    try { return await fn(); }
    finally {
      this.inFlight--;
      const rt = Date.now() - start;
      this.recordResponseTime(rt);
    }
  }

  recordResponseTime(rt) {
    this.responseTimes.push(rt);
    if (this.responseTimes.length >= 100) {
      this.responseTimes.sort((a, b) => a - b);
      const p99 = this.responseTimes[Math.floor(this.responseTimes.length * 0.99)];
      if (p99 > this.targetP99) {
        this.limit = Math.max(1, Math.floor(this.limit * 0.9));
      } else {
        this.limit = Math.ceil(this.limit * 1.05);
      }
      this.responseTimes = [];
    }
  }
}

五、生产环境下的最佳实践

5.1 多层防护策略

单一限流或熔断无法应对所有场景。生产环境通常采用多层防护:

  • CDN/WAF 层:拦截 DDoS 攻击和异常 User-Agent,保护基础设施。
  • 网关层(API Gateway):统一限流(基于 IP、用户、接口维度),拦截非法请求。
  • 服务层(Service Mesh):微服务间的熔断和超时控制,防止级联故障。
  • 数据层:数据库连接池限制、慢查询熔断。

5.2 关键指标监控

限流和熔断的落地离不开完善的监控告警体系:

  • QPS / 限流拒绝率:了解限流是否过于宽松或过于严格
  • 熔断器状态变更次数:频繁熔断说明下游服务不稳定
  • P50/P95/P99 响应时间:判断系统健康度
  • 成功率 / 错误率:衡量熔断触发的时机是否合理

5.3 灰度发布与配置热更新

限流和熔断的参数不应该硬编码在代码中。通过配置中心(如 Nacos、Apollo、Consul)实现参数的动态调整,可以做到:

  • 大促前预热提高限流阈值
  • 服务异常时紧急降低熔断阈值或手动开启熔断
  • A/B 测试不同限流策略的效果

5.4 避退重试与抖动(Backoff & Jitter)

熔断器触发后,客户端不应立即无脑重试,而应采用指数退避 + 随机抖动(Exponential Backoff with Jitter)的重试策略,避免多个客户端同时重试造成"惊群效应"。

function exponentialBackoffWithJitter(attempt, baseDelay = 100, maxDelay = 10000) {
  const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
  const jitter = delay * 0.25 * Math.random();
  return delay + jitter;
}

六、总结

限流和熔断是高并发系统架构中不可或缺的两种防御手段。限流像是"节流阀",控制系统接受的流量;熔断则是"保险丝",在危险来临时主动切断以保护整体。

本文覆盖了从基础算法到生产实战的完整知识体系:

  • 限流算法:计数器窗口、滑动窗口、漏桶、令牌桶,各有适用场景
  • 熔断器模式:Closed/Open/Half-Open 三种状态的自动状态机
  • 自适应限流:基于 CPU、响应时间等指标动态调整限流阈值
  • 生产实践:多层防护、监控告警、配置热更新、退避重试

没有银弹,只有多层次的综合防护。希望本文能为各位架构师和工程师在面对高并发挑战时提供有价值的参考。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部