手写负载均衡算法在go微服务中常因忽略健康探测、连接池隔离等关键点而失败;grpc需显式配置service config启用round_robin;http轮询需配合实例列表reload重置计数器;权重须动态拉取并归一化;least_connections需用sync.map统计真实活跃连接数。

手写负载均衡算法在 Go 微服务里几乎总是错的起点——它容易忽略健康探测、连接池隔离、优雅下线和指标暴露,而这些恰恰是生产环境里最常出问题的地方。
gRPC-Go 启用 round_robin 必须显式配置 service config
gRPC-Go v1.27+ 默认禁用客户端负载均衡,即使你传了多个地址,dns:///svc.example.com 也只会打到第一个解析出的 IP。不配 grpc.WithDefaultServiceConfig,轮询根本不会生效。
- 正确姿势:
grpc.Dial("dns:///svc.example.com", grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`)) - 如果用
passthrough:///10.0.1.1:8080,10.0.1.2:8080,必须同时注册自定义 balancer,否则 panic 错误是:"no load balancing policy configured" - 别忘了导入 resolver:确保已执行
_ "google.golang.org/grpc/resolver/dns",否则 DNS 解析直接失败
HTTP 客户端轮询时原子计数器必须配合实例列表 reload
用 atomic.AddUint64(&idx, 1) % len(instances) 做轮询看似线程安全,但实例列表更新后,旧的 idx 可能越界或命中已下线节点。
- 每次
instances更新(比如服务发现回调触发),必须重置计数器,推荐用atomic.StoreUint64(&idx, 0) - 不要把轮询逻辑塞进
RoundTrip方法里——那里是高并发热点,锁或原子操作都可能成为瓶颈 - 更稳的做法:用
sync.Map缓存每个 endpoint 的退避时间,遇到connection refused或 HTTP 5xx 时写入time.Now().Add(30 * time.Second),选节点前先过滤
加权轮询权重不能硬编码,得从服务注册中心动态拉取
静态权重(比如代码里写死 Weight: 3)在弹性扩缩容场景下完全失效。真实流量分配必须响应实例 CPU、内存、RT 等指标变化。
- Consul/etcd 的 KV 存权重时,建议用路径如
/services/myapi/instances/10.0.1.1:8080/weight,避免单 key 大对象更新引发竞争 - 权重更新不能立即生效:要等 Resolver 触发
UpdateClientConnState,balancer 才会收到新实例列表;自己实现时需保证instances替换是原子的(用atomic.Value.Store) - 注意权重归一化:若某实例权重为 0,应从候选池剔除,而非让它占“0 份”,否则
totalWeight计算易出错
least_connections 在 Go 里必须用 sync.Map 统计活跃连接数
每个后端的连接数不是靠 http.Transport.MaxIdleConnsPerHost 能反映的——那是空闲连接池上限,不是当前活跃请求数。
- 真正要统计的是:从
RoundTrip开始到resp.Body.Close()结束之间的请求数量 - 用
sync.Map存map[string]int64,key 是host:port,增减都用atomic.AddInt64,避免读写锁阻塞 - 务必处理 panic 场景:如果请求中途 panic(比如 context canceled),必须确保连接计数被减掉,否则连接数只增不减
最容易被忽略的是:所有策略都依赖服务发现的实时性,但 etcd watch 或 Consul blocking query 都可能丢事件;没有兜底的定期全量拉取,实例上下线就会滞后几十秒甚至更久。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











