Picker在每次RPC调用(Invoke/NewStream)时起作用,决定请求发往哪个已就绪的SubConn,而非连接建立时选定后端。

Picker接口到底在哪个环节起作用
gRPC Go 的负载均衡不是在连接建立时一次性选完,而是在每次 Invoke 或 NewStream 时才调用 Picker.Pick。这意味着它不决定“连哪台后端”,而是决定“这次请求发给哪个已存在的子连接”。如果你看到负载没分散、总打到同一台实例,大概率是 Picker 没被触发,或返回了固定 SubConn。
- Picker 只对
round_robin、least_request等内置策略或自定义 balancer 生效;直接 Dial 时传grpc.WithInsecure()但没配grpc.WithBalancerName("round_robin"),Picker 根本不会注册 - Picker 的输入
PickInfo里FullMethodName是空字符串(非 unary/rpc 场景下),别依赖它做路由判断 - Picker 返回的
SubConn必须已处于READY状态,否则 gRPC 会静默 fallback 到下一个 Picker 调用 —— 这容易误判为“Picker 没生效”
怎么写一个带健康检查的 Picker
原生 round_robin 不感知后端是否真能处理请求,只看 SubConn 状态。要实现“跳过超时/5xx 多的节点”,得在 Picker 里查缓存的指标,而不是重连探测。
- 不要在
Pick方法里做 HTTP 请求或net.Dial:阻塞主线程,拖慢整个 RPC 调用链 - 用独立 goroutine 定期调用后端
/healthz或解析 access log,把结果写进map[resolver.Address]float64(比如成功率、P99 延迟) - Pick 时遍历
SubConn列表,用balancer.GetAddr拿到对应地址,查缓存得分,加权随机选 —— 注意并发读写要加sync.RWMutex - 示例片段:
func (p *healthPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { p.mu.RLock() defer p.mu.RUnlock() var candidates []pickerItem for _, sc := range p.subConns { addr := balancer.GetAddr(sc) score := p.scores[addr] if score > 0.3 { // 成功率阈值 candidates = append(candidates, pickerItem{sc: sc, weight: score}) } } // 加权随机选... }
为什么自定义 Picker 总是 fallback 到 pick_first
gRPC Go 的 balancer 构建链很敏感:只要任意一环返回 nil 或错误,就会退化成默认的 pick_first(即只用第一个 SubConn),且不报错、不打日志。
- 常见断点:
Builder.Build返回的balancer.Balancer实例,其UpdateClientConnState方法必须正确处理resolver.State中的Addresses—— 如果你过滤掉了所有地址(比如误判 health check 失败),gRPC 就认为“没可用后端”,直接 fallback -
Picker实现里不能 panic,也不能返回nil的PickResult.SubConn;哪怕暂时没候选,也该返回ErrNoSubConnAvailable让上层重试,而非静默失败 - 调试技巧:在
Build和Pick里加log.Printf,确认是否被调用;用grpc.WithBalancerName("xxx")时拼写必须和balancer.Register里的完全一致(区分大小写)
Picker 和 resolver.Address 的关系容易被忽略
resolver.Address 看似只是个 IP+Port,但它携带的 Attributes 字段才是 Picker 做差异化调度的关键入口 —— 比如灰度标签、机房信息、权重系数,都得靠这个透传。
立即学习“go语言免费学习笔记(深入)”;
- resolver 不该只返回
[]resolver.Address{{Addr: "10.0.1.2:8080"}};应该带上元数据:resolver.Address{ Addr: "10.0.1.2:8080", Attributes: attributes.New("zone", "shanghai", "weight", 100), } - Picker 里用
attr := address.Attributes拿到attributes.Attributes,再用attr.Value("weight")解包 —— 注意类型断言要防 panic - 如果多个后端地址用了相同
Addr(比如 VIP + keepalived),gRPC 会去重合并,导致 Picker 看不到全部实例;必须保证每个Address.Addr唯一,或用Attributes区分逻辑身份










