熔断器模式借鉴了电路保护中的断路器思路,在分布式系统中用来隔离不可用的外部依赖。当某个网络服务连续失败达到阈值,熔断器会切换到打开状态,直接拒绝后续请求并立即抛出降级异常,避免调用方被拖垮。打开状态不会永远持续,经过冷却时间后熔断器会转入半开状态,此时只放行少量请求作为探针,观察依赖是否真正恢复。如果这些试探请求的成功次数达到设定阈值,熔断器重新关闭,流量恢复正常;只要失败比例仍然偏高,熔断器就会再次打开。这个半开阶段的设计直接决定了系统在依赖恢复期的表现,本文将围绕 Ruby 中的实现方式,拆解半开试探的状态转换、配置技巧和常见调优错误。

半开状态如何参与熔断器状态机
熔断器的完整生命周期由关闭、打开、半开三个状态组成。关闭状态下请求正常通过,同时记录失败次数或错误率;连续失败达到阈值后进入打开状态。打开状态下请求被快速失败,通常不执行真实调用,以给依赖留出恢复时间。当冷却时间结束,熔断器不直接切换到关闭,而是进入半开状态,这是为了避免依赖恢复不彻底时全量流量再次造成冲击。半开状态的核心任务是试探,它只允许有限数量的请求访问真实依赖,并根据这些样本的成败决定后续走向。
半开状态的转换通常依赖两个参数:试探请求数和成功阈值。例如允许最多 5 个试探请求,至少 3 个成功则认为依赖已恢复。这种设计可以在恢复速度与风险控制之间取得平衡。如果试探请求全部成功,熔断器立即关闭;如果出现一个失败,某些实现会立即回到打开状态,另一些实现会继续放行剩余试探请求,最后根据成功率判断。前者更保守,后者能容忍偶发抖动。两者的选择取决于业务对依赖稳定性的敏感程度。
下面的 Ruby 代码展示了一个简化状态机的核心逻辑,重点关注半开状态下的计数与转换条件。
class HalfOpenBreaker
attr_accessor :state, :failure_count
def initialize(allowed_probes: 5, success_threshold: 3, open_seconds: 10)
@allowed_probes = allowed_probes
@success_threshold = success_threshold
@open_seconds = open_seconds
@state = :closed
@failure_count = 0
@opened_at = nil
@probe_count = 0
@probe_successes = 0
end
def call
if @state == :open
if Time.now - @opened_at >= @open_seconds
enter_half_open
else
raise "circuit open"
end
end
if @state == :half_open && @probe_count >= @allowed_probes
if @probe_successes >= @success_threshold
reset_to_closed
else
reopen_circuit
raise "circuit open"
end
end
@probe_count += 1 if @state == :half_open
begin
result = yield
record_success
result
rescue StandardError => e
record_failure
raise e
end
end
private
def enter_half_open
@state = :half_open
@probe_count = 0
@probe_successes = 0
end
def record_success
@probe_successes += 1 if @state == :half_open
reset_to_closed if @state == :closed
end
def record_failure
if @state == :half_open
reopen_circuit if @probe_count >= @allowed_probes
else
@failure_count += 1
if @failure_count >= 3
@state = :open
@opened_at = Time.now
end
end
end
def reset_to_closed
@state = :closed
@failure_count = 0
@probe_count = 0
@probe_successes = 0
end
def reopen_circuit
@state = :open
@opened_at = Time.now
@probe_count = 0
@probe_successes = 0
end
end
Ruby 项目中配置半开试探的实践方式
Ruby 生态中常用的熔断器库包括 Circuitbox、Semian 和 Stoplight。Circuitbox 提供了一套基于时间窗口和错误阈值的熔断器,虽然它的核心配置更偏重于关闭到打开的判定,但底层同样包含半开探测机制。Semian 针对 MySQL、Redis、Net::HTTP 等资源做了更细粒度的适配,允许通过 success_threshold、error_threshold、error_timeout 等参数控制故障判定,半开阶段会按比例放行请求。Stoplight 则把熔断器封装为红绿灯模型,冷却时间结束后自动进入半开状态,允许一个请求探测,成功则恢复为绿灯。
使用这些库时,半开状态通常不需要单独开发状态机,但配置是否合理直接影响恢复表现。例如 Circuitbox 的 sleep_window 控制打开状态持续多久,volume_threshold 控制窗口内最少请求数,error_threshold 设置错误率阈值。当冷却窗口过后,下一次请求会被当作半开探针,如果成功则关闭熔断器。这里一个常见误区是只配置了错误阈值,却忽略 sleep_window 过短导致半开探针频繁触发,依赖还没有充分恢复就再次被流量打穿。
如果业务需要更精细的半开控制,可以基于上面自定义的 HalfOpenBreaker 类包裹外部调用。下面示例展示如何将半开试探应用于一个网络服务客户端:
breaker = HalfOpenBreaker.new(allowed_probes: 4, success_threshold: 2, open_seconds: 15)
def fetch_remote_data(client, breaker)
breaker.call do
client.get("/api/v1/orders")
end
rescue => e
{ error: "degraded", reason: e.message }
end
这段代码把真实 HTTP 请求放进 breaker.call 的块中,块成功返回时调用 record_success,抛出异常时调用 record_failure。半开阶段最多允许 4 个请求,至少有 2 个成功后恢复关闭。相比直接全量放行,这种方式把风险控制在很小范围内。需要注意的是,块内捕获异常后如果返回降级对象,熔断器仍然会认为调用成功,因此必须让异常正确抛出到熔断器层,或者在块内显式向熔断器报告失败。
半开试探调优与常见问题
半开试探的配置没有通用最优值,需要根据依赖类型和业务特征调整。试探请求数量过少会导致恢复时间变长,比如只允许 1 个请求,如果这个请求因为偶发网络抖动失败,熔断器会重新打开并再等一轮冷却时间,恢复被大幅延迟。试探请求数量过多,则可能在依赖刚恢复但吞吐量仍然很低时形成二次压力,把下游再次打垮。通常只读接口可以适当增加试探数量,写接口则尽量保守。
成功率阈值同样关键。若阈值设为 100%,任何一次试探失败都会让熔断器重新打开,对网络闪断过于敏感;若阈值过低,则可能在依赖仍不稳定时放大流量。比较稳妥的做法是区分错误类型:连接超时、连接拒绝等网络层错误应计入失败;而业务层 4xx 错误通常不应触发熔断。Ruby 中可以在 rescue 分支里过滤 Errno::ECONNREFUSED、Net::ReadTimeout 等异常,避免把参数错误误判为依赖不可用。
另一个常被忽略的问题是试探请求的超时设置。半开阶段由于只放行少量请求,应该使用更短的超时时间,以便快速获得探测结果。如果沿用正常请求的较长超时,一个半开探针可能占用线程资源几十秒,导致后续探针无法及时发出,甚至影响其他业务。下列表格总结了不同场景下的参考配置。
| 依赖类型 | 试探请求数 | 成功阈值 | 冷却时间建议 |
|---|---|---|---|
| 只读缓存或搜索服务 | 3 到 5 | 2 到 3 | 5 到 10 秒 |
| 核心交易 API | 1 到 2 | 1 | 15 到 30 秒 |
| 消息队列生产端 | 2 到 3 | 2 | 10 到 20 秒 |
最后还要关注监控。半开状态的出现频率、试探成功率和冷却次数能够反映依赖的恢复质量。如果某个依赖频繁从半开回到打开,说明冷却时间不足或错误阈值偏低,可以结合日志和指标调整参数。Ruby 中可以在状态变更时打印结构化日志,记录 state、probe_count 和 probe_successes,为后续调优提供数据支持。