用net/http实现轮询分发器应避免魔改roundtripper,而用原子计数器选后端、复用全局client、重写host和url、清空requesturi、健康检查发head请求、流式转发请求体并关闭body。

用 net/http 实现轮询分发器,别碰 http.RoundTripper 一开始
Go 标准库的 http.Client 默认不支持后端列表切换,直接写个简单轮询逻辑比魔改 RoundTripper 更快、更可控。真实场景里你大概率只需要在请求前选一个后端地址,而不是全程接管连接复用。
- 先定义后端列表:
[]string{"http://10.0.1.10:8080", "http://10.0.1.11:8080"},别存成*http.Client切片——每个 client 都带独立连接池,反而干扰复用 - 用原子计数器做无锁轮询:
atomic.AddUint64(&counter, 1) % uint64(len(backends)),避免加锁和 map 查找开销 - 别在 handler 里每次 new
http.Client,复用一个全局http.Client,并设好Timeout和Transport.MaxIdleConnsPerHost
转发请求时必须重写 Host 头和 URL,否则后端收不到正确路径
Go 的 httputil.NewSingleHostReverseProxy 看似省事,但它默认把原始 Host 头透传过去,而你的负载均衡器是中间层,后端服务通常依赖 Host 做路由或 TLS SNI 匹配。直接用它容易出现 404 或证书错误。
- 手动构造
*http.Request:用req.URL.Path和req.URL.RawQuery拼出目标 URL,别用req.URL.String()—— 它可能含原始 host 和 scheme - 显式设置
req.Host = backendHost(比如"10.0.1.10:8080"),否则后端看到的是你 LB 的域名 - 清空
req.RequestURI(设为""),否则 net/http 在写入底层连接时会优先用它,导致路径错乱
健康检查不能只 ping 端口,得发真实 HTTP 请求并设超时
只用 net.DialTimeout 检查端口通不通,会漏掉后端进程僵死、HTTP server 卡住但 TCP 连接仍存活的情况。真实故障中,「端口开着但返回不了 HTTP 响应」非常常见。
- 用单独 goroutine 轮询,每 10 秒对每个后端发一次
HEAD /health,Client.Timeout设为 2 秒以内 - 失败连续 3 次才标记为 down,避免瞬时抖动误判;恢复需连续 2 次成功才重新加入轮询池
- 状态存在
map[string]bool里没问题,但读写要加sync.RWMutex—— 检查协程写,分发逻辑只读,别全用sync.Mutex
别让请求体读取阻塞整个 handler,req.Body 必须用 io.Copy 流式转发
如果用 io.ReadAll(req.Body) 把整个请求体读进内存再转发,大文件上传或长流式请求(如 SSE)会吃光内存,还可能触发 http: request body too large 错误。
立即学习“go语言免费学习笔记(深入)”;
- 用
io.Copy直接从req.Body写到下游http.Post的请求体,或用req.Body作为http.NewRequest的 body 参数(注意:此时不能重复读) - 务必调用
req.Body.Close(),否则连接不会归还给连接池,短时间内就耗尽MaxIdleConnsPerHost - 如果需要修改请求头(比如加
X-Forwarded-For),在io.Copy前做,别等 copy 完再补——body 已被消费完
最麻烦的不是轮询逻辑,而是健康状态和请求体生命周期的耦合:一个后端刚被标为 down,但已有几个请求正在往它身上转发,这些请求得自然完成,不能强行中断。所以状态切换和请求分发之间要有明确的“快照”边界,比如用原子指针切换整个后端列表切片,而不是边遍历边过滤。










