直接用net/http拼代理易出错,因默认不透传请求体、不复用连接、不处理Upgrade头,导致POST体为空、WebSocket 400、长连接频繁重建;须显式关闭Body、配置Transport参数、手动拷贝头字段并特殊处理CONNECT/Upgrade/gRPC等协议细节。

为什么直接用 net/http 拼代理容易出错
Go 的 net/http 默认不透传请求体、不复用连接、不处理 Upgrade 头(比如 WebSocket),直接写 http.ServeHTTP 转发会丢数据或卡死。常见现象是 POST 请求体为空、长连接频繁重建、WebSocket 连接 400。
- 必须显式调用
req.Body.Close(),否则连接无法复用 -
http.Transport要设置MaxIdleConnsPerHost和IdleConnTimeout,否则默认只维持 2 个空闲连接 - 转发前需手动拷贝
Content-Length和Transfer-Encoding,否则后端可能读不到完整 body - 对
CONNECT方法(HTTPS 代理)和Upgrade请求,要单独判断并透传,不能走普通RoundTrip
用 httputil.NewSingleHostReverseProxy 快速起步但得改什么
它省了路由和基础转发逻辑,但开箱即用会挂:默认不透传 Host 头、不支持 HTTP/2 后端、不处理超时。实际部署必须重写 Director 和替换 Transport。
-
Director函数里必须设req.URL.Host = "backend:8080",且清空req.URL.Scheme(否则可能拼出http://backend:8080/http://...) - 替换默认
Transport:启用ForceAttemptHTTP2: true,设置MaxIdleConns: 1000,否则压测时连接数暴涨 - 禁用
Proxy字段(避免走系统代理),加DialContext控制 DNS 解析超时 - 对
Upgrade请求,要在Director后手动复制Connection和Upgrade头,否则 WebSocket 断连
如何让代理支持 gRPC over HTTP/2
gRPC 客户端默认走 HTTP/2,但 Go 的 httputil.ReverseProxy 不识别 h2c(HTTP/2 cleartext),也不透传 te: trailers 头,直接转发会返回 404 或 502。
- 后端 URL 必须用
h2c://协议(而非http://),否则http.Transport会降级为 HTTP/1.1 - 在
Director中清除req.Header["Te"]并设req.Header.Set("Te", "trailers") - 启用
http2.ConfigureTransport包装Transport,否则即使后端支持 h2,客户端也可能协商失败 - gRPC 健康检查路径(
/healthz)需单独路由,避免被代理规则拦截
性能瓶颈常卡在哪儿
实测中 QPS 上不去,90% 是连接池或 GC 问题,不是 CPU 瓶颈。比如 1k 并发下 runtime.mprof 显示大量 net/http.(*persistConn).readLoop goroutine 阻塞,说明连接没及时释放。
立即学习“go语言免费学习笔记(深入)”;
-
Transport.IdleConnTimeout设太长(如 90s)会导致连接堆积,建议 30s;KeepAlive设 30s,避免 TCP 层探测延迟 - 避免在
Director中做 JSON 解析或 DB 查询——每个请求都执行,会拖慢整个代理链 - 日志打太多(尤其
req.Header全量打印)会触发大量内存分配,用zap.Stringer替代fmt.Printf - 如果用了中间件(如 JWT 验证),确保
context.WithTimeout作用于整个 handler,否则超时后 goroutine 仍运行
真正难的不是转发逻辑,是连接生命周期和协议细节的平衡——比如一个 Upgrade 请求漏掉一个头,整条 WebSocket 就断;一个 Content-Length 没同步,gRPC 流就卡住。这些地方没法靠框架自动兜底。



















