4. 断路器模式:状态机与熔断恢复
断路器模式,说白了就是给流水线装一个「保险丝」。我在早期做 CI/CD 平台时,遇到过上游服务挂了,结果整个部署流水线被拖死的情况。后来我意识到——与其让故障扩散,不如主动切断。
断路器的核心是一个三态状态机:关闭 → 打开 → 半开。这三个状态决定了流水线在面对故障时的行为。
4.1 状态机详解
先看一张状态转换图,你就能直观理解它的工作逻辑。
4.2 关闭状态:正常运转
关闭状态是断路器的默认状态。此时流水线正常调用下游服务或组件。每次调用成功,计数器清零;每次调用失败,计数器加一。
我个人的习惯是:不要只统计次数。举个例子,如果某服务每秒调用 1000 次,失败 5 次其实不算什么。但如果每秒只调用 10 次,失败 5 次就很严重了。
熔断阈值配置建议
不要用固定次数,用「滑动窗口内的失败率」。比如:最近 60 秒内,失败率超过 50% 则触发熔断。
4.3 打开状态:快速失败
当失败次数或失败率达到阈值,断路器切换到打开状态。此时所有请求被直接拒绝,不执行实际调用。这能防止流水线被卡住,也避免给下游系统增加压力。
我曾经遇到一个场景:数据库连接池耗尽,结果所有微服务都在重试,反而把数据库打得更死了。断路器打开后,请求立刻返回「服务不可用」,流水线可以走降级逻辑,而不是死等。
⚠️ 注意:打开状态不是永久性的
断路器打开后,会启动一个计时器。计时器到期后自动进入半开状态。这个时间就是「恢复时间」。
4.4 半开状态:试探恢复
半开状态是断路器的「试探期」。此时断路器会放行少量请求,看看下游是否恢复正常。
- 试探成功:请求正常返回,断路器关闭,恢复正常调用。
- 试探失败:请求再次失败,断路器重新打开,计时器重置。
这里有个关键点:试探请求的数量。放行太多,可能又把下游打垮;放行太少,恢复速度太慢。我一般设置为总请求量的 5%~10%,或者固定放行 3~5 个请求。
4.5 熔断阈值与恢复时间
这两个参数是断路器配置的核心。我整理了一个表格,方便你对比不同场景的推荐值。
| 参数 | 说明 | 推荐值 | 我的经验 |
|---|---|---|---|
| 失败阈值 | 触发熔断的失败次数或失败率 | 滑动窗口内失败率 ≥ 50% | 不要低于 30%,否则误触发太多 |
| 请求阈值 | 滑动窗口的最小请求量,低于此值不触发熔断 | 窗口内至少 10 次请求 | 防止冷启动时误判 |
| 恢复时间 | 断路器从打开到半开的等待时间 | 30~60 秒 | 根据下游恢复速度调整,太短容易反复熔断 |
| 半开试探数 | 半开状态下允许放行的请求数 | 3~5 个 | 试探成功后再逐渐增加流量 |
4.6 代码示例:断路器实现
下面是一个简化版的断路器实现,用 Python 演示核心逻辑。你可以在流水线中嵌入类似的组件。
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = 1
OPEN = 2
HALF_OPEN = 3
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_time=30, half_open_max=3):
self.state = CircuitState.CLOSED
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_time = recovery_time
self.half_open_max = half_open_max
self.half_open_count = 0
self.last_failure_time = None
def call(self, func):
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_time:
self.state = CircuitState.HALF_OPEN
self.half_open_count = 0
else:
raise Exception("断路器打开,请求被拒绝")
try:
result = func()
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
def _on_success(self):
if self.state == CircuitState.HALF_OPEN:
self.half_open_count += 1
if self.half_open_count >= self.half_open_max:
self.state = CircuitState.CLOSED
self.failure_count = 0
elif self.state == CircuitState.CLOSED:
self.failure_count = 0
def _on_failure(self):
self.last_failure_time = time.time()
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.OPEN
elif self.state == CircuitState.CLOSED:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
4.7 避坑指南
我在项目中踩过不少坑,这里列几个最常见的:
- 恢复时间设置太短:我曾经设了 5 秒,结果下游还没恢复,断路器反复打开关闭,流水线一直在抖动。建议至少 30 秒起步。
- 忽略滑动窗口:用次数做阈值,高峰期容易误触发。用滑动窗口内的失败率更准确。
- 半开试探不设上限:半开状态下放行太多请求,可能又把下游打垮。一定要限制试探数量。
- 没有记录断路器事件:断路器切换状态时,一定要打日志或发告警。否则你都不知道流水线为什么突然变慢了。
💡 我的小技巧
断路器状态变化时,除了日志,还可以暴露一个 Prometheus 指标。这样在 Grafana 上就能看到熔断次数和持续时间,排查问题快很多。
4.8 总结
断路器模式的核心就三句话:关闭时统计失败,打开时快速失败,半开时试探恢复。配置好熔断阈值和恢复时间,你的流水线就不会被一个故障拖死。
嗯,这里要注意:断路器不是银弹。它解决的是「防止故障扩散」的问题,但不能替代重试、超时、降级等其他容错手段。把这些组合起来用,效果才最好。