熔断器:自动检测故障服务,快速失败保护系统
Created By RV, and licensed with Creative Commons "[CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/"
目的与场景
在分布式系统中,一个下游服务的故障可能引发上游连锁反应——调用方不断重试、线程阻塞、资源耗尽,最终拖垮整个集群。
在这一章,讲解如何使用 Kree4X 内置的熔断器(Circuit Breaker),在检测到服务持续故障时自动切断调用,快速失败以保护系统,并在服务恢复后自动恢复调用。
一. 概念
1. 什么是熔断器
熔断器是一个状态机,守护在调用方与被调用服务之间,监控每一次调用的成功与失败。
它有三个状态:
- Closed(关闭):正常放行所有调用。持续记录调用结果,当失败率或慢调用率达到阈值时,切换到 Open。
- Open(打开):拒绝所有调用,直接抛出
CircuitBreakerError(code=503)。经过设定的冷却时间后,自动切换到 Half-Open。 - Half-Open(半开):允许有限数量的探测调用通过。探测成功则回到 Closed,探测失败则重新回到 Open。
┌──────────┐ 失败率达阈值 ┌──────────┐ 冷却时间到期 ┌───────────┐
│ Closed │ ──────────────→ │ Open │ ──────────────→ │ Half-Open │
│ (放行) │ │ (拒绝) │ │ (探测) │
└──────────┘ ←────────────── └──────────┘ ←────────────── └───────────┘
探测失败 探测成功
2. 为什么需要熔断器
当服务持续不可用时,调用方不断发起请求只会:
- 浪费网络带宽和计算资源
- 增加下游恢复的负担
- 可能耗尽调用方的连接池或线程池,引发自身故障
熔断器的作用是快速失败——在检测到故障后立即拒绝调用,让调用方有时间处理降级逻辑,同时给下游服务恢复的空间。
3. 默认关闭,按需开启
Kree4X 的熔断器默认关闭——创建节点后不会自动拦截调用。
需要显式调用 enableCircuitBreaker(kreex) 开启:
import { create, enableCircuitBreaker } from '@kree4js/kree4n'
const node = create('my-node')
enableCircuitBreaker(node) // 开启熔断器
关闭熔断器(恢复直通):
node.circuitBreaker.disable()
4. 阈值:BreakerThresholds
熔断器根据滑动窗口内的调用统计做决策,阈值通过 BreakerThresholds 配置:
| 阈值 | 默认值 | 说明 |
|---|---|---|
errorRate |
0.5 | 错误率阈值:failCount / totalCount 达到此值触发熔断 |
minSamples |
10 | 最小样本数:调用次数不足时不触发熔断(避免样本过少误判) |
openDwellTime |
30000 | Open 状态持续时间(ms),到期后自动进入 Half-Open |
halfOpenSlots |
1 | Half-Open 状态允许的并发探测调用数 |
windowSize |
60 | 滑动窗口容量(按调用次数) |
windowTime |
undefined | 滑动窗口时间跨度(ms);undefined 表示仅按次数窗口 |
slowCallTime |
undefined | 慢调用判定阈值(ms);undefined 表示不检测慢调用 |
slowCallRate |
undefined | 慢调用率阈值;undefined 表示不检测慢调用 |
5. 慢调用检测(可选)
除了错误率,熔断器还可以检测慢调用:
slowCallTime:一次调用耗时超过此值(ms)即判定为慢调用slowCallRate:慢调用占比达到此阈值时触发熔断
两者均未设置时,慢调用检测关闭。两者同时设置时,任一条件满足即触发熔断(OR 语义)。
// 慢调用检测:耗时超过 200ms 算慢调用,慢调用占比达 50% 触发熔断
node.circuitBreaker.addOrUpdateRule(
new Rule(
{ nodeId: 'target-node', service: 'svc' },
{ minSamples: 5, slowCallTime: 200, slowCallRate: 0.5 }
)
)
6. 桶策略:决定分桶粒度
熔断器为每一个 (service, nodeId, method) 组合维护一个独立的breaker实例。桶策略(BarrelPolicy)决定统计的聚合粒度。
四种内置策略:
| 策略 | 分桶维度 | 适用场景 |
|---|---|---|
PerServicePolicy(默认) |
(service, nodeId) | 每个节点的每个服务独立统计 |
PerNodePolicy |
nodeId | 同一节点的所有服务共享一个 breaker |
ShareServicePolicy |
service | 同名服务跨所有节点共享一个 breaker |
ShareServiceMethodPolicy |
(service, method) | 同一服务的不同方法独立统计 |
全局设置桶策略:
const { ShareServiceMethodPolicy } = Kree4JS.CircuitBreaker
node.circuitBreaker.breakerBarrel(new ShareServiceMethodPolicy())
集群级设置(仅对该服务存根生效):
const svc = node.service('my-svc')
svc.breakerBarrel(new ShareServicePolicy())
7. 规则:Rule
规则(Rule)将路由键与阈值绑定。路由键决定规则作用的范围:
| 路由键 | 作用范围 |
|---|---|
{ nodeId, service } |
精确节点:仅对该节点的该服务生效 |
{ service } |
服务默认:对该服务的所有节点生效 |
{} |
全局默认:对所有服务生效 |
规则支持热更新——修改阈值后立即生效,无需重启。
8. 恢复机制
Open 状态的 breaker 不会永久拒绝调用。内部的 TimeWheel 在 openDwellTime 到期后自动将其翻转为 Half-Open,允许探测调用通过。
探测调用的成功或失败决定最终走向:
- 探测成功 → Closed(恢复正常)
- 探测失败 → Open(重新冷却)
二. 示例代码
在下边的示例中,我们模拟一个持续故障的服务:
- node-a:注册
svc服务,hello方法始终抛错(模拟宕机) - node-b:调用方,开启熔断器,自定义规则触发熔断
1. 搭建拓扑
import Kree4JS from '@kree4js/kree4js'
import { create, enableCircuitBreaker } from '@kree4js/kree4n'
const { Rule } = Kree4JS.CircuitBreaker
const nodeA = create('node-a', 'Flaky provider')
nodeA.register('svc', {
hello () { throw new Error('service unavailable') }
})
nodeA.listen('tcp://127.0.0.1:8070', { frameLimit: 512 })
const nodeB = create('node-b', 'CB caller')
nodeB.attach('tcp://127.0.0.1:8070', { frameLimit: 512 })
await nodeA.start()
await nodeB.start()
enableCircuitBreaker(nodeB)
2. 自定义规则:快速触发
默认阈值需要 10 次失败才触发,这里用低 minSamples 演示:
nodeB.circuitBreaker.addOrUpdateRule(
new Rule(
{ nodeId: 'node-a-id', service: 'svc' },
{ minSamples: 2, errorRate: 0.5, openDwellTime: 5000 }
)
)
3. 连续调用触发熔断
const svc = nodeB.service('svc').timeout(5000)
// 第1次调用失败:minSamples=2 未达,breaker 仍 closed
try { await svc.hello('a') } catch (_) {}
console.log(nodeB.circuitBreaker.getState({ nodeId: 'node-a-id', service: 'svc' }))
// → 'closed'
// 第2次调用失败:minSamples=2 达到,errorRate=1.0 >= 0.5,breaker → open
try { await svc.hello('b') } catch (_) {}
console.log(nodeB.circuitBreaker.getState({ nodeId: 'node-a-id', service: 'svc' }))
// → 'open'
4. 熔断中:调用被快速失败
try {
await svc.hello('c')
} catch (e) {
console.log(e.name) // → 'CircuitBreakerError'
console.log(e.code) // → 503
}
5. 状态观察
// 实时指标
const metrics = nodeB.circuitBreaker.getMetrics({ nodeId: 'node-a-id', service: 'svc' })
console.log(metrics.callCount) // 窗口内总调用数
console.log(metrics.errorRate) // 当前错误率
// 当前阈值
const thresholds = nodeB.circuitBreaker.getThresholds({ nodeId: 'node-a-id', service: 'svc' })
console.log(thresholds.errorRate) // 0.5
console.log(thresholds.minSamples) // 2
console.log(thresholds.openDwellTime) // 5000
6. 恢复演示
// 等待 5s(openDwellTime),breaker 自动进入 half-open
await new Promise(resolve => setTimeout(resolve, 5100))
console.log(nodeB.circuitBreaker.getState({ nodeId: 'node-a-id', service: 'svc' }))
// → 'half-open'
// Half-Open 探测调用:成功则 closed,失败则重新 open
try { await svc.hello('d') } catch (_) {}
console.log(nodeB.circuitBreaker.getState({ nodeId: 'node-a-id', service: 'svc' }))
// → 'open'(探测失败,重新冷却)
7. 强制操作
// 强制关闭:恢复放行
nodeB.circuitBreaker.forceClose({ nodeId: 'node-a-id', service: 'svc' })
// 强制打开:立即拒绝
nodeB.circuitBreaker.forceOpen({ nodeId: 'node-a-id', service: 'svc' })
// 强制重置:metrics 归零,状态回到 closed
nodeB.circuitBreaker.forceReset({ nodeId: 'node-a-id', service: 'svc' })
8. 清除规则
nodeB.circuitBreaker.clearRules()
// 所有 breaker 恢复默认阈值,状态回到 closed
三. 须强调的细节
1. 默认关闭,enableCircuitBreaker 才生效
create() 创建的节点,熔断器默认关闭。
不调用 enableCircuitBreaker(kreex) 或 kreex.circuitBreaker.enable(),所有调用直通,不经过熔断器拦截。
enableCircuitBreaker 是幂等的,多次调用安全。
2. Breaker 懒创建
Breaker 不是在 addOrUpdateRule 时创建的,而是在首次调用时按桶策略分配。
未调用过的服务不会有 breaker 实例,getState 返回 undefined。
3. minSamples 未达到前不触发
滑动窗口内的调用次数不足 minSamples 时,即使错误率 100% 也不会触发熔断。
这避免了样本过少导致的误判——例如首次调用失败就熔断是不合理的。
4. HALF-OPEN 的 probe slot 机制
Half-Open 状态下,允许 halfOpenSlots 个并发探测调用通过(默认 1 个)。
探测调用成功 → Closed;失败 → 重新 Open。
探测调用数量有上限,避免恢复期间大量流量涌入。
5. 四种桶策略差异
桶策略决定了 breaker 的分桶粒度,直接影响故障隔离的范围:
- PerServicePolicy(默认):每个 (service, nodeId) 独立。node-a 的 svc 故障不影响 node-b 的 svc。
- PerNodePolicy:每个 nodeId 共享。node-a 的任一服务故障会影响该节点的所有服务。
- ShareServicePolicy:同名服务跨节点共享。任一节点故障会熔断所有节点的同名服务。
- ShareServiceMethodPolicy:每个 (service, method) 独立。同一服务的 hello 故障不影响 bye。
6. 节点下线自动清理
当节点从 grid 中移除时,该节点的 breaker 实例和关联的 rule 会被自动清理,不会残留。
四. 涉及到的API
1. 开启/关闭熔断器
/**
* 开启熔断器(幂等)。
*
* @param {KreeX} kreex - KreeX 实例。
* @returns {KreeX} 同一实例,支持链式。
*/
enableCircuitBreaker(kreex): KreeX
// 等价于
kreex.circuitBreaker.enable()
/**
* 关闭熔断器:拦截器卸载,已有 breaker 清除。
*/
kreex.circuitBreaker.disable()
2. 规则管理
/**
* 添加或更新规则(热生效)。
*/
kreex.circuitBreaker.addOrUpdateRule(...rules: Rule[]): void
/**
* 移除规则。
*/
kreex.circuitBreaker.removeRule(...rules: Rule[]): void
/**
* 清除所有规则,breaker 恢复默认阈值,状态回到 closed。
*/
kreex.circuitBreaker.clearRules(): void
3. 强制操作
/**
* 强制打开指定范围的 breaker。
* scope 至少包含一个维度(nodeId/service/method/nodeName)。
*/
kreex.circuitBreaker.forceOpen(scope): void
/**
* 强制关闭:恢复放行。
*/
kreex.circuitBreaker.forceClose(scope): void
/**
* 强制重置:metrics 归零,状态回到 closed。
*/
kreex.circuitBreaker.forceReset(scope): void
4. 状态观察
/**
* 查询 breaker 状态。breaker 未创建时返回 undefined。
*/
kreex.circuitBreaker.getState(key): CircuitState | undefined
/**
* 查询实时指标:{ callCount, errorCount, errorRate, slowCallCount, slowCallRate }
*/
kreex.circuitBreaker.getMetrics(key): Metrics | undefined
/**
* 查询当前阈值。
*/
kreex.circuitBreaker.getThresholds(key): BreakerThresholds | undefined
5. 桶策略
/**
* 全局设置桶策略。
*/
kreex.circuitBreaker.breakerBarrel(policy?: BarrelPolicy): BarrelPolicy
// 存根级设置
serviceStub.breakerBarrel(policy): this
6. Rule 构造
import Kree4JS from '@kree4js/kree4js'
const { Rule } = Kree4JS.CircuitBreaker
/**
* 构造规则:路由键 + 阈值。
*
* @param {CircuitBreakerKeyLike} key - 路由键:{ nodeId?, service?, method? }
* @param {Partial<BreakerThresholds>} thresholds - 阈值(未设置的使用默认值)。
*/
new Rule(key, thresholds): Rule
7. Manager 事件
const { CircuitBreakerManagerEvent } = Kree4JS.CircuitBreaker
kreex.circuitBreaker.manager.on(CircuitBreakerManagerEvent.Opened, (payload) => {
// payload: { nodeId, service, method, reason }
})
kreex.circuitBreaker.manager.on(CircuitBreakerManagerEvent.Closed, (payload) => { ... })
kreex.circuitBreaker.manager.on(CircuitBreakerManagerEvent.HalfOpened, (payload) => { ... })
kreex.circuitBreaker.manager.on(CircuitBreakerManagerEvent.StateChanged, (payload) => {
// payload: { state, from, reason }
})
五. 可运行代码
完整示例代码,参见:14-circuit-breaker.mjs