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