go本身不提供跨节点负载均衡能力,生产中必须结合服务发现、健康检查、连接复用与路由决策;硬编码轮询或默认reverseproxy均因无故障感知和连接管理而不可靠。

Go 本身不提供开箱即用的跨节点负载均衡能力,http.Server 只做本机请求分发;高并发下的“负载均衡”必须拆解为「服务发现 + 路由决策 + 健康感知 + 连接复用」四个动作,缺一不可。
为什么直接轮询 servers 列表在生产中会挂掉
常见错误是写一个带 index 的轮询结构体,每次 Next() 返回下一个地址——这在节点宕机时完全失效:请求仍会打到已失联的 IP,超时堆积、协程阻塞、下游雪崩。更糟的是,它不感知节点真实负载(CPU、连接数、响应延迟),也不做重试或降级。
- 没健康检查 → 每次调用都可能触发
connection refused或i/o timeout - 没连接池复用 → 每个请求新建 TCP 连接,快速耗尽
ulimit -n - 没上下文超时 → 一个慢节点拖垮整条调用链
- 没原子更新 → 并发读写
index导致错位或 panic
用 httputil.NewSingleHostReverseProxy 做网关层 LB 的硬伤
开发阶段用它快速转发还行,但默认行为对高并发极不友好:
-
Director函数里不改req.Host和req.URL.Scheme,后端收不到正确 Host 头,静态资源或重定向会出错 - 底层
http.Transport默认MaxIdleConnsPerHost = 0,等价于禁用长连接,每请求建新连接 - 无自动重试机制:一次
RoundTrip失败就返回 502,不会 fallback 到其他节点 - 所有节点共用一个
Transport,一个节点网络抖动可能污染整个连接池
必须显式配置:
proxy.Transport = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
}
客户端 LB 必须配合服务注册中心才可控
硬编码 servers = []string{"10.0.1.1:8080", "10.0.1.2:8080"} 是运维灾难。真实场景下,节点随时扩缩、发布、故障,LB 列表必须动态刷新:
- 启动时从
etcd或consul拉取初始列表,监听 key 变更(如watch("services/api/")) - 每个节点暴露
/healthz接口,只检查本地监听和 goroutine 健康,不做 DB 查询 - 后台 goroutine 每 5s 对每个节点发起
HEAD /healthz,失败三次标记为unhealthy - 选择节点时,只从
healthy子集内调度,避免轮询空集合 - 用
atomic.Value替代sync.Mutex缓存当前可用列表,读多写少场景性能差 3~5 倍
最容易被忽略的三个性能断点
不是算法多炫酷,而是这些细节卡住吞吐量:
-
http.Client必须全局复用,绝不能在 handler 里new(http.Client)—— 否则 DNS 缓存失效、连接池隔离、TLS 握手重复 - 所有下游调用必须传
context.WithTimeout(r.Context(), 2*time.Second),否则上游 cancel 不了下游 goroutine - 如果用
gRPC,别直接依赖dns:///resolver,要集成etcdresolver +round_robinbalancer,否则服务发现不生效
真正的高并发 LB 不是“选哪个后端”,而是“什么时候选、选完怎么发、发失败怎么办、选错了谁负责”。这些逻辑一旦散落在业务代码里,很快就会变成救火现场。











