You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js中基于opossum实现单例Circuit Breaker失效问题排查

使用opossum实现单例断路器失效问题排查

我尝试用opossum库在Node.js中实现单例模式的Circuit Breaker,但遇到两个核心问题:断路器的统计指标始终从0开始累积,即便达到失败阈值,电路也不会触发开路。参考opossum的Issue#593后尝试用闭包实现单例,但方案无效,想明确问题原因及排查方向。

快速测试步骤:下载测试代码后,执行命令 npm install && node src/singleton.js 即可本地运行。


代码文件

singleton.js

let Adapter = require('./adapter');
let routerHandlerWrapper = require('./handler-wrapper');

const start = async function () {
  let i=0;
  for (i = 0; i < 100; i++) {
    const adapter = new Adapter(routerHandlerWrapper);
    await adapter.callPartner(i);
    console.log("attempt: " + i);
  }
}
start();

handler-wrapper.js

const { breaker } = require('./circuit-breaker');
module.exports = function (routerController, next) {
  console.log("Creating controller circuit");
  const breakerController = breaker(routerController);
  breakerController.on('open', () => {
    console.log('The circuit is open ' + JSON.stringify(breakerController.stats));
  });

  return async function (req, res, i) {
    try {
      // console.log(breakerController.stats);
      await breakerController.fire(req, res, i);
    } catch (err) {
      next(err);
    }
  };
};

circuit-breaker.js

const CircuitBreaker = require('opossum');

const circuitBreakerOptions = {
  enabled: true,
  timeout: 10000, // 如果函数执行超过10秒,触发失败
  errorThresholdPercentage: 50, // 当50%的请求失败时,触发断路
  volumeThreshold: 5,
  rollingCountTimeout: 300000,
  resetTimeout: 20000 // 20秒后尝试恢复电路
}

const options = {
  // errorFilter: err => ((!err || !err.code)
  //   ? false // 触发断路器开路
  //   : (err.code >= 300 && err.code < 500))
  ...circuitBreakerOptions
};

module.exports = {
  breaker: asyncFunc => new CircuitBreaker(asyncFunc, options)
};

adapter.js

class Adapter {
  constructor(routerHandlerWrapper) {
    this.routerHandlerWrapper = routerHandlerWrapper
  }
  async asyncFunctionThatCouldFail(x, y, i) {
    if (i > 40) {
      return await new Promise((resolve, reject) => setTimeout(() => resolve("positive"), 1000));
    }
    await new Promise((resolve, reject) => setTimeout(() => reject(new Error('timeout')), 1000));
    console.log("the args: " + x + " " + y + " " + i);
    throw "error";
  }

  async callPartner(i) {
    const x = "value1", y = "value2";
    let wrappedFunc = this.routerHandlerWrapper(this.asyncFunctionThatCouldFail, console.error);
    await wrappedFunc(x, y, i);
  }
}

module.exports = Adapter;

问题根源与排查方向

1. 核心问题:断路器实例被重复创建

你的代码存在两处导致实例无法复用的逻辑:

  • 循环中每次执行new Adapter(),创建全新的Adapter实例;
  • 每次调用Adapter.callPartner()时,都会执行this.routerHandlerWrapper(),而该函数内部会新建一个CircuitBreaker实例。

每个断路器实例的统计数据独立存储,导致永远无法累积到触发阈值的请求量,指标自然每次从0开始。

2. 单例实现未生效

你参考的Issue#593提到用闭包实现单例,但当前代码完全没有做实例缓存控制。handler-wrapper.js导出的函数每次被调用都会新建断路器,根本没有复用逻辑。


修复方案

方案1:在handler-wrapper中用闭包缓存实例

修改handler-wrapper.js,通过Map缓存已创建的断路器,确保同一个业务函数复用同一个实例:

const { breaker } = require('./circuit-breaker');
const breakerCache = new Map(); // 用闭包缓存断路器实例

module.exports = function (routerController, next) {
  // 以业务函数引用为key,确保同一函数复用同一断路器
  if (!breakerCache.has(routerController)) {
    console.log("Creating controller circuit");
    const breakerController = breaker(routerController);
    breakerController.on('open', () => {
      console.log('The circuit is open ' + JSON.stringify(breakerController.stats));
    });
    breakerCache.set(routerController, breakerController);
  }
  const breakerController = breakerCache.get(routerController);

  return async function (req, res, i) {
    try {
      await breakerController.fire(req, res, i);
    } catch (err) {
      next(err);
    }
  };
};

方案2:调整Adapter创建逻辑

不要在循环中重复创建Adapter,复用同一个实例减少不必要的资源消耗:
修改singleton.js:

let Adapter = require('./adapter');
let routerHandlerWrapper = require('./handler-wrapper');

const start = async function () {
  const adapter = new Adapter(routerHandlerWrapper); // 仅创建一次Adapter
  let i=0;
  for (i = 0; i < 100; i++) {
    await adapter.callPartner(i);
    console.log("attempt: " + i);
  }
}
start();

注:该方案需结合方案1的实例缓存才能彻底解决问题。

额外排查点

  • 错误识别逻辑:确认抛出的错误是否被断路器正确统计为失败(opossum默认会将Promise reject的情况标记为失败,可临时打开errorFilter注释测试);
  • 阈值配置验证:检查volumeThreshold:5是否符合预期,确保请求量达到阈值后才会触发错误率判断;
  • 实例唯一性验证:在handler-wrapper.js中打印breakerController的内存地址或ID,确认是否为同一个实例。

内容的提问来源于stack exchange,提问作者Pavithran Ravichandiran

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 02:50:22