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
相关产品推荐
相关产品推荐

