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 能把熔断状态机写得很清楚,但真正难的是判断哪些调用可以降级,哪些调用必须失败。