bigywater.tech
gRPC · 41

错误传播:服务 A 调 B 时状态码怎么翻译

服务 A 调服务 B 失败了,A 该把错误怎么交给自己的客户端?直接 return err 有什么问题?重试叠加会怎样?下游超时该报什么码?这篇用 客户端 → A → B 把每种做法的后果都量出来。

透传 4 个 code
B 的 InvalidArgument、Unauthenticated、PermissionDenied、FailedPrecondition 被透传给客户端,客户端会误以为是自己的错
3 → 9
客户端 3 次重试 × A 的 3 次重试,B 收到的次数
303.7 ms vs 937.6 ms
A 把下游超时透传成 DeadlineExceeded 与翻译成 Unavailable 时,客户端的总耗时

code 的含义是对着调用者的

实测 · 本机执行输出
  B 返回                 A 原样透传 → 客户端         A 翻译 → 客户端
  NotFound             NotFound             NotFound
  InvalidArgument      InvalidArgument      Internal
  Unauthenticated      Unauthenticated      Internal
  PermissionDenied     PermissionDenied     Internal
  FailedPrecondition   FailedPrecondition   Internal
  ResourceExhausted    ResourceExhausted    ResourceExhausted
  Unavailable          Unavailable          Unavailable
  DeadlineExceeded     DeadlineExceeded     Unavailable
  Internal             Internal             Internal

下游返回 InvalidArgument,说的是 A 传的参数有问题,不是 A 的客户端的参数有问题。原样透传会让客户端误解、误处理。常见做法是把它们折叠成 Internal。翻译表要自己定,不是协议规定的。

message 不要透传

实测 · 本机执行输出
  透传:客户端看到 code=Internal message="B 内部错误:库表 orders_v2 找不到,连接 10.0.0.5:5432 失败"
  翻译:客户端看到 code=Internal message="下游服务出错"
  B 已经停掉,透传:客户端看到 code=Unavailable message="connection error: desc = \"transport: Error while dialing: dial tcp 127.0.0.1:9803: connect: connection refused…"
  B 已经停掉,翻译:客户端看到 code=Unavailable message="下游服务:Unavailable"

下游的 message 里可能有表名、地址等内部信息,对外返回通用描述,细节写日志。

重试要放在一层

实测 · 本机执行输出
  只有客户端重试 3 次                        B 收到 3 次;客户端最终:Unavailable
  客户端 3 次 × A 应用层重试 3 次              B 收到 9 次;客户端最终:Unavailable
  客户端 3 次 × A 的 gRPC 重试策略 3 次        B 收到 9 次;客户端最终:Unavailable
  只有 A 重试 3 次(客户端不重试)                B 收到 3 次;客户端最终:Unavailable

每层都重试,下游收到的次数是各层次数相乘。多层服务里通常只在一层重试,并配合重试节流。

超时的翻译

实测 · 本机执行输出
  透传(DeadlineExceeded)     客户端最终 DeadlineExceeded,用了 303.7 ms;B 收到 1 次,到达时刻 [2.033625ms]
  翻译(Unavailable)          客户端最终 Unavailable,用了 937.6 ms;B 收到 3 次,到达时刻 [1.877584ms 314.555542ms 635.466917ms]

下游超时可以报 DeadlineExceeded 或 Unavailable,两者对客户端重试行为的影响不同:翻译成 Unavailable 会触发客户端重试,可能把慢下游进一步压垮。

分步演示

错误翻译
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。链路是本机进程内的 客户端 → A → B;B 按请求里的 N 返回对应的 gRPC code;A 的翻译函数是我写的示例策略;总耗时在调用返回后立即读取(调用之后不再 sleep);B 收到的次数由 B handler 里的计数器得到
    • 没有验证:三层及以上重试的放大倍数(27 倍是外推);翻译成 Unavailable 之外的方案(如自定义 Aborted 或用 errdetails 标明来源);重试预算(retry budget)或 hedging;翻译对 grpc-gateway 的 HTTP 状态码的影响(第 29 篇);跨语言服务的状态码约定;status 的 details 在翻译时是否保留(第 22 篇)
    京ICP备2026067502号-1京公网安备11010502063487号