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 总结

断路器模式的核心就三句话:关闭时统计失败,打开时快速失败,半开时试探恢复。配置好熔断阈值和恢复时间,你的流水线就不会被一个故障拖死。

嗯,这里要注意:断路器不是银弹。它解决的是「防止故障扩散」的问题,但不能替代重试、超时、降级等其他容错手段。把这些组合起来用,效果才最好。