gRPC客户端重试必须显式启用,Kratos默认不开启;需通过grpc.RetryPolicy配置JSON格式重试策略并注入CallOption,且必须包含UNAVAILABLE等状态码,同时服务端需配合超时控制与优雅关闭。

gRPC客户端重试必须显式启用,Kratos默认不开启
Kratos底层用的是标准grpc.ClientConn,而gRPC官方从1.26+开始才支持内置重试,且**默认关闭**。如果你没在grpc.Dial()里配grpc.WithDefaultCallOptions()带grpc.WaitForReady(false)和grpc.RetryPolicy,那任何失败都会立刻返回错误,不会重试。
常见错误是以为加了transport.WithMiddleware(retry.Middleware)就万事大吉——但那是HTTP transport的重试中间件,对gRPC transport完全无效。
- 重试策略只能通过
grpc.CallOption注入到每次RPC调用中,或设为默认选项 -
grpc.RetryPolicy必须是JSON字面量(不是Go struct),且字段名严格匹配gRPC规范:"MaxAttempts"、"InitialBackoff"、"MaxBackoff"、"BackoffMultiplier"、"RetryableStatusCodes" - 状态码列表必须用全大写字符串,如
"UNAVAILABLE"、"DEADLINE_EXCEEDED",小写或数字都会静默失效
retryable_status_codes别漏掉UNAVAILABLE,这是网络抖动最常见原因
服务刚上线、DNS解析延迟、连接被LB中断时,gRPC常返回rpc error: code = Unavailable desc = ...。这个状态码不在gRPC默认重试白名单里,必须手动加入,否则哪怕配了重试策略也毫无作用。
Kratos项目里典型配置长这样:
conn, err := grpc.Dial(
"service:///user",
grpc.WithInsecure(),
grpc.WithDefaultCallOptions(
grpc.RetryPolicy(`{
"MaxAttempts": 4,
"InitialBackoff": "0.1s",
"MaxBackoff": "1s",
"BackoffMultiplier": 2,
"RetryableStatusCodes": [ "UNAVAILABLE", "DEADLINE_EXCEEDED", "RESOURCE_EXHAUSTED" ]
}`),
),
)-
RESOURCE_EXHAUSTED用于限流场景,比如服务端熔断后返回此码 - 不要加
UNKNOWN或INTERNAL——它们往往代表服务端逻辑错误,重试只会放大问题 - 最大尝试次数建议≤4,避免雪崩;初始退避时间别低于100ms,太短会打满服务端
服务端也要配合:超时控制 + graceful shutdown
客户端重试再完善,服务端没做好超时和优雅关闭,照样会把重试请求堆成积压队列。Kratos项目中需检查两处:
- 在
grpc.NewServer()里设置grpc.MaxConcurrentStreams(1000),防止流数耗尽导致新请求被拒绝为UNAVAILABLE - 所有handler函数必须接收
ctx context.Context并及时响应ctx.Done(),尤其DB查询、HTTP下游调用等IO操作 - App启动时注册
app.WithGracefulShutdownTimeout(30 * time.Second),确保OS发送SIGTERM后,正在处理的请求有足够时间完成
否则会出现“客户端已重试3次,第4次刚发出去,服务端就直接close listener”——这种竞态下重试完全失效。
重试日志和指标必须单独埋点,不能依赖通用错误统计
Kratos默认的transport/grpc监控只统计最终结果,不区分“首次失败”和“第3次才成功”。这意味着你看到P99延迟突增,却无法判断是单次慢还是重试叠加造成的。
正确做法是在service层手动记录:
- 每次RPC调用前打一条
debug日志,含attempt=1、method=/user.v1.UserService/GetUser - 仅当
err != nil && !status.IsOK(err)且准备重试时,再记一次debug日志,把attempt递增 - 在Prometheus里用
grpc_client_attempt_count{method=~".+",code!="OK"} > 1告警,比单纯看grpc_client_failed_total有用得多
最容易被忽略的是:重试行为本身会放大下游压力,尤其当多个客户端同时重试同一失败服务时。务必确认你的重试策略和下游容量是否匹配——这不是框架能替你决定的事。


















