102 lines
5.1 KiB
Markdown
102 lines
5.1 KiB
Markdown
---
|
||
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):服务雪崩怎么防、熔断器状态机、重试的注意事项、分布式事务方案对比。
|