必须用context.WithTimeout控制每次RPC调用超时,grpc.Dial()的WithTimeout仅作用于初始连接建立;需为每次调用创建独立带超时的ctx并及时cancel,服务端须检查ctx.Done()及时退出;仅对codes.Unavailable和codes.DeadlineExceeded做指数退避重试,禁用硬编码sleep,HTTP客户端还需配置IdleConnTimeout、KeepAlive和MaxIdleConnsPerHost。

gRPC调用超时必须用context.WithTimeout,不能只靠.Dial()设超时
gRPC客户端连接建立默认是异步的,grpc.Dial()立即返回*grpc.ClientConn,根本不管后端是否可达。网络抖动时,连接可能卡在后台重连队列里,直到第一次client.GetUser(ctx, req)才暴露问题——此时报的rpc error: code = Unavailable desc = connection closed或context deadline exceeded,其实是连接层失败,不是业务超时。
正确做法是:每次RPC调用都传入独立的context.WithTimeout,比如ctx, cancel := context.WithTimeout(parentCtx, 1200*time.Millisecond);defer cancel()必须写在调用前,否则goroutine泄漏;超时值建议设为下游P95延迟的1.2–1.5倍(压测得P95=950ms,就设1200ms)。
- 别复用已
cancel()的ctx进重试循环——第二次ctx.Err()就是context.Canceled,重试无效还泄漏goroutine -
grpc.Dial()里的WithTimeout(5 * time.Second)只控制初始连接建立,必须配,但不能替代调用层超时 - 服务端也要检查
ctx.Done(),及时退出长耗时逻辑,避免资源浪费
哪些错误码该重试?gRPC官方重试拦截器生效条件很苛刻
gRPC从v1.29+才支持客户端重试,且默认关闭。光写grpc_retry.WithMax(3)没用,必须同时满足三件事:grpc.WithChainUnaryInterceptor(grpc_retry.UnaryClientInterceptor())、grpc_retry.WithCodes(codes.Unavailable, codes.DeadlineExceeded)、proto方法上声明retryable: true。漏一个,重试就不触发。
业务层自己实现重试更可控:只对codes.Unavailable(连接断开)、codes.DeadlineExceeded(真超时)做指数退避,用backoff.Retry();codes.NotFound、codes.InvalidArgument绝对不重试——它们是业务错误,重试只会放大压力。
立即学习“go语言免费学习笔记(深入)”;
- 别在for循环里硬写
time.Sleep()——退避策略要带jitter,避免雪崩式重试 - HTTP客户端同理:只重试
url.Error且err.Timeout()为true的,或errors.Is(err, syscall.ECONNREFUSED) - 响应码重试必须严格:
resp.StatusCode >= 500才考虑,400/401/404直接返回
Keepalive不配等于裸奔,跨机房场景下空闲断连必现
云厂商SLB、NAT网关普遍60秒空闲断连,gRPC客户端若不发keepalive,第一次请求或空闲后下一次请求就会触发完整重连链路(DNS+TCP+TLS),实测跨AZ延迟毛刺达150–300ms。这不是超时,是连接重建耗时。
客户端必须显式传grpc.WithKeepaliveParams(),且Time(发送间隔)必须大于Timeout(等待响应超时),例如Time: 30 * time.Second, Timeout: 10 * time.Second;服务端对应配grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{PermitWithoutStream: true}),否则低频调用时连接会被静默踢掉。
- 别只构造
keepalive.ClientParameters{}变量却不塞进grpc.Dial()选项列表——配置根本不会生效 -
PermitWithoutStream: true能保活,但掩盖真实路径故障:NAT设备单向清理连接后,首次RPC仍会卡住几秒 - Keepalive ping走HTTP/2 PING帧,不经过应用层,无法替代端到端健康检查
HTTP客户端超时链断裂:Timeout只是冰山一角
只设http.Client.Timeout毫无意义——它只管连接建立和读响应头,不控整个请求生命周期。网络抖动恢复后,大量请求卡在net/http: request canceled (Client.Timeout exceeded while awaiting headers),但curl -v却能通,说明连接复用到了陈旧连接。
必须显式配置Transport:设IdleConnTimeout(如30 * time.Second)防止空闲连接滞留,KeepAlive(如15 * time.Second)主动探测,MaxIdleConnsPerHost至少100防抖动期间池枯竭。
- 别复用
http.DefaultClient——它的全局设置会让健康检查请求也被重试三次,放大下游压力 - 自定义
RoundTripper封装原生transport,只拦截业务HTTP调用,不影响指标上报等旁路流量 - DNS解析必须接管:用
grpc.WithResolvers()或net.DefaultResolver设Timeout,按记录TTL缓存,避免打到下线实例
真正卡点不在“要不要重试”,而在“重试前是否确认连接可用”——Keepalive保活、分层超时、错误码精准识别这三件事没做扎实,重试只是把问题从一处搬到另一处。


















