
haproxy 默认无法在客户端复用长连接时实现请求级负载均衡;根本原因是 http/1.1 keep-alive 使 tcp 连接长期保持,导致所有请求被路由至同一后端。可通过服务端主动关闭连接、客户端禁用复用,或 haproxy 配置 `http-server-close` 等策略强制按请求分发。
在 Go 构建的长连接场景(如基于 go-martini 的 Web 服务 + 自定义 Go 客户端)中,HAProxy 出现“只打到一个后端”的现象,并非配置错误或 HAProxy 故障,而是 HTTP/1.1 协议默认行为与负载均衡逻辑之间的天然冲突。curl 表现正常,是因为其默认每次请求后主动关闭连接(Connection: close),而你的 Go 客户端和服务器均启用了 Keep-Alive,形成持久 TCP 连接 —— HAProxy 在该连接生命周期内会将所有后续请求转发至首次选定的后端服务器,造成“会话粘滞”(session stickiness),违背了轮询/随机等负载均衡意图。
✅ 推荐解决方案:HAProxy 配置 http-server-close
这是最优雅、侵入性最小的方案。它让 HAProxy 对客户端保持连接复用(提升性能),但对每个后端服务器独立建立并关闭连接,从而确保每条 HTTP 请求都能被重新负载均衡:
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance roundrobin
http-server-close # ← 关键配置:断开与后端的连接,但保留与客户端的连接
server srv1 192.168.1.10:8080 check
server srv2 192.168.1.11:8080 check
server srv3 192.168.1.12:8080 check? http-server-close 是 HAProxy 1.5+ 的标准选项,适用于绝大多数 REST API 和微服务场景。它等效于在每个响应头中自动注入 Connection: close 给后端,并在响应完成后立即关闭与后端的连接,而客户端连接不受影响。
⚙️ 其他可行方案(按推荐度排序)
-
Go 客户端侧控制(显式关闭连接)
在发起请求前设置 Request.Close = true,强制使用 HTTP/1.0 或添加 Connection: close 头:req, _ := http.NewRequest("GET", "http://haproxy/api", nil) req.Close = true // ← 关键:禁用客户端连接复用 client.Do(req)✅ 优点:无需修改 HAProxy;❌ 缺点:牺牲连接复用带来的性能收益,增加 TCP 握手开销。
Go 服务端侧控制(关闭响应连接)
在 handler 中设置 ResponseWriter.Header().Set("Connection", "close") 并调用 w.(http.Flusher).Flush()(若适用),但更推荐统一由 HAProxy 控制,避免业务代码耦合网络策略。❌ 不推荐 http-close:它会同时关闭客户端和后端连接,彻底丧失连接复用优势,仅适用于极低频、调试场景。
? 验证是否生效
部署后,通过以下方式确认负载均衡已恢复:
- 使用 haproxy -vv 检查版本支持(需 ≥ 1.5);
- 查看 HAProxy 日志(启用 option httplog)观察 srv1/srv2/srv3 是否交替出现;
- 抓包验证:tcpdump -i any port 8080 应看到客户端→HAProxy 连接长期存在,而 HAProxy→各后端的连接呈短连接、高频新建状态。
? 总结
| 方案 | 是否修改代码 | 是否影响性能 | 推荐指数 |
|---|---|---|---|
| http-server-close(HAProxy) | 否 | 否(客户端仍复用) | ⭐⭐⭐⭐⭐ |
| req.Close = true(Go 客户端) | 是 | 是(每次新建 TCP) | ⭐⭐⭐ |
| Connection: close 响应头(Go 服务端) | 是 | 是(同上) | ⭐⭐ |
核心原则:让负载均衡决策发生在“请求级别”,而非“连接级别”。 优先通过反向代理层(HAProxy)解耦连接管理与业务逻辑,既保障可维护性,又兼顾性能与可靠性。










