一句话总结
分布式系统中,故障不是"如果"而是"何时".
弹性治理通过超时,重试,熔断,限流,隔离舱和降级六把武器,让系统在部分组件失败时仍能提供有损但可用的服务--而非全面崩溃.
为什么需要弹性治理
单体应用中,一个模块调另一个模块是进程内函数调用--要么成功,要么 panic.微服务之间隔着网络,多了一整类新的失败模式:
| 失败模式 | 表现 | 后果 |
|---|---|---|
| 网络超时 | 请求发出去,响应迟迟不来 | goroutine/线程挂起,资源耗尽 |
| 服务过载 | 下游 QPS 超出承载能力 | 响应变慢 → 上游超时 → 重试 → 雪崩 |
| 网络分区 | 部分实例不可达 | 请求持续打到不可达实例 |
| 级联故障 | A 依赖 B,B 依赖 C,C 挂了 | 从 C → B → A 逐层传播 |
| 重试风暴 | 所有调用方同时重试 | 下游负载 × N,加速崩溃 |
没有弹性治理的微服务 = 圣诞树彩灯串联电路.一个灯泡坏了,整串灯全灭.
弹性治理 = 把彩灯改成并联 + 保险丝.一个灯泡坏了其他继续亮,短路时保险丝跳闸保护电路.
超时(Timeout)
最基础也最重要的弹性机制:给每个外部调用设置一个最长等待时间.
为什么必须设置超时
没有超时的调用 = 无限等待.当下游卡住时:
- 调用方 goroutine 被阻塞
- 请求持续到来,新的 goroutine 不断阻塞
- goroutine 数量暴涨,内存耗尽
- 服务 OOM 或完全无响应
超时策略
| 层级 | 设置位置 | 推荐值 |
|---|---|---|
| 连接超时 | TCP 连接建立 | 1-3s |
| 请求超时 | 单次 RPC 等待响应 | 根据接口 P99 × 2-3 |
| 端到端超时 | Gateway 或客户端 | 整条链路总预算 |
|
超时要沿调用链递减
如果 Gateway 超时 5s,内部 A→B 超时设为 10s--A 等 B 等 10s,但 Gateway 5s 就返回了,B 的工作白做.
正确做法:使用 gRPC Deadline 传播(02 篇讲过),或手动保证内部超时 < 外部超时.
重试(Retry)
瞬时故障(网络抖动,某个 Pod 重启中)可以通过重试自愈.但重试是双刃剑--用不好会加速系统崩溃.
何时重试
| 状态码 | 是否重试 | 原因 |
|---|---|---|
Unavailable |
重试 | 服务暂时不可用,可能很快恢复 |
DeadlineExceeded |
谨慎重试 | 可能是慢而非挂,重试可能加重负载 |
ResourceExhausted |
退避后重试 | 限流中,等一下再试 |
Internal |
通常不重试 | 服务端 bug,重试也是同样结果 |
InvalidArgument |
绝不重试 | 参数错误,重试 100 次也不会变对 |
NotFound |
绝不重试 | 资源不存在是确定性结果 |
退避策略(Backoff)
不能立即重试--所有客户端同时重试会形成重试风暴.必须加退避:
| 策略 | 间隔公式 | 特点 |
|---|---|---|
| 固定间隔 | 1s, 1s, 1s | 简单但容易同步重试 |
| 指数退避 | 1s, 2s, 4s, 8s | 给下游恢复时间 |
| 指数退避 + 抖动 | 1s±random, 2s±random, 4s±random | 打散重试时间点,避免雷群效应 |
|
重试的前提:操作幂等
如果 CreateOrder 不是幂等的,重试可能创建两个订单.重试策略只能用于幂等操作(查询天然幂等;写操作需要幂等设计--03 篇讲过).
对非幂等操作,宁可返回错误让用户重试,也不要自动重试.
熔断(Circuit Breaker)
当下游持续故障时,重试只会加重负担.熔断器的逻辑:如果连续 N 次调用失败,就不再调用下游,直接快速失败--给下游喘息恢复的机会.
三种状态
|
| 状态 | 行为 | 转换条件 |
|---|---|---|
| Closed(关闭) | 正常通过所有请求 | 错误率超阈值 → Open |
| Open(打开) | 所有请求直接失败,不调下游 | 冷却时间到 → Half-Open |
| Half-Open(半开) | 放过少量探测请求 | 探测成功 → Closed;失败 → Open |
Go 实现示例
|
熔断器的颗粒度
每个下游服务一个熔断器,而非全局一个.否则 A 服务挂了导致 B 服务的熔断器也打开--误伤.
更细粒度可以按"服务+方法"设置独立熔断器.
限流(Rate Limiting)
限流保护自身 不被过量请求打垮.与熔断不同--熔断保护的是调用链下游,限流保护的是自己.
限流层次
| 位置 | 保护对象 | 典型实现 |
|---|---|---|
| Gateway | 整个系统入口 | APISIX / Kong 插件 |
| 服务自身 | 单个服务的承载能力 | 中间件 / 拦截器 |
| 下游调用 | 防止打垮依赖方 | 客户端限流器 |
令牌桶实现
|
分布式限流
上面是单实例限流.如果服务有 10 个副本,每个限 100 QPS = 总共 1000 QPS.
需要精确的全局限流时用 Redis 做中心计数器(如 go-redis/redis_rate),但增加了 Redis 依赖和延迟.大多数场景下,单实例限流 × 副本数的近似值够用.
隔离舱(Bulkhead)
名字来自船舶设计--把船体分成多个水密隔舱,一个进水不会全船沉没.
在微服务中:为不同的依赖/调用方分配独立的资源池,一个下游出问题不会耗尽所有资源.
隔离方式
| 方式 | 实现 | 场景 |
|---|---|---|
| 线程池隔离 | 每个下游用独立线程池/goroutine pool | Java (Hystrix),Go 中用 channel 信号量 |
| 信号量隔离 | 限制并发调用数 | 轻量,不需要额外线程 |
| 进程隔离 | 不同依赖的调用放在不同 Pod/容器 | 极端隔离需求 |
|
降级(Degradation)
当上游故障或自身过载时,主动放弃部分非核心功能,保证核心链路可用.
降级策略
| 策略 | 做法 | 示例 |
|---|---|---|
| 返回缓存 | 用最近一次正常结果 | 推荐服务挂了 → 返回热门商品缓存 |
| 返回默认值 | 预设的兜底数据 | 用户头像服务挂了 → 返回默认头像 |
| 功能关闭 | 直接跳过非核心调用 | 评论服务过载 → 商品页不显示评论区 |
| 简化流程 | 跳过非必要步骤 | 风控服务超时 → 小额订单直接放行 |
|
降级分级
实践中将功能按重要性分级:
P0(核心) = 登录,下单,支付--绝不降级.
P1(重要) = 库存校验,风控--可短时跳过但需告警.
P2(一般) = 推荐,评论,个性化--可长时间降级不影响核心体验.
降级开关通过配置中心(06 篇 ConfigMap / 远程配置)动态控制.
六把武器的组合使用
这些机制不是互斥的--生产环境中它们层层叠加:
|
|
弹性治理的可观测性
治理策略必须配合监控,否则你不知道它们是否生效,阈值是否合理:
| 指标 | 含义 | 关注点 |
|---|---|---|
| 熔断器状态变化 | Open/Close 切换次数 | 频繁切换说明阈值设置不合理 |
| 重试次数 / 重试成功率 | 重试是否有效 | 重试成功率低说明不是瞬时故障 |
| 限流触发次数 | 流量是否超出容量 | 持续触发需要扩容 |
| 降级触发次数 | 依赖健康度 | 频繁降级需要修复根因 |
| P99 延迟变化 | 超时是否合理 | P99 接近超时值说明阈值太紧 |
快速回顾
- 超时:每个外部调用必须设 deadline,沿调用链递减
- 重试:仅限幂等操作 + 可重试状态码,必须指数退避 + 抖动
- 熔断:连续失败 → 快速失败保护下游,冷却后半开探测
- 限流:令牌桶保护自身承载能力,Gateway + 服务双层限流
- 隔离舱:不同下游独立资源池,防止一个拖垮全部
- 降级:按功能重要性分级,非核心功能可返回缓存/默认值/跳过
- 六把武器层层叠加使用,配合可观测性验证效果
动手练习
- 验证重试状态码过滤:写一个 gRPC 服务端,随机 50% 概率返回 Internal 错误.客户端配置自动重试(仅重试 Unavailable),观察 Internal 错误不会被重试
- 实现熔断器:用
sony/gobreaker给客户端加熔断器:连续 5 次失败后熔断 10 秒.验证熔断期间请求直接失败不再打到服务端 - 测试隔离舱:实现一个信号量隔离舱(并发限制 5),用 10 个并发请求测试,观察第 6 个开始被拒绝
- 组合弹性治理链路:实现"限流 → 超时 → 熔断 → 降级"完整链路.服务端延迟从 100ms 逐步升高到 5s,观察各层机制依次触发