返回文章列表

Rust 服务调用下游,什么时候该熔断,什么时候该硬等

214·2 分钟阅读
Rust微服务

下游已经开始超时,调用方还在一遍遍重试。几秒后,连接池满了,线程忙着等待,入口请求也开始排队。

熔断听起来像微服务标配,但很多系统要么不用,要么乱用。

不用的问题是:下游故障会把调用方一起拖死。

乱用的问题是:下游只是抖了一下,调用方立刻大量失败。

熔断不是为了惩罚下游,而是为了保护调用方的资源。


超时、重试、熔断是一组策略

单独讨论熔断没有意义。

调用下游时,至少有三层保护:

策略 解决什么
超时 单次调用不能无限等
重试 短暂故障可以恢复
熔断 连续故障时停止继续消耗资源

如果没有超时,熔断统计会滞后;如果没有重试边界,熔断前就可能把下游打爆;如果没有熔断,下游持续故障会拖垮调用方。

可以先定义调用结果:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CallOutcome {
    Success,
    Timeout,
    TransportError,
    Rejected,
}

不是所有错误都应该计入熔断。比如 400 参数错误是调用方问题,不代表下游不可用。


熔断器有三个状态

经典熔断器状态不复杂:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BreakerState {
    Closed,
    Open,
    HalfOpen,
}

含义也直接:

  • Closed:正常放行
  • Open:快速失败,不再调用下游
  • HalfOpen:放少量探测请求,看下游是否恢复

状态迁移可以这样表达:

fn on_success(state: BreakerState) -> BreakerState {
    match state {
        BreakerState::HalfOpen => BreakerState::Closed,
        other => other,
    }
}

失败时要看错误率、连续失败次数或滑动窗口。文章里不展开算法,重点是边界:熔断器管理的是调用许可,不是业务状态。


熔断阈值不要只看失败次数

连续 5 次失败就熔断,看起来简单,但低流量服务会被误伤。

更合理的是同时看请求量和错误比例:

#[derive(Debug)]
struct BreakerWindow {
    total: u64,
    failed: u64,
}
 
fn should_open(window: &BreakerWindow) -> bool {
    window.total >= 20 && window.failed * 100 / window.total >= 50
}

这段逻辑表达两个条件:

  • 样本量足够
  • 失败比例足够高

如果只有 2 个请求失败就熔断,可能只是偶发抖动。如果 100 个请求失败 60 个,还继续硬等,就是调用方在自损。

我的偏好是:熔断阈值要同时考虑样本量和错误率。


HalfOpen 不能一次放开

熔断打开一段时间后,服务需要探测下游是否恢复。

危险做法是时间一到就全部放开。下游刚恢复,可能马上被积压流量再次打挂。

HalfOpen 应该少量放行:

#[derive(Debug)]
struct ProbeBudget {
    max_probe: u64,
    used: u64,
}
 
fn can_probe(budget: &ProbeBudget) -> bool {
    budget.used < budget.max_probe
}

探测成功,再逐步恢复。探测失败,继续打开。

这和限流、背压是一套思路:恢复也要有节奏,不要把“重新尝试”变成第二次冲击。


熔断后要有降级策略

熔断只是停止调用,不代表用户体验已经处理好。

不同接口的降级方式不同:

接口 降级策略
推荐列表 返回缓存或空列表
用户头像 返回默认头像
库存校验 失败,不允许继续
支付扣款 失败或进入待确认
风控检查 看业务风险,通常不能静默跳过

可以把降级结果明确建模:

#[derive(Debug, PartialEq, Eq)]
enum Fallback<T> {
    Value(T),
    FailFast,
}
 
fn recommend_fallback() -> Fallback<Vec<u64>> {
    Fallback::Value(Vec::new())
}

能不能降级,不是技术决定,是业务风险决定。

不要为了可用性跳过支付、权限、风控这类关键校验。


熔断指标必须按下游拆开

全局错误率对熔断帮助不大。

你需要按下游、接口、错误类型看:

  • user-api 超时率
  • payment-api 连接错误
  • risk-api 熔断打开次数
  • HalfOpen 探测成功率
  • 快速失败次数

用快照表达:

#[derive(Debug)]
struct BreakerSnapshot {
    downstream: &'static str,
    state: BreakerState,
    rejected: u64,
}
 
fn is_open(snapshot: &BreakerSnapshot) -> bool {
    snapshot.state == BreakerState::Open
}

熔断打开时,日志和告警必须说清楚是哪一个下游,不然排查会变成猜。


结论

熔断不是微服务装饰品,它是调用方保护自己资源的手段。下游持续故障时,继续等待和重试只会把故障扩散。

超时限制单次等待,重试处理短暂抖动,熔断处理持续故障;熔断阈值要看样本量和错误率;恢复要走 HalfOpen 探测;降级策略必须按业务风险决定。

Rust 能把熔断状态机写得很清楚,但真正难的是判断哪些调用可以降级,哪些调用必须失败。