用net/http可实现生产级轻量负载均衡器:需自定义Transport设连接限制与超时,轮询用原子计数器,HTTPS后端显式配置tls.Config,健康检查用Ticker控制频率并设独立超时,避免使用gorilla/reverseproxy。

用 net/http 实现最简轮询负载均衡器
Go 自带的 net/http 足够写一个生产可用的轻量级负载均衡器,不需要额外框架。核心是把请求代理到后端,再把响应原样转回客户端。
常见错误是直接用 http.DefaultTransport 而不设置超时和连接复用限制,导致后端挂掉时连接堆积、goroutine 泄漏。
- 必须自定义
http.Transport:设置MaxIdleConns、MaxIdleConnsPerHost和IdleConnTimeout - 每个后端应封装为独立的
*http.Client或共享同一Transport,避免共用全局默认实例 - 轮询逻辑建议用原子计数器(
atomic.AddUint64)而非锁,减少争用 - 示例片段:
var counter uint64 backends := []string{"http://10.0.1.10:8080", "http://10.0.1.11:8080"} idx := atomic.AddUint64(&counter, 1) % uint64(len(backends)) target := backends[idx]
处理 HTTP/2 和 TLS 后端时的证书验证陷阱
当后端地址是 https:// 且启用 HTTP/2 时,Go 默认会校验证书。如果后端用自签名证书或私有 CA,不显式配置就会报 x509: certificate signed by unknown authority。
这不是“关掉验证”就能解决的问题——关了会埋下中间人风险;硬加根证书又容易在容器或跨环境部署时失效。
立即学习“go语言免费学习笔记(深入)”;
- 推荐做法:用
tls.Config{RootCAs: certPool}显式加载可信 CA,而不是设InsecureSkipVerify: true - 若后端域名与证书 SAN 不匹配(比如用 IP 直连),需同时设置
ServerName字段,否则 TLS 握手失败 - HTTP/2 要求后端支持 ALPN,老版本 Nginx 或未开启
http2的服务会静默降级到 HTTP/1.1,但不会报错——得靠抓包或日志确认实际协议
健康检查怎么做才不拖垮后端
主动探活是负载均衡器的关键能力,但高频、无节制的健康检查请求反而会让脆弱后端雪崩。
Go 标准库没有内置健康检查调度器,得自己控制节奏和失败阈值,不能依赖第三方定时器随意发请求。
- 用
time.Ticker控制检查频率(如 10 秒一次),避免用time.AfterFunc嵌套递归调用 - 每次检查必须设独立超时(
context.WithTimeout),建议 ≤ 2s,防止 goroutine 卡住 - 连续失败 3 次才标记下线,恢复需单独成功 2 次才重新加入——避免抖动导致频繁切换
- 检查路径别用
/healthz这类可能触发完整业务链路的接口,优先用/ping或 TCP 连通性探测
为什么不用 gorilla/reverseproxy?
很多人第一反应是抄 gorilla/reverseproxy 示例,但它在 Go 1.22+ 中已明显过时:不支持 HTTP/2 正向代理、header 处理逻辑僵硬、无法细粒度控制重试和超时。
它适合快速原型,不适合长期维护的 LB 组件。真正可控的方式是用 http.RoundTripper + http.Handler 自行组装。
- 标准
httputil.NewSingleHostReverseProxy生成的 proxy 无法修改 request header 中的Host字段(除非重写Director),而很多后端依赖正确 Host 路由 - 它的错误处理是 panic 式的(如后端不可达时直接返回 502 而不记录日志),不利于排查
- 如果你需要基于 header 或 path 做分流(比如灰度路由),必须绕过它,直接操作
*http.Request
真正难的不是转发逻辑,而是状态同步、优雅下线、指标暴露这些周边——它们没出现在任何“教程”里,但线上出问题时第一个找上门。


















