grpc dial 必须显式配置重试策略,consul 仅负责服务注册发现与健康检查,不参与请求级重试;重试由 grpc.withdefaultserviceconfig 注入 json 策略控制,仅对 unary rpc 生效,且需精确匹配 proto service 名。

gRPC Dial 时必须显式配置重试策略,Consul 自身不控制调用重试
Consul 只管服务注册、发现和健康状态,gRPC 客户端发起的实际请求是否重试、重试几次、间隔多久,完全由 gRPC 自身的 RetryPolicy 控制。Gin 层若通过 gRPC 调用下游服务,重试逻辑不在 Consul 配置里,而在 grpc.Dial 的 service config 中。
- 默认情况下 gRPC 禁用重试(即使服务节点列表来自 Consul)
- 重试行为只对 unary RPC 生效,streaming 不支持
- 必须用
grpc.WithDefaultServiceConfig注入 JSON 格式的重试策略,不能靠中间件或全局变量“透传”
如何写一个带重试的 gRPC 连接配置
以下配置让 gRPC 客户端在调用失败时最多重试 3 次,初始退避 100ms,指数增长上限 1s,仅对 UNAVAILABLE 和 DEADLINE_EXCEEDED 错误生效:
userConn, err := grpc.Dial(
fmt.Sprintf("consul://%s:%d/%s", host, port, serviceName),
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(`{
"methodConfig": [{
"name": [{"service": "proto.UserService"}],
"retryPolicy": {
"MaxAttempts": 3,
"InitialBackoff": ".1s",
"MaxBackoff": "1s",
"BackoffMultiplier": 2,
"RetryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]}`),
)
-
name字段必须精确匹配 proto 中的service名(如UserService),大小写敏感 - 若服务名含 package,例如
user.v1.UserService,则name也得写全 - 重试不会跨节点——每次重试都走当前负载均衡器选中的节点,不是“换一个实例再试”
Consul 返回的节点列表本身不带重试语义
你调用 api.Health().ServiceNodes 或通过 consul:// 协议让 gRPC 自动发现服务,拿到的只是 IP:Port 列表。Consul 不会告诉你“这个节点失败后该切到哪个”,也不参与请求级重试决策。
- 如果想实现“节点级故障转移 + 请求级重试”双重保障,需组合使用:
PassingOnly: true过滤掉非 healthy 节点 + gRPC 内置 retry policy - 别依赖
ServiceNodes结果顺序做“主备切换”——它不保证稳定性,且 Consul 默认按节点注册时间排序,不是按健康度或延迟 - 若下游服务响应慢导致频繁
DEADLINE_EXCEEDED,要同步检查Check.Timeout和你的 gRPC client 的grpc.WaitForReady(false)行为
Gin 路由层无法替代 gRPC 的重试控制
有人试图在 Gin 的 c.Next() 或中间件里捕获错误后手动重试,这是危险的:HTTP 中间件不知道 gRPC 的 stream 状态、metadata、deadline,也无法复用已建立的连接上下文。
- 重试必须在 gRPC client 层完成,否则可能造成重复写、状态不一致(比如两次 CreateOrder)
- 若业务逻辑要求“强一致性重试”,应改用幂等 key + 服务端去重,而不是靠客户端反复发
- Gin 只负责把请求转发给 gRPC client,它的角色是协调者,不是重试引擎











