用net/http可实现轻量轮询负载均衡器:自定义RoundTripper复用http.Transport,克隆请求并更新URL/Host,配合健康检查(sync.Map缓存状态、定时探测)、HTTP/2/TLS适配及错误处理策略。

用 net/http 实现最简轮询负载均衡器
Go 自带的 http.RoundTripper 和 http.Transport 就能搭出可用的负载均衡器,不用引入第三方库。核心是自己实现一个 RoundTripper,把请求按策略转发给后端节点。
常见错误是直接在 RoundTrip 方法里硬编码 http.DefaultClient.Do() —— 这会绕过连接复用、超时控制和 TLS 复用,导致连接爆炸或证书验证失败。
- 用
&http.Transport{}作为底层传输,复用连接池和 TLS 会话 - 后端地址必须用完整 URL(如
"http://10.0.1.2:8080"),不能只传host:port - 每次转发前要克隆原始
*http.Request,否则修改URL.Host会影响后续中间件或日志 - 别忘了设置
req.Header.Set("Host", backendHost),否则后端可能因 Host 不匹配返回 404
// 示例:简单轮询
type RoundRobinBalancer struct {
backends []string
mu sync.RWMutex
idx int
}
func (b *RoundRobinBalancer) RoundTrip(req *http.Request) (*http.Response, error) {
b.mu.Lock()
backend := b.backends[b.idx%len(b.backends)]
b.idx++
b.mu.Unlock()
<pre class="brush:php;toolbar:false;">u, _ := url.Parse(backend)
req2 := req.Clone(req.Context())
req2.URL.Scheme = u.Scheme
req2.URL.Host = u.Host
req2.Host = u.Host // 关键
return http.DefaultTransport.RoundTrip(req2)}
处理健康检查与故障剔除
真实场景下,后端挂了不能还往死里发请求。Go 没有内置健康检查机制,得自己加定时探测 + 状态缓存。
容易踩的坑是用 time.Tick 做探测 —— 它不防重入,多个 goroutine 可能并发调用 http.Get,压垮监控接口;还有人把健康状态存在 map 里但没加锁,导致读写冲突 panic。
- 用
sync.Map存后端健康状态(map[string]bool),避免锁竞争 - 探测用
http.Client并显式设Timeout: 2 * time.Second,防止卡住整个探测周期 - 剔除故障节点后,轮询索引要重置,否则可能长期跳过有效节点
- 恢复逻辑别用“连续成功 3 次”,而用“最后一次成功时间 > 30s 前”,更稳
HTTP/2 和 TLS 配置对负载均衡的影响
如果后端是 HTTPS,http.Transport 默认会启用 HTTP/2,但某些老版本 Nginx 或自签名服务不兼容,会导致 http2: server sent GOAWAY and closed the connection 错误,请求直接失败。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
另一个典型问题是客户端证书透传 —— 负载均衡器默认不会把原始请求的 TLS 客户端证书信息(比如 ClientHello 中的 SNI 或证书链)转发给后端,导致 mTLS 验证失败。
- 禁用 HTTP/2:给
Transport加ForceAttemptHTTP2: false - 透传客户端证书需手动读取
req.TLS.PeerCertificates,序列化后塞进自定义 header(如X-Client-Cert) - 若后端要求双向 TLS,负载均衡器自身也要配置
TLSClientConfig,否则连不上 - 注意
MaxIdleConnsPerHost要设够,否则高并发下 TLS 握手成为瓶颈
为什么不用 gorilla/reverseproxy?
很多人一上来就抄 gorilla/reverseproxy 示例,结果发现它不支持健康检查、无法动态增删后端、也没有权重或一致性哈希——它本质是个反向代理骨架,不是负载均衡器。
真正要用它,得自己 patch Director 函数做路由决策,再额外起 goroutine 维护后端状态。等你加上熔断、限流、指标暴露,代码量和维护成本已经超过手写一个轻量级 RoundTripper。
-
gorilla/reverseproxy的Director是同步执行的,不适合做耗时健康判断 - 它的
Director无法感知下游响应状态码,没法自动标记节点异常 - 升级 Go 版本后,它对 HTTP/2 流控的处理可能和标准库不一致,出现静默丢包
- 如果你只需要轮询或随机,50 行内自己写个
RoundTripper更可控
负载均衡真正的复杂点不在转发逻辑,而在状态同步、时序控制和错误传播边界——比如一个后端抖动,要不要立刻踢出?踢出后新请求怎么分?这些没法靠库自动解决,得根据业务容忍度来定策略。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










