5.1 KiB
5.1 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.grpc-microservices.governance | 服务治理:超时、重试、熔断、限流、注册发现、链路追踪、分布式事务 | lesson | backend |
|
|
advanced |
|
3 |
|
|
2 |
服务治理:超时、重试、熔断、限流、注册发现、链路追踪、分布式事务
解释说明
服务拆开之后,一次用户请求会经过多个服务,任何一个慢了或挂了都可能拖垮整条链路(雪崩)。服务治理就是防止这种情况、并且出问题时能定位。
| 手段 | 解决什么 | 要点 |
|---|---|---|
| 超时 | 下游卡住时不无限等待 | 每次调用都设;越往下游超时越短,总和不超过入口超时 |
| 重试 | 偶发失败 | 只重试幂等操作和可重试错误;指数退避 + 抖动;限制次数;不要每一层都重试(会放大流量) |
| 熔断 | 下游已经故障时快速失败,给它恢复时间 | 关闭 → 打开(直接失败)→ 半开(试探)→ 关闭 |
| 限流 | 保护自己不被打垮 | 令牌桶 / 滑动窗口;超出返回 429 / ResourceExhausted |
| 降级 | 非核心功能出问题时保证核心可用 | 推荐服务挂了就返回默认列表 |
| 注册发现 | 服务实例动态变化时找到地址 | Consul、etcd、Nacos;Kubernetes 里直接用 Service 的 DNS |
| 负载均衡 | 请求分给多个实例 | gRPC 是长连接,需要客户端负载均衡或服务网格,否则请求会集中在一个实例 |
| 链路追踪 | 定位慢在哪个服务 | 用 OpenTelemetry 在请求中传递 trace ID,每个服务记录自己的 span |
分布式事务:一个业务操作跨多个服务的数据库。
- 2PC(两阶段提交):强一致,但协调者单点、锁持有时间长,互联网业务很少用。
- TCC:每个服务实现 Try(预留资源)/ Confirm / Cancel,业务侵入大。
- Saga:拆成一串本地事务,每一步有对应的补偿操作;失败时逆序执行补偿。
- 可靠消息 + 最终一致:发件箱 + MQ + 幂等消费(见消息可靠性),大部分业务的首选,再加定时对账。
案例
订单服务调用库存服务和优惠券服务:
- 入口超时 2 秒;调用库存 500ms、优惠券 300ms;
- 优惠券服务故障时熔断,下单时提示“优惠券暂不可用”,仍然允许原价下单(降级);
- 下单跨“订单、库存、优惠券”三个数据库:用 Saga——创建订单(待确认)→ 扣库存 → 核销优惠券,任一步失败就逆序补偿(释放库存、取消订单)。
代码示例
完整代码和测试见 examples/resilience(Go 标准库实现)。重试的核心:
func Retry(ctx context.Context, attempts int, base time.Duration, fn func(context.Context) error) error {
var err error
for i := range attempts {
if err = fn(ctx); err == nil {
return nil
}
var p Permanent
if errors.As(err, &p) || errors.Is(err, ErrOpen) || i == attempts-1 {
return err // 不可重试的错误、熔断中、最后一次:直接返回
}
backoff := base << i // 指数退避
backoff += time.Duration(rand.Int64N(int64(backoff)/2 + 1)) // 抖动
select {
case <-ctx.Done():
return errors.Join(err, ctx.Err()) // 截止时间到了就不再重试
case <-time.After(backoff):
}
}
return err
}
组合使用:先熔断后重试(熔断打开时 ErrOpen 不再重试),外层由 context.WithTimeout 控制总时长:
ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
defer cancel()
err := resilience.Retry(ctx, 3, 50*time.Millisecond, func(ctx context.Context) error {
return breaker.Do(ctx, func(ctx context.Context) error {
_, err := inventoryClient.Reserve(ctx, req)
return err
})
})
生产环境通常不自己写,而是用 gRPC 自带的重试配置(service config)、服务网格(Istio 等)或成熟的库;自己实现一遍是为了理解原理和面试。
易错点
- 每一层都重试 3 次:三层调用会放大成 27 倍请求,雪上加霜。在一个地方重试即可。
- 重试非幂等操作:扣款、下单重复执行。要么不重试,要么带幂等键。
- 下游超时比上游长:上游已经超时返回,下游还在干活,白白浪费资源。
- 熔断把业务错误算作失败:用户输错参数也能把熔断器打开。
- gRPC 长连接 + Kubernetes Service 默认负载均衡:连接建立后请求都落到同一个 Pod。用客户端负载均衡(headless Service +
round_robin)或服务网格。 - 没有 trace ID:出了问题只能在多个服务的日志里按时间猜。
面试常问
见 gRPC 与微服务面试题:服务雪崩怎么防、熔断器状态机、重试的注意事项、分布式事务方案对比。