grpc客户端需通过自定义resolver.builder实现服务发现,配合grpc.withresolvers和round_robin策略及服务配置启用负载均衡;代理须透传metadata和context以保障链路追踪与超时传递。

GRPC客户端怎么自动发现服务地址而不是写死localhost:8080
GRPC本身不内置服务发现,硬编码地址在生产环境必然失败。必须靠外部机制把服务实例列表喂给客户端。
最直接的做法是用grpc.WithResolvers注册自定义解析器,让grpc.Dial能动态拉取地址。Resolver不是插件,而是一个实现了resolver.Builder接口的结构体,负责监听变化、更新resolver.State。
- 不要在
Build方法里做阻塞初始化——它会被Dial同步调用,卡住连接 - 务必在
ResolveNow里触发一次主动刷新,否则首次连接可能用到过期缓存 - 更新
State时要传全新resolver.Address切片,不能复用旧对象(GRPC会浅比较)
示例关键片段:
func (r *etcdResolver) ResolveNow(_ resolver.ResolveNowOptions) {
go r.watchAndUpdate() // 异步触发,避免阻塞
}
为什么用round_robin负载均衡策略却总打到同一台实例
因为GRPC默认只启用passthrough解析器,它把整个URL当单个地址处理,根本不会触发负载均衡逻辑。
立即学习“go语言免费学习笔记(深入)”;
必须配合自定义Resolver使用,并显式开启策略:
- 连接时加
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`) - Resolver返回的
resolver.State里Addresses字段至少含2个不同Addr,否则LB无从选择 - 确保后端服务注册的IP+端口真实可通——LB不会做连通性校验,挂掉的实例仍会参与轮询
常见错误现象:rpc error: code = Unavailable desc = connection closed反复出现,但日志显示始终连同一台机器。
代理模式下如何透传原始调用上下文(如trace_id)
GRPC代理(比如用grpc-go写的中间网关)默认不转发metadata,所有header都会被清空。
必须手动提取并注入:
- 在代理的
UnaryInterceptor里用metadata.FromIncomingContext读取原始MD - 调用下游前用
metadata.NewOutgoingContext把相同key-value塞回去 - 注意
trace_id这类字段名要小写(GRPC规范要求metadata key全小写,否则Go client会忽略)
漏掉这一步,链路追踪就会断在代理层,Jaeger里只看到“代理→下游”,看不到“上游→代理”。
透明化远程调用时,context.DeadlineExceeded错误到底是哪边超时
不是网络问题,而是上下文传播链断裂导致的误判。GRPC的timeout由客户端设置的context.WithTimeout逐跳传递,但代理若没透传context,下游就拿不到deadline,只能用自己的默认值(常为0),最终表现为“下游响应慢”,实际是代理丢弃了超时信号。
- 检查代理是否在每次
Invoke或NewClientStream前都用req.Context()构造新context - 避免在代理里用
context.Background()发起下游调用 - 如果用了
WithBlockDial选项,会掩盖真实的超时来源——建议生产环境禁用
最容易被忽略的是:代理自身处理请求的耗时(比如鉴权、日志序列化)也会计入客户端deadline,这部分无法被下游感知,但会导致客户端先超时。










