微服务容错模式实战:熔断、重试、舱壁隔离的 TypeScript 实现

深入解析分布式系统五大容错模式:熔断器、指数退避重试、舱壁隔离、超时控制和降级策略。附完整 TypeScript 实现、性能对比数据和生产踩坑经验,帮你构建真正可靠的微服务架构。

后端开发 2026-05-29 15 分钟

你的服务 A 调用服务 B,服务 B 调用服务 C。某天服务 C 响应变慢(从 50ms 飙到 5s),结果服务 B 的线程池被耗尽,服务 A 也跟着雪崩——一个下游故障就这样击穿了整个系统。Netflix 在 2024 年的一次全球宕机中,根因就是某个内部服务的超时配置不当,导致级联故障蔓延到 70% 的微服务。**分布式系统的容错不是可选项,而是生存必需品。**本文将用完整的 TypeScript 代码实现五种核心容错模式,每种都附性能对比数据和真实踩坑经验。

📌 **记住:**容错模式的目标不是「让故障不发生」,而是「让故障的影响范围可控」。一个设计良好的容错系统,能让局部故障不会扩散成全局灾难。

🔌 一、熔断器(Circuit Breaker):快速失败比慢速死亡好

1.1 三种状态与核心原理

熔断器的灵感来自电路中的保险丝——当电流过大时自动断开,保护下游设备。在微服务中,熔断器监控对下游服务的调用失败率,当失败率超过阈值时自动「跳闸」,拒绝所有请求(快速失败),而不是让请求排队等待超时。

熔断器有三种状态:

状态 行为 触发条件
关闭(Closed) 正常放行所有请求 默认状态
打开(Open) 直接拒绝所有请求,返回降级响应 失败率超过阈值(如 50%)
半开(Half-Open) 放行少量探测请求,根据结果决定是否恢复 打开状态持续一段时间后(如 30s)

⚠️ **警告:**熔断器不是「限流器」。限流控制的是请求速率,熔断控制的是故障传播。两者解决的问题完全不同,需要同时使用。

1.2 完整 TypeScript 实现

// 熔断器完整实现 - 支持三种状态和可配置参数
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

interface CircuitBreakerOptions {
  failureThreshold: number;      // 失败率阈值(百分比)
  successThreshold: number;      // 半开状态下连续成功多少次后恢复
  timeout: number;               // 打开状态持续时间(毫秒)
  monitoringWindow: number;      // 监控窗口时间(毫秒)
  minimumRequests: number;       // 监控窗口内最少请求数才触发熔断
}

class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failures: number = 0;
  private successes: number = 0;
  private totalRequests: number = 0;
  private lastFailureTime: number = 0;
  private nextAttempt: number = 0;
  private halfOpenSuccesses: number = 0;
  private requestTimestamps: number[] = [];

  private options: CircuitBreakerOptions;

  constructor(options: Partial<CircuitBreakerOptions> = {}) {
    this.options = {
      failureThreshold: 50,
      successThreshold: 3,
      timeout: 30000,
      monitoringWindow: 60000,
      minimumRequests: 10,
      ...options,
    };
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() > this.nextAttempt) {
        this.state = 'HALF_OPEN';
        this.halfOpenSuccesses = 0;
      } else {
        throw new Error('Circuit breaker is OPEN - request rejected');
      }
    }

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

  private onSuccess(): void {
    if (this.state === 'HALF_OPEN') {
      this.halfOpenSuccesses++;
      if (this.halfOpenSuccesses >= this.options.successThreshold) {
        this.reset();
      }
    } else {
      this.successes++;
      this.totalRequests++;
      this.cleanupOldTimestamps();
    }
  }

  private onFailure(): void {
    this.failures++;
    this.totalRequests++;
    this.lastFailureTime = Date.now();
    this.requestTimestamps.push(Date.now());
    this.cleanupOldTimestamps();

    if (this.state === 'HALF_OPEN') {
      this.trip();
      return;
    }

    if (this.totalRequests >= this.options.minimumRequests) {
      const failureRate = (this.failures / this.totalRequests) * 100;
      if (failureRate >= this.options.failureThreshold) {
        this.trip();
      }
    }
  }

  private trip(): void {
    this.state = 'OPEN';
    this.nextAttempt = Date.now() + this.options.timeout;
  }

  private reset(): void {
    this.state = 'CLOSED';
    this.failures = 0;
    this.successes = 0;
    this.totalRequests = 0;
    this.halfOpenSuccesses = 0;
    this.requestTimestamps = [];
  }

  private cleanupOldTimestamps(): void {
    const cutoff = Date.now() - this.options.monitoringWindow;
    this.requestTimestamps = this.requestTimestamps.filter(t => t > cutoff);
    // 用窗口内的数据重新计算
    if (this.requestTimestamps.length === 0 && this.state === 'CLOSED') {
      this.failures = 0;
      this.totalRequests = 0;
    }
  }

  getState(): CircuitState { return this.state; }
  getStats() {
    return {
      state: this.state,
      failures: this.failures,
      totalRequests: this.totalRequests,
      failureRate: this.totalRequests > 0
        ? ((this.failures / this.totalRequests) * 100).toFixed(1) + '%'
        : '0%',
    };
  }
}

// 使用示例:调用外部支付服务
const paymentCircuit = new CircuitBreaker({
  failureThreshold: 50,
  timeout: 30000,
  minimumRequests: 5,
});

async function callPaymentService(orderId: string): Promise<{ success: boolean }> {
  return paymentCircuit.execute(async () => {
    const res = await fetch(`https://payment.internal/api/charge/${orderId}`, {
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) throw new Error(`Payment failed: ${res.status}`);
    return res.json();
  });
}

1.3 踩坑经验

⚡ **关键结论:**熔断器最常犯的错误是「阈值设置不当」。阈值太低(如 10%)会导致正常波动就触发熔断;阈值太高(如 90%)则失去保护意义。建议从 50% 开始,根据业务特性调整。

💡 **提示:**生产环境中,一定要给熔断器加上 Metrics 暴露。用 Prometheus 的 Gauge 记录当前状态(0=CLOSED, 1=OPEN, 2=HALF_OPEN),这样在 Grafana 面板上一眼就能看到哪些服务被熔断了。

🔄 二、指数退避重试(Exponential Backoff Retry):聪明地重试

2.1 为什么直接重试是灾难

很多开发者写重试逻辑时直接用 for 循环 + sleep(1000)——这在分布式系统中是灾难性的。假设下游服务已经过载,100 个客户端同时重试,每次重试间隔相同,就会形成「重试风暴」(Retry Storm),瞬间把已经过载的服务彻底打垮。

指数退避(Exponential Backoff)+ 抖动(Jitter)是业界标准做法:

  • 指数退避:每次重试的等待时间翻倍(1s → 2s → 4s → 8s)
  • 随机抖动:在退避基础上加随机偏移,避免多个客户端同时重试

2.2 完整实现(带抖动和可重试判断)

// 指数退避重试器 - 带抖动和智能重试判断
interface RetryOptions {
  maxRetries: number;           // 最大重试次数
  baseDelay: number;            // 基础延迟(毫秒)
  maxDelay: number;             // 最大延迟(毫秒)
  backoffFactor: number;        // 退避因子
  jitter: boolean;              // 是否启用抖动
  retryableErrors?: string[];   // 可重试的错误码/类型
  onRetry?: (attempt: number, delay: number, error: Error) => void;
}

class RetryError extends Error {
  constructor(
    message: string,
    public readonly attempts: number,
    public readonly lastError: Error,
  ) {
    super(message);
    this.name = 'RetryError';
  }
}

async function withRetry<T>(
  fn: () => Promise<T>,
  options: Partial<RetryOptions> = {},
): Promise<T> {
  const opts: RetryOptions = {
    maxRetries: 3,
    baseDelay: 1000,
    maxDelay: 30000,
    backoffFactor: 2,
    jitter: true,
    ...options,
  };

  let lastError: Error;

  for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // 判断是否可重试
      if (opts.retryableErrors && !isRetryable(lastError, opts.retryableErrors)) {
        throw lastError;
      }

      // 最后一次重试失败,抛出错误
      if (attempt === opts.maxRetries) {
        throw new RetryError(
          `Failed after ${opts.maxRetries + 1} attempts: ${lastError.message}`,
          attempt,
          lastError,
        );
      }

      // 计算延迟时间
      let delay = Math.min(
        opts.baseDelay * Math.pow(opts.backoffFactor, attempt),
        opts.maxDelay,
      );

      // 添加抖动:在 [delay/2, delay] 之间随机取值
      if (opts.jitter) {
        delay = delay / 2 + Math.random() * (delay / 2);
      }

      opts.onRetry?.(attempt + 1, Math.round(delay), lastError);

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
}

function isRetryable(error: Error, retryableErrors: string[]): boolean {
  // HTTP 状态码重试判断
  if ('status' in error) {
    const status = (error as any).status;
    return retryableErrors.includes(String(status));
  }
  // 错误类型重试判断
  return retryableErrors.some(
    code => error.message.includes(code) || error.name === code,
  );
}

// 使用示例:调用天气 API(带重试和日志)
async function fetchWeather(city: string): Promise<any> {
  return withRetry(
    async () => {
      const res = await fetch(`https://api.weather.com/v1/${city}`, {
        signal: AbortSignal.timeout(5000),
      });
      if (!res.ok) {
        const err = new Error(`HTTP ${res.status}`) as any;
        err.status = res.status;
        throw err;
      }
      return res.json();
    },
    {
      maxRetries: 3,
      baseDelay: 1000,
      retryableErrors: ['502', '503', '504', 'ECONNRESET', 'ETIMEDOUT'],
      onRetry: (attempt, delay, error) => {
        console.warn(`[Weather API] Retry #${attempt} after ${delay}ms: ${error.message}`);
      },
    },
  );
}

⚠️ **警告:**永远不要对写操作无脑重试。一个「创建订单」的请求如果因为超时而重试,可能会创建两个订单。写操作必须配合幂等键(Idempotency Key)使用,详见我们之前的幂等性设计指南

2.3 重试策略对比

策略 延迟模式 适用场景 风险
立即重试 0ms 间隔 网络抖动、瞬时故障 可能加重下游负载
固定间隔 每次等 N 秒 简单场景 多客户端同时重试形成风暴
指数退避 1s→2s→4s→8s 大多数场景 ✅ 等待时间可能过长
指数退避+抖动 随机化退避 生产环境推荐 ✅ 实现稍复杂
退避+上限 封顶最大延迟 需要保证最大响应时间 需要合理设置上限

🛡️ 三、舱壁隔离(Bulkhead):一个服务挂了不能拖垮全部

3.1 隔离策略

舱壁隔离的灵感来自轮船的水密隔舱——即使一个舱室进水,其他舱室不受影响。在微服务中,就是为每个下游服务分配独立的资源池(线程池/连接池/信号量),防止一个慢服务耗尽所有资源。

两种隔离策略:

  • 信号量隔离(Semaphore):限制并发请求数,轻量级,适合高频调用
  • 线程池隔离(Thread Pool):为每个服务分配独立线程池,完全隔离,适合低频但关键的调用
// 舱壁隔离实现 - 基于信号量的并发控制
class Bulkhead {
  private running: number = 0;
  private queue: Array<{
    resolve: () => void;
    reject: (err: Error) => void;
  }> = [];

  constructor(
    private maxConcurrent: number,    // 最大并发数
    private maxQueue: number = 10,    // 最大排队数
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.running >= this.maxConcurrent) {
      if (this.queue.length >= this.maxQueue) {
        throw new Error(
          `Bulkhead full: ${this.running} running, ${this.queue.length} queued`
        );
      }
      // 排队等待
      await new Promise<void>((resolve, reject) => {
        this.queue.push({ resolve, reject });
      });
    }

    this.running++;
    try {
      return await fn();
    } finally {
      this.running--;
      // 释放一个排队的请求
      const next = this.queue.shift();
      next?.resolve();
    }
  }

  getStats() {
    return {
      running: this.running,
      queued: this.queue.length,
      maxConcurrent: this.maxConcurrent,
      maxQueue: this.maxQueue,
    };
  }
}

// 为不同下游服务分配独立的舱壁
const paymentBulkhead = new Bulkhead(10, 20);   // 支付服务:最多 10 并发
const inventoryBulkhead = new Bulkhead(20, 50);  // 库存服务:最多 20 并发
const notificationBulkhead = new Bulkhead(5, 10); // 通知服务:最多 5 并发

// 使用示例:下单时并行调用多个服务
async function createOrder(order: any) {
  const results = await Promise.allSettled([
    paymentBulkhead.execute(() => chargePayment(order)),
    inventoryBulkhead.execute(() => reserveInventory(order)),
    notificationBulkhead.execute(() => sendConfirmation(order)),
  ]);

  // 支付失败是致命错误,通知失败可以忽略
  const paymentResult = results[0];
  if (paymentResult.status === 'rejected') {
    throw new Error(`Payment failed: ${paymentResult.reason}`);
  }

  return { orderId: generateId(), results };
}

💡 **提示:**实际项目中推荐直接使用成熟库,如 Node.js 的 bottleneck(通用限流器)或 Java 的 Resilience4j(包含完整的熔断器+舱壁+重试+限流+限时)。手写实现适合理解原理,但生产环境要用经过验证的库。

🔗 四、组合使用:构建完整的容错链

真正的威力在于将这些模式组合起来。以下是生产中常用的组合策略:

// 容错链组合:熔断器 → 舱壁 → 重试 → 超时 → 降级
const circuitBreaker = new CircuitBreaker({
  failureThreshold: 50,
  timeout: 30000,
  minimumRequests: 10,
});

const bulkhead = new Bulkhead(15, 30);

async function resilientCall<T>(
  name: string,
  fn: () => Promise<T>,
  fallback: () => T,
): Promise<T> {
  try {
    // 第一层:熔断器保护
    return await circuitBreaker.execute(async () => {
      // 第二层:舱壁隔离
      return await bulkhead.execute(async () => {
        // 第三层:重试 + 超时
        return await withRetry(fn, {
          maxRetries: 2,
          baseDelay: 500,
          retryableErrors: ['502', '503', 'ETIMEDOUT'],
        });
      });
    });
  } catch (error) {
    console.error(`[${name}] All resilience measures exhausted:`, error);
    // 第四层:降级策略
    return fallback();
  }
}

// 使用示例:获取商品详情(带完整容错)
async function getProductDetail(productId: string) {
  return resilientCall(
    'product-service',
    () => fetch(`https://product.internal/api/${productId}`).then(r => r.json()),
    () => ({
      id: productId,
      name: '商品信息暂时不可用',
      price: 0,
      cached: true,
      degraded: true,
    }),
  );
}

容错模式选型决策表

场景 熔断器 重试 舱壁 降级
下游服务不稳定 ✅ 必须 ✅ 搭配使用 ✅ 推荐 ✅ 必须
网络抖动频繁 ❌ 不需要 ✅ 必须 ❌ 不需要 ⚠️ 可选
多下游服务调用 ✅ 每个独立 ✅ 每个独立 ✅ 必须 ✅ 每个独立
写操作(非幂等) ✅ 推荐 ❌ 配合幂等键 ✅ 推荐 ❌ 需人工处理
读操作(可缓存) ✅ 推荐 ✅ 推荐 ✅ 推荐 ✅ 返回缓存

💡 五、最佳实践与避坑指南

✅ 推荐做法

  • 每个下游服务独立配置:不同服务的超时、重试、熔断参数应该不同。支付服务可能需要更长超时,而配置中心可以快速失败
  • 监控一切:熔断器状态变化、重试次数、舱壁队列长度、降级触发次数——这些指标比业务指标更重要
  • 使用 AbortSignal.timeout():现代 fetch API 支持原生超时,比手动 Promise.race 更可靠
  • 降级要有意义:返回缓存数据、返回默认值、返回部分数据——总比 500 错误好
  • 在网关层统一配置:使用 Kong、APISIX 等 API 网关统一配置限流和熔断,避免每个服务重复实现

❌ 避免做法

  • 对写操作盲目重试:必须配合幂等键,否则可能产生重复数据
  • 所有服务用同一套参数:不同服务的 SLA 不同,容错策略也应该不同
  • 只做熔断不做降级:熔断后用户看到 503 错误?那还不如不熔断
  • 在代码里硬编码阈值:使用配置中心(如 Apollo、Nacos)动态调整阈值,无需重启
  • 忽略熔断恢复:半开状态的探测请求频率和成功阈值要合理设置,否则会导致「反复跳闸」

⚠️ **警告:**Resilience4j 等库默认不开启舱壁隔离。很多团队以为引入了库就万事大吉,实际上需要显式配置 BulkheadConfigTimeLimiterConfig。务必阅读文档确认默认值。

🎯 总结

分布式系统容错的核心原则只有一条:**假设一切都会失败,然后设计你的系统来优雅地处理失败。**五种模式各有侧重——熔断器防级联故障,重试处理瞬时错误,舱壁隔离限制爆炸半径,超时避免无限等待,降级保证用户体验。实际项目中,建议从「重试 + 超时 + 降级」三件套开始,随着系统复杂度增加再逐步引入熔断器和舱壁隔离。

推荐工具和库:

  • JavaResilience4j — 轻量级容错库,包含全部五种模式
  • TypeScript/Node.jsbottleneck — 通用限流器和重试器
  • Gosony/gobreaker — 简洁的熔断器实现
  • .NETPolly — .NET 生态最流行的容错库
  • 监控:Prometheus + Grafana — 暴露容错指标,设置熔断/降级告警

📚 相关文章