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

5.1 KiB
Raw Blame History

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
grpc-microservices
超时
重试
熔断
限流
服务发现
链路追踪
Saga
分布式事务
advanced
backend.grpc-microservices.grpc-proto
3
backend.grpc-microservices.scenario.monolith-split
backend.mq.reliability
backend.grpc-microservices.interview
14
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 与微服务面试题:服务雪崩怎么防、熔断器状态机、重试的注意事项、分布式事务方案对比。