分布式计算节点均衡依赖服务发现、健康探测、连接复用和策略反馈四者联动;gRPC需满足resolver前缀、显式配置负载策略及非空地址列表;HTTP轮询须复用Client并合理管理连接;加权与least_connections策略需原子同步状态并避免性能瓶颈。

分布式计算节点均衡不是靠“选算法”就能解决的,而是服务发现、健康探测、连接复用和策略反馈四者联动的结果。
gRPC 客户端启用 round_robin 必须满足三个条件
新版 grpc-go(v1.40+)默认不启用任何负载均衡策略,直接 Dial 会打到第一个解析出的地址,导致“看似配置了却没生效”。常见错误现象是:日志里只看到一个后端被调用,service_request_total{instance="10.0.1.5:8080"} 指标始终为 0,其余实例无流量。
- 目标 URL 必须带 resolver 前缀,如
"etcd:///user-svc"或"dns:///user-svc.example.com";若用passthrough:///10.0.1.1:8080,10.0.1.2:8080,需额外注册 balancer - 必须显式传入
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),WithBalancerName已被弃用 - resolver 返回的地址列表不能为空——检查 Consul/Etcd 中服务是否注册成功、健康检查是否通过(HTTP
/health返回 200)、客户端是否监听到变更事件
HTTP 客户端轮询容易踩的连接复用坑
用 http.Client + 自定义 RoundTripper 实现轮询时,90% 的性能问题来自连接管理不当。典型错误是每次请求都新建 http.Client,或未设置 Transport.MaxIdleConnsPerHost,导致 TIME_WAIT 爆满、DNS 解析失败、延迟飙升。
- 复用全局
http.Client实例,Transport需设MaxIdleConnsPerHost = 100和IdleConnTimeout = 30 * time.Second - 轮询逻辑不能放在
RoundTrip内部加锁——用atomic.AddUint64(&idx, 1)+ 取模,避免 goroutine 竞争阻塞 - 遇到
connection refused或 HTTP 5xx,不能仅重试,要临时将该Addr加入退避 map:backoff.Store(addr, time.Now().Add(30*time.Second)),并在Next()中跳过过期项
加权轮询权重更新必须原子同步
权重不是静态配置,而是随节点 CPU、内存、队列长度动态调整的。如果权重变更与轮询计数器不同步,会出现“新权重未生效就已调度”或“旧权重持续占用过多流量”的偏差。
立即学习“go语言免费学习笔记(深入)”;
- 权重字段必须用
atomic.Value包裹,更新时Load()当前实例列表 → 修改副本中每个Instance.Weight→Store()替换整个切片 - 不要在
Next()中实时计算总权重——预计算并缓存,否则高并发下for循环遍历权重数组会成为瓶颈 - Consul KV 中存储权重时,建议用
service/user-svc/weights路径,客户端监听该 key 变更,避免全量拉取服务列表触发抖动
least_connections 策略需谨慎维护连接计数
该策略适合长连接场景(如 WebSocket、gRPC streaming),但对 HTTP 短连接意义不大——因为连接生命周期太短,计数误差大。真实落地时,连接数统计本身就成了单点瓶颈。
- 用
sync.Map存储每个Addr的活跃连接数,http.Transport.DialContext回调中递增,RoundTrip结束后递减 - 避免在
Next()中遍历全部实例查最小值——改用堆(container/heap)维护,插入/弹出 O(log n) - 注意 gRPC 场景下连接数 ≠ 请求并发数:
ClientConn是复用的,应统计每个addr上的stream数,而非 TCP 连接数
真正难的不是写轮询逻辑,而是让“实例列表更新”“健康状态感知”“连接池状态”“权重调节信号”四者在毫秒级完成同步。多数线上故障不是算法错,而是某一层状态滞后了 200ms —— 比如健康检查探活间隔设为 5s,但故障节点已在第 3 秒宕机,这 2 秒内请求仍会发过去。


















