用 sync.Map 统计各后端活跃连接数实现最少连接调度:键为后端地址,值为 atomic.Int64 计数器,proxy 前加 1、defer 中减 1;遍历 sync.Map 找最小值(非排序),结合健康检查过滤不健康节点,统一“连接”定义为 handler 生命周期,并保障状态与健康联动。

用 sync.Map 统计每个后端的活跃连接数
最少连接数调度本质是「实时感知并比较各节点当前连接数」,Golang 没有内置负载均衡器,得自己维护状态。直接用普通 map 并发读写会 panic,必须用线程安全结构。sync.Map 是最轻量、无锁路径较多的选择,比 sync.RWMutex + map 在高并发读多写少场景下更省开销。
常见错误是把连接数存在局部变量或 HTTP handler 闭包里——每次请求都重算,根本不是“当前”连接数。必须全局唯一、跨 goroutine 共享。
- 键用后端地址字符串(如
"10.0.1.10:8080"),值用int64类型原子计数器(sync/atomic) - 每次 proxy 前调用
atomic.AddInt64加 1,handler 结束时(哪怕 panic)用defer减 1 - 不要在
http.RoundTrip外层统计——HTTP/2 复用连接,一个http.Transport实例可能对应多个逻辑连接,得按业务粒度定义“连接”
选节点时遍历 sync.Map 找最小值,别用排序
你不需要对所有后端按连接数排序,只要找到当前最小的那个。排序(sort.Slice)会分配切片、拷贝 key 列表、额外比较,纯属浪费。直接一次遍历 + 记录最小值即可,O(n) 时间、零内存分配。
容易踩的坑是忽略节点不可用的情况:某个后端已宕机但连接数仍为 0,盲目选它会导致请求超时。必须结合健康检查结果过滤。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 遍历前先用
sync.Map.Range收集所有「健康且连接数已知」的节点 - 跳过连接数为负或未初始化的项(
sync.Map.Load返回false时说明该节点从未被注册过) - 如果所有节点都不健康,返回错误或 fallback 到轮询,别硬选一个
net/http.Transport 的 MaxIdleConnsPerHost 会影响“连接数”语义
你统计的“当前连接数”,和底层 TCP 连接、HTTP/1.1 keep-alive、HTTP/2 stream 不是一回事。Golang 的 http.Transport 会复用连接,一个后端可能只有 1 个 TCP 连接但承载几十个并发请求。这时候你统计的是业务层的“正在处理的请求数”,不是网络层连接数。
如果误把 Transport 的空闲连接池大小当调度依据,结果会严重失真。比如设了 MaxIdleConnsPerHost: 100,但实际只发了 2 个请求,连接数显示为 0 —— 它根本没建新连接。
- 务必统一“连接”的定义:推荐以 handler 进入到
defer执行结束为生命周期 - 禁用
http.Transport.IdleConnTimeout或设足够长(如 90s),避免连接被后台回收导致计数滞后 - HTTP/2 场景下,单个连接可承载多路请求,此时“最少连接数”退化为“最少活跃请求”,仍是合理策略
健康检查不及时会导致调度倾斜
最少连接数算法本身很朴素,但它极度依赖后端状态的准确性。如果某个节点已挂,但你的健康检查间隔是 30 秒,这 30 秒内所有新请求仍会按连接数被分过去——而它的连接数始终是 0,变成“最优节点”。
没有银弹,只能折中:用短周期主动探测(如 3~5 秒),配合失败快速熔断(连续 2 次失败即标记不健康),再加被动异常捕获(如 http.Client.Do 返回 context.DeadlineExceeded 或 net.OpError 时立刻减连接数并标记临时不可用)。
- 健康检查用独立 goroutine,别塞进请求处理链路,否则影响主流程延迟
- 状态缓存要带版本号或时间戳,避免多个 goroutine 同时更新造成覆盖
- 连接数统计和健康状态必须原子联动:节点被标为不健康时,应清空其连接数(或置为极大值),防止被选中
真正难的不是怎么选最小值,而是让这个“最小值”反映真实负载。连接数只是代理,背后是超时控制、重试策略、健康反馈速度——任何一个环节慢半拍,整个调度就失效。

















