Go标准库无内置负载均衡器,需用net/http.RoundTripper自实现:复用连接池、支持超时重试、运行时更新后端;避免在handler中轮询导致连接不复用、配置丢失、阻塞goroutine;正确做法是原子轮询+请求克隆+健康检查异步缓存+HTTP/2兼容处理。

Go 标准库不提供开箱即用的负载均衡器,但你完全可以用 net/http.RoundTripper 自己写一个——它比在 handler 里随机选后端、或直接调 http.DefaultClient.Do() 更可靠,能复用连接池、走超时/重试逻辑、支持运行时更新后端列表。
为什么不能在 HTTP handler 里写轮询逻辑
常见错误是收到请求后才去选后端、拼 URL、再用 http.DefaultClient.Do() 发出去。这会导致:
- 每次请求都新建 TCP 连接,压根没走
http.Transport的连接复用机制 - 无法继承
Client.Timeout、Client.CheckRedirect等配置 - 健康检查、权重、失败重试都得自己从头实现,且容易阻塞整个 handler goroutine
- 后端挂了还在转发,4xx/5xx 错误率飙升,日志里全是
connection refused或context deadline exceeded
如何正确实现 RoundTrip 轮询(含原子索引与克隆请求)
核心是封装一个结构体,实现 RoundTripper 接口,内部用 atomic.Uint64 维护轮询索引,并确保每次转发前克隆原始 *http.Request:
- 不直接修改原请求的
req.URL和req.Host,否则会影响中间件(如日志、鉴权)对原始 Host 的判断 - 后端地址必须是完整 URL(如
"http://10.0.1.10:8080"),不能只传"10.0.1.10:8080",否则req.URL.Scheme为空,http.Transport会 panic - 克隆后要显式设置
req2.Host = u.Host,否则后端 Nginx 或 Gin 可能因 Host 不匹配返回 404 - 底层 Transport 应复用
&http.Transport{}实例,而非每次都 new 或用http.DefaultTransport(它被全局共享,改了会影响其他模块)
type RoundRobinBalancer struct {
backends []string
index atomic.Uint64
transport http.RoundTripper
}
<p>func (b <em>RoundRobinBalancer) RoundTrip(req </em>http.Request) (*http.Response, error) {
i := b.index.Add(1) % uint64(len(b.backends))
backend := b.backends[i]</p><pre class='brush:php;toolbar:false;'>u, err := url.Parse(backend)
if err != nil {
return nil, err
}
req2 := req.Clone(req.Context())
req2.URL.Scheme = u.Scheme
req2.URL.Host = u.Host
req2.Host = u.Host
return b.transport.RoundTrip(req2)}
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
健康检查必须异步做,不能塞进 RoundTrip
有人把 http.Get("http://.../health") 直接写在 RoundTrip 方法里,结果一节点卡住,整个连接池被 block 住。正确做法是:
- 用
sync.Map缓存后端健康状态:map[string]bool,key 是 backend 地址字符串 - 单独起 goroutine,用
time.Ticker定期探测,每个探测用独立http.Client并设Timeout: 2 * time.Second - 探测失败时,
sync.Map.Store(backend, false);成功则Store(..., true) - 轮询时跳过
Load() == false的节点;若全挂了,返回http.ErrUseLastResponse或自定义错误,让上层决定是否 fallback 到降级逻辑 - 别用 “连续成功 3 次” 恢复节点,而用 “最后一次成功时间 > 30s 前”,避免抖动导致频繁上下线
HTTP/2 和 TLS 兼容性问题常被忽略
当后端是 HTTPS 时,http.Transport 默认启用 HTTP/2,但某些老版 Nginx、OpenResty 或自签名证书服务不兼容,会报错:
http2: server sent GOAWAY and closed the connection
这不是负载均衡逻辑的问题,而是协议协商失败。临时解法是禁用 HTTP/2:
- 给 Transport 加配置:
ForceAttemptHTTP2: false - 或更彻底:设
NextProtos: []string{"http/1.1"} - 注意:禁用 HTTP/2 后,TLS 会话复用仍有效,只是少了多路复用优势
- 长期方案是统一后端 TLS 配置,比如升级 Nginx 到 1.21+,并开启
http2_max_requests和ssl_buffer_size调优
真实生产环境里,最易被绕过的点不是算法,而是 DNS 缓存和健康状态同步延迟——如果后端用域名注册(如 svc-a.default.svc.cluster.local),http.Transport 默认缓存 DNS 解析结果长达 30 分钟,扩缩容后新实例根本收不到流量。

















