用 net/http 可实现轻量轮询负载均衡器:后端抽象为独立 http.roundtripper,atomic.uint64 管理索引,初始化健康检查,避免取模倾斜,配置 idleconntimeout 与 maxidleconnsperhost 防 fd 耗尽,并用 time.afterfunc + 状态标记实现简易熔断。

用 net/http 实现轮询负载均衡器,不依赖第三方库
Go 标准库完全够用,核心是把后端服务抽象为可调度的 http.RoundTripper,再套一层请求分发逻辑。不需要引入 gorilla/mux 或 gin,纯 net/http + sync/atomic 就能跑起来。
关键点在于:别直接用 http.DefaultTransport,每个后端应持有独立的 &http.Transport{},避免连接复用干扰;轮询索引用 atomic.Uint64,比加锁更轻量。
- 后端列表初始化时做健康检查(比如发 HEAD 请求),剔除不可达地址
- 轮询不是简单取模:
atomic.AddUint64(&idx, 1) % uint64(len(backends)),否则高并发下会倾斜 - 务必设置
Transport.IdleConnTimeout和Transport.MaxIdleConnsPerHost,否则长连接堆积导致 fd 耗尽
处理后端故障:超时、重试与熔断的朴素实现
真实场景里,一个后端挂了不能卡住整个请求。Go 没有内置熔断器,但可以用 time.AfterFunc + 状态标记模拟简易熔断——连续 3 次失败且间隔
-
http.Client的Timeout只控制整次请求,不够细;建议拆成Transport.DialContext(建连)、ExpectContinueTimeout(等待 100-continue)、ResponseHeaderTimeout(等响应头)三级超时 - 重试只对幂等方法(
GET、HEAD、OPTIONS)生效,且最多 1 次;重试前必须req.Body.Close()并重新req.Body = io.NopCloser(bytes.NewReader(...)),否则第二次读不到 body - 别用
context.WithTimeout包整个 handler,会导致 cancel 波及后端 transport 连接池
并发安全的 backend 列表热更新
运维可能随时增删后端,但不能停机 reload。标准做法是用 sync.RWMutex 包裹 backend 切片,但要注意:读多写少场景下,频繁写锁会阻塞所有请求。
立即学习“go语言免费学习笔记(深入)”;
- 更优解是用原子指针替换整个切片:
atomic.StorePointer(&backendsPtr, unsafe.Pointer(&newBackends)),读时backends := (*[]*backend)(atomic.LoadPointer(&backendsPtr)) - 更新前先深拷贝原列表,做健康探测,确认新列表可用后再原子切换
- 避免在 HTTP handler 里调用
os/exec或同步读配置文件,IO 阻塞会拖垮并发能力
为什么不用 httputil.NewSingleHostReverseProxy?
它方便,但隐藏太多细节:默认不透传 X-Forwarded-For,修改请求头要重写 Director 函数,错误处理是 panic 打印日志,无法接入自定义指标。生产环境需要明确知道每个环节谁负责超时、谁负责重试、谁记录失败原因。
- 自己构造
http.Request并调用backendTransport.RoundTrip(req),能精确控制 header 处理、body 复制、status code 映射 - 反向代理的
FlushInterval默认 0,流式响应(如 SSE)会卡住;手动 flush 必须判断response.Header.Get("Content-Type")是否含text/event-stream - 如果后端返回
Connection: close,你得主动关掉 client 连接,否则 transport 会误以为还能复用
最易被忽略的是 connection 复用边界:同一个 http.Transport 实例不能混用 HTTP/1.1 和 HTTP/2 后端,TLS 握手参数冲突会导致静默失败。要么按协议拆 transport,要么统一升级到 h2。










