Theory · Microservice · Resilience
依赖不可靠是分布式系统的常态 —— 超时 · 重试 · 熔断 · 降级 · 隔离,五件套把故障关在笼子里
故障不可避免,目标不是"不失败",而是失败得可控:快速失败、影响面收敛、可自动恢复
级联失败(雪崩):慢依赖拖垮调用方线程/连接 → 调用方也变慢 → 故障沿调用链逐级放大
熔断器:sony/gobreaker(Go);策略引擎:sentinel-golang;框架内置:go-zero/Kratos——思想同源 Hystrix
Cascading Failure
Overview
单跳止损:不无限等待。必须逐跳传播(deadline),且任何一跳的超时要小于上游剩余预算
把偶发失败变成功:退避+抖动、只重试可重试错误、预算限制防放大、幂等前提
停止无效尝试:失败率超阈值→快速失败→间歇探测恢复。保护调用方自己,也给依赖喘息时间
有损服务:返回缓存/默认值/简化逻辑。与熔断联动——熔断打开时执行什么
限制爆炸半径:资源分组(舱壁模式),一个依赖拖垮只影响自己的资源池
调用库存:200ms 超时 → 超时/连接拒绝重试 1 次(预算 10%)→ 失败率 50% 熔断 → 熔断期间走本地预扣 + 异步补偿 → 库存/支付/风控各自独立连接池隔离
依赖分级决定投入:强依赖(主链路,挂了业务不可用)→ 快速失败+兜底预案;弱依赖(旁路,如积分/通知)→ 降级开关常备,出问题直接关闭。面试先讲分级,再讲手段——"所有依赖一视同仁"是减分项。
Timeout
// 入口 SLO 500ms 的预算切分 入口网关 500ms 总预算 ├─ auth 服务 50ms (不可超) ├─ order 服务 400ms(三个兄弟项之和≤400) │ ├─ 库存 RPC 150ms ×1重试 │ ├─ 商品 RPC 100ms(可降级) │ └─ DB 查询 100ms └─ 余量(序列化/调度) 50ms
关键点:重试预算要单独留(200ms×2 重试 > 上游剩余就别试了)——gRPC 的重试策略自动检查剩余 deadline。
拨号超时 < 请求超时 < 整体 deadline;连接池等待超时单独设——缺失它是"连接池耗尽拖垮服务"的直接原因。
超时 ≠ 结束:下游可能已经执行成功(响应没回来)。所以超时后的重试必须配幂等(见幂等 deck);占用的 goroutine 要及时退出(context cancel 传导到 IO 操作)。
Retry
delay = base * 2^attempt // 指数退避 delay += rand(0, delay*0.5) // 抖动 // 例: base=100ms // 1次:100-150ms 2次:200-300ms // 3次:400-600ms (设上限)
没有抖动的退避 = 所有客户端同一时刻重试(重试同步化,二次踩踏)。AWS 官方文档明确要求 jitter——这是被生产事故验证过的细节。
限制"重试流量占总请求比例"(10-20%):成功率暴跌时预算耗尽、重试自动停止——把重试从"放大器"变成"无成本保险"。Finagle/gRPC retryThrottling/Envoy 内置:maxTokens 1000, tokenRatio 0.1。
A→B→C 每跳重试 3 次:C 收到的最大请求量 = 3(A重试) × 3(B重试) = 9 倍。多层叠加时重试是乘法不是加法——所以每跳重试次数要递减(入口 2 次,中间层 1 次,靠近 DB 不重试),且预算兜底。
Circuit Breaker
Fallback / Degradation
熔断解决"要不要继续打",降级解决"不打之后用户看到什么"。实现上两者常在一个组件里:gobreaker 的 Fallback / sentinel 的 blockHandler。没有降级逻辑的熔断 = 只是把报错变得更快,用户体验仍然中断。
恢复顺序与降级顺序相反(先恢复数据一致性要求高的);降级期间要打显式指标(fallback_total)与日志,让"有损服务"可观测可统计——SRE 视角:降级率也是 SLO 的一部分。
Bulkhead
不同依赖使用独立资源池:A 依赖的连接池/信号量打满,不影响 B 依赖——把"一个慢依赖拖垮整个服务"限制为"只影响走 A 池的请求"。
| 隔离维度 | Go 实现 |
|---|---|
| 连接池隔离 | 每依赖独立 HTTP/gRPC 连接池、独立 DB 连接池 |
| 并发隔离 | semaphore 限制单依赖并发(golang.org/x/sync/semaphore) |
| 实例隔离 | 核心服务与普通服务物理分离部署 |
| 请求分类 | 核心/非核心接口标记,非核心先限 |
Hystrix 线程池隔离(强隔离,多一跳线程切换)vs 信号量隔离(轻量,同一进程)。Go 无线程池概念,goroutine 天然轻量——但goroutine 泄漏会等效放大:每阻塞一个 goroutine 就耗一份栈内存。Go 的等价物是:semaphore 并发上限 + context 超时强制退出 + 连接池上限(pool_max)。
并发上限 = 目标吞吐 × 依赖 RT(Little's Law)。例:某依赖 P99 200ms、期望它最多承担 500 QPS → 上限 100 并发;超过的部分快速失败转降级。上限是保护自己,不是限制吞吐——先压测定容量再设上限。
Bulkhead · Boundary
限流拒绝"多余的外部流量"(保护服务);隔离约束"自己对外占用的资源"(保护调用方的其他依赖调用)。方向相反、常常配合:进来的限流、出去的隔离。
更粗粒度:核心链路服务与后台任务(导表/报表)分集群部署——一个批处理任务打爆网卡不至于影响在线交易。K8s 节点池 + taint/toleration 实现。
舱壁速查:进来的限流、出去的隔离;并发上限 = 吞吐 × RT(Little's Law)——先压测定容量,再设上限;Pod 级物理隔离是比连接池更粗的防线。
Implementations
| 框架 | 语言/定位 | 特点 |
|---|---|---|
| Hystrix | Java(Netflix,已停更) | 熔断隔离语义开山之作;线程池隔离;已停更,思想永存 |
| Resilience4j | Java(函数式轻量) | Hystrix 推荐继任;函数式组合器,各组件可叠加 |
| Sentinel | Java + Go 多语言(阿里) | 流控/熔断/热点/系统自适应;控制台规则下发;sentinel-golang GA |
cb := gobreaker.NewCircuitBreaker(
gobreaker.Settings{
Name: "pay",
MaxRequests: 5, // half-open 探测数
Interval: 10*time.Second, // 计数窗口
Timeout: 30*time.Second, // open 冷却
ReadyToTrip: func(c gobreaker.Counts) bool { // 失败率>50% 且样本≥20
return c.Requests >= 20 && c.TotalFailures*2 > c.Requests
},
})
v, err := cb.Execute(func() (any, error) { return callPay(ctx, req) })
go-zero 内置熔断(Google SRE 自适应算法,按成功率动态调拒绝概率);Kratos 生态接 sentinel-golang/gobreaker。趋势:熔断器按调用方部署,网络位置越近越好。
只要熔断 → gobreaker(库级零部署);要控制台+热点/自适应 → sentinel-golang;框架统一治理 → go-zero/Kratos 内置;Mesh → 治理下沉 Sidecar(贴近调用方)。
Interview QA · 1/2
常用滑动窗口(最近 10s)或计数窗口(最近 N 个)。关键参数:失败率阈值(40-60%)与最小样本量(≥20)——低流量 2 个失败就是 100%,无样本下限会误熔断。Sentinel 有慢调用比例/异常比例/异常数三策略。
依赖刚恢复时承载能力未知,全量涌入可能再次压垮——half-open 只放少量探测(如 5 个),全部成功才逐步恢复。"恢复比熔断更谨慎":熔断保护自己是硬需求,恢复可以慢。
限流站在服务端视角:拒绝多余的流量,保护自己不被打垮(主动防御);熔断站在调用方视角:发现某个依赖持续失败后主动停止调用,保护自己的资源并给依赖喘息(被动止损)。一个系统两者都要:进来的限流、出去的熔断。
顺序:熔断判断在重试之外层(熔断打开时连第一次请求都不发,更不重试)。计数:重试的成功/失败要计入熔断统计,否则统计失真;重试预算与熔断失败率共享窗口。实现上 gRPC service config 的 retryPolicy + 熔断拦截器组合时,注意别把"重试 3 次都失败"算成 3 次独立错误放大失败率。
go-zero 采用 Google SRE 的过载保护算法:不用固定失败率阈值,而是根据请求量与成功率计算丢弃概率——丢弃概率 = max(0, (requests - K×accepts) / (requests + 1))。请求量越高越接近 K 倍接受数时,拒绝概率越平滑上升,避免固定阈值在临界点抖动。
两条去向:① 降级逻辑承接(缓存/默认值/简化流程)——这是设计目标;② 没有降级逻辑时快速失败(错误立刻返回)——至少不再消耗调用方资源。所以"熔断必须配降级"是设计规范;只熔断不降级等于把慢错误换成快错误。
Interview QA · 2/2
先分级:库存/支付=强依赖(快速失败+预案),优惠券/积分/通知=弱依赖(降级开关常备)。再上五件套:全链路 deadline 500ms 传播;库存超时 200ms 重试 1 次(预算 10%);支付失败率 50% 熔断+冷却 30s;熔断期走本地预扣+异步对账补偿;支付/库存/风控独立连接池隔离。最后接观测:熔断/降级指标+告警。
① 语义性失败:参数校验失败、余额不足、库存不够——重试结果必然相同;② 权限类:401/403;③ 非幂等写且状态未知:超时后的下单请求可能已成功,直接重试=重复下单,必须先查单或带幂等键;④ 上游 deadline 将耗尽:剩余预算不够一次完整尝试就不重试。
理论值:并发 = 目标吞吐 × 平均 RT。例:依赖 RT 200ms、给它 500 QPS → 上限 100。实际用压测校准:找到该依赖的拐点(延迟开始上翘的并发数),上限设为拐点的 70-80%——留出缓冲。上线后按 P99 调整,并配告警观察"快速失败率"是否常触发。
方法:假设(注入支付延迟 2s,订单服务应熔断+降级,不影响商品浏览)→ 注入(ChaosBlade/Chaos Mesh 注网络延迟/实例杀/依赖故障)→ 验证(指标:熔断打开时间、降级率、核心接口 SLO)→ 复盘改进。原则:从小爆炸半径开始、生产环境要有时段与一键终止开关。
异常熔断看错误率——抓"明确挂掉";慢调用熔断看 RT——抓"活着但在溺水"(如 RT 阈值 1s,慢调用比例超 50% 触发)。后者对"拖垮调用方资源"更有效,因为慢请求不报错误、只占资源。生产两者都要:Sentinel 三策略(慢调用比例/异常比例/异常数)覆盖全谱。
三层验证:① 单测:mock 依赖持续失败,断言熔断状态机迁移与降级返回;② 指标:熔断器状态(open/close 计数)、降级率、快速失败率看板;③ 演练:预发或生产注入依赖故障,验证自动恢复时间符合预期(MTTR)。没有演练过的容错代码,故障时大概率是新的故障点。
Related & References