超时必须由外层 context 统一控制,Kratos 依赖 errgroup.WithContext(ctx) 实现协同取消;HTTP/gRPC 入口已注入带超时的 ctx,应透传而非用 context.Background() 手动构造;重试分 transport 层自动重试与业务级显式重试,后者须每次检查 ctx.Err()。

超时必须由外层 context 统一控制,不能在子请求里各自设 timeout
Kratos 的超时不是靠 transport 层单独配置生效的,而是依赖 errgroup.WithContext(ctx) 传入的可取消上下文。一旦任一子 goroutine 报错或超时,其余会收到 ctx.Err() 并退出——这是协同取消的前提。
常见错误写法:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)。这会导致子请求的 ctx 和主流程无关,cancel 不会传播到下游服务,重试和超时都形同虚设。
正确做法是:从 HTTP/gRPC 入口拿到原始 ctx(已带超时),直接透传给 errgroup.WithContext(ctx),再由它分发给所有并发子请求。
- HTTP Server 默认会基于
server.Timeout配置自动注入带超时的context.Context - gRPC Server 同理,依赖
grpc.MaxCallRecvMsgSize和拦截器中注入的 deadline-aware ctx - 手动构造 ctx 时,务必用
req.Context()或transport.Context()而非context.Background()
重试要分清“框架级”和“业务级”,别混用
Kratos 自身不提供通用重试中间件,但 transport 层(如 grpc.Client)支持连接/调用失败时的自动重试,而业务逻辑里的重试(比如聚合多个下游服务时某一个失败后重试)必须由你显式编码实现。
典型误区:在 errgroup 里对单个子请求做 for-loop 重试,却不检查 ctx.Err(),导致超时后还在无效重试。
- transport 层重试适用于网络抖动、连接闪断等瞬时故障,配置项是
grpc.RetryPolicy或http.RetryPolicy - 业务聚合重试必须包裹在
func() error内,并在每次循环开头加select { case - 不要对幂等性不确定的服务盲目重试,尤其涉及支付、库存扣减等场景
errgroup.Wait() 只返回第一个非 nil 错误,panic 不被捕获
errgroup.Group 的设计目标是“快速失败 + 协同取消”,不是错误收集器。它不会等所有 goroutine 执行完,也不会包装 panic 为 error。
如果你看到日志里有 panic 但 Wait() 返回 nil,大概率是某个子 goroutine 没 recover 就 panic 了,而 errgroup 对此无感知。
- 必须在每个子 goroutine 函数体内加
defer func() { if r := recover(); r != nil { ... } }() - 若需记录全部错误,得自己维护一个
sync.Map或切片,在每个子函数结尾显式写入 -
Wait()返回nil只代表“没遇到第一个非 nil error”,不代表所有子请求都成功
重试 + 超时组合下,最容易被忽略的是 backoff 策略和 retry budget
简单地设置 “重试 3 次,每次间隔 100ms” 在高并发下可能引发雪崩:100 个并发请求 × 3 次 = 300 次下游调用,远超下游承载能力。
Kratos 生态里没有内置 exponential backoff 或 jitter 支持,得自己集成或封装。
- 推荐用
github.com/cenkalti/backoff/v4,配合ctx控制总耗时 - 重试次数应随上游 timeout 缩短而动态减少,例如 2s timeout 最多允许 2 次重试
- 避免在
errgroup外部做重试——那会破坏协同取消语义,导致 goroutine 泄漏











