直接用 net/http 的 roundtrip 就够了,因其接口天然适合负载均衡,只需实现 roundtrip 方法并控制后端选择、重试与健康检查,无需第三方框架或修改服务端逻辑。

为什么直接用 net/http 的 RoundTrip 就够了
Go 标准库的 http.RoundTripper 接口天然适合做负载均衡——它只负责把 *http.Request 发出去、拿到 *http.Response,中间怎么选后端、怎么重试、怎么健康检查,全由你控制。不需要引入第三方框架,也不用改写整个 HTTP 服务栈。
关键点在于:别去动 http.ServeHTTP 或 http.Handler,那是处理「进来的请求」;负载均衡器是「发出去的请求」代理,核心逻辑在客户端侧。
-
http.Transport是默认的RoundTripper,但它不支持动态后端列表,得自己实现 - 必须实现
RoundTrip(*http.Request) (*http.Response, error)方法,且不能阻塞(比如同步健康检查) - 所有后端地址建议预解析为
*url.URL,避免每次请求都调用url.Parse
如何实现轮询(Round Robin)策略并避开挂掉的节点
轮询是最容易上手的策略,但真实场景下必须跳过不可用节点,否则用户会直面超时或连接拒绝错误。
做法不是「维护一个可用列表」,而是「每次选节点时实时探测 + 有限重试」:缓存失败时间窗口(比如 30 秒),期间跳过该节点;探测用轻量级 HEAD 请求或 TCP 连接测试,不要等完整 HTTP 响应。
- 用
sync.Map存每个后端的lastFailureTime time.Time,比加锁读写更轻量 - 轮询索引用原子操作更新(
atomic.AddUint64(&idx, 1)),避免多 goroutine 竞态 - 如果所有节点都在冷却期,就 fallback 到第一个节点并强制尝试——总比直接返回 503 好
- 别在
RoundTrip里做 DNS 刷新,后端 IP 变更应由外部信号(如 SIGHUP)触发重新加载
如何让 http.Client 正确复用连接并设置超时
负载均衡器如果每次请求都新建 TCP 连接,吞吐会断崖下跌。必须复用 http.Transport 并精细控制超时链路。
注意三个超时不是叠加关系,而是分层生效: DialTimeout 控制建连,ResponseHeaderTimeout 控制响应头到达,Timeout 是总时限(含重定向)。负载均衡场景下,通常要缩短前两者,放宽总时限以容纳重试。
- 设置
transport.MaxIdleConnsPerHost = 100(默认 2),不然并发高时连接池立刻打满 -
transport.IdleConnTimeout = 30 * time.Second,太长会占着连接不放,太短频繁重建 - 别把
client.Timeout设成 5 秒就完事——后端可能已故障,应配合重试逻辑,在RoundTrip内部做最多 2 次换节点重试 - 记得禁用
transport.TLSClientConfig.InsecureSkipVerify = true仅用于测试,生产必须校验证书
为什么健康检查不能放在 RoundTrip 里同步执行
同步健康检查会让每个用户请求卡住几百毫秒,尤其当后端批量宕机时,整个负载均衡器会雪崩式变慢。健康检查必须异步、低频、带退避。
典型做法是起一个单独 goroutine,按后端数量分片轮询(比如每 5 秒扫一遍所有节点,每次只查 1/10),失败后指数退避(1s → 2s → 4s),恢复后清空冷却时间。
- 不要用
time.Ticker直接遍历全部后端——节点数一多,单次扫描就超时 - 健康检查请求必须设独立的超时(如
500 * time.Millisecond),和业务请求隔离 - 检查失败时只更新
sync.Map中的时间戳,不 panic、不 log 每次失败(用计数器 + 定期汇总告警) - 如果后端支持 /health 端点,优先用 HTTP GET;否则用
net.DialTimeout测 TCP 连通性
最常被忽略的是:负载均衡器自身没有“启动就绪”状态。上线前没等健康检查跑完第一轮,就可能把流量导到全挂的后端。上线脚本里得加个简单等待逻辑,比如轮询 sync.Map 确认至少一个节点标记为可用再开 listen。











