Files
zhuangang/topics/backend/grpc-microservices/02-governance.md
T

102 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.grpc-microservices.governance
title: 服务治理:超时、重试、熔断、限流、注册发现、链路追踪、分布式事务
type: lesson
domain: backend
stack: [grpc-microservices]
tags: [超时, 重试, 熔断, 限流, 服务发现, 链路追踪, Saga, 分布式事务]
level: advanced
prerequisites: [backend.grpc-microservices.grpc-proto]
est_hours: 3
related: [backend.grpc-microservices.scenario.monolith-split, backend.mq.reliability, backend.grpc-microservices.interview]
week: [14]
order: 2
---
# 服务治理:超时、重试、熔断、限流、注册发现、链路追踪、分布式事务
## 解释说明
服务拆开之后,一次用户请求会经过多个服务,任何一个慢了或挂了都可能拖垮整条链路(**雪崩**)。服务治理就是防止这种情况、并且出问题时能定位。
| 手段 | 解决什么 | 要点 |
| --- | --- | --- |
| 超时 | 下游卡住时不无限等待 | 每次调用都设;越往下游超时越短,总和不超过入口超时 |
| 重试 | 偶发失败 | 只重试幂等操作和可重试错误;指数退避 + 抖动;限制次数;**不要每一层都重试**(会放大流量) |
| 熔断 | 下游已经故障时快速失败,给它恢复时间 | 关闭 → 打开(直接失败)→ 半开(试探)→ 关闭 |
| 限流 | 保护自己不被打垮 | 令牌桶 / 滑动窗口;超出返回 429 / `ResourceExhausted` |
| 降级 | 非核心功能出问题时保证核心可用 | 推荐服务挂了就返回默认列表 |
| 注册发现 | 服务实例动态变化时找到地址 | Consul、etcd、Nacos;Kubernetes 里直接用 Service 的 DNS |
| 负载均衡 | 请求分给多个实例 | gRPC 是长连接,需要客户端负载均衡或服务网格,否则请求会集中在一个实例 |
| 链路追踪 | 定位慢在哪个服务 | 用 OpenTelemetry 在请求中传递 trace ID,每个服务记录自己的 span |
**分布式事务**:一个业务操作跨多个服务的数据库。
- **2PC(两阶段提交)**:强一致,但协调者单点、锁持有时间长,互联网业务很少用。
- **TCC**:每个服务实现 Try(预留资源)/ Confirm / Cancel,业务侵入大。
- **Saga**:拆成一串本地事务,每一步有对应的补偿操作;失败时逆序执行补偿。
- **可靠消息 + 最终一致**:发件箱 + MQ + 幂等消费(见[消息可靠性](../mq/03-reliability.md)),大部分业务的首选,再加定时对账。
## 案例
订单服务调用库存服务和优惠券服务:
- 入口超时 2 秒;调用库存 500ms、优惠券 300ms;
- 优惠券服务故障时熔断,下单时提示“优惠券暂不可用”,仍然允许原价下单(降级);
- 下单跨“订单、库存、优惠券”三个数据库:用 Saga——创建订单(待确认)→ 扣库存 → 核销优惠券,任一步失败就逆序补偿(释放库存、取消订单)。
## 代码示例
完整代码和测试见 [examples/resilience](examples/resilience/)(Go 标准库实现)。重试的核心:
```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` 控制总时长:
```go
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 与微服务面试题](interview.md):服务雪崩怎么防、熔断器状态机、重试的注意事项、分布式事务方案对比。