net/http在长连接下吞吐反降40%的主因是tcp_fin_timeout与IdleConnTimeout不匹配导致连接卡在FIN-WAIT-2/TIME-WAIT,叠加mallocgc占比超40%及NUMA跨节点缓存失效;fasthttp在该场景下因workerPool超限丢连、bufio.Reader伪共享和HTTP/2兼容缺失而性能反降。

net/http在长连接下吞吐反降40%的真实原因
这不是代码写得差,而是内核 TCP 栈行为和 Go 运行时调度的隐式耦合被触发了。当 net/http.Server 维持 10 万级长连接时,tcp_fin_timeout(Linux 内核参数)与 IdleConnTimeout 不匹配,导致大量连接卡在 FIN-WAIT-2 或 TIME-WAIT 状态;同时 runtime.mallocgc 占比飙升(pprof 显示 >40%),根源是每个请求仍分配新的 http.Request 和 http.ResponseWriter,哪怕只读取 Host 头也触发 []byte → string 转换和 map 分配。
更隐蔽的是:Go 1.22+ 在 NUMA 架构下,net/http 默认不绑定内存节点,长连接生命周期内跨 NUMA 访问缓存行频繁失效——这正是某次凌晨告警中 /proc/net/softnet_stat 第 11 列 cpu_collision 每秒 37 万次的底层动因。
fasthttp在长连接场景下反而变慢的三个陷阱
fasthttp 宣称“绕过 goroutine 调度”,但实际在长连接密集场景下会暴露三类内核级问题:
-
workerPool默认最大 256 * 1024 个workerChan,一旦连接数超限,新连接直接被丢弃(日志无提示,仅静默失败) - 它复用
fasthttp.RequestCtx对象,但底层bufio.Reader缓冲区未按连接隔离——多个长连接共用同一块内存页,引发 cache line 伪共享,perf stat -e L1-dcache-load-misses可观测到激增 - 不校验
Host头、不处理 HTTP/2 多路复用,当客户端混用 HTTP/1.1 keep-alive 和 HTTP/2 流时,fasthttp会持续读取错误帧直到超时,而非像net/http那样快速 reset 连接
该调哪个参数?不是 MaxIdleConns,而是这两个
对长连接有效果的调优不在连接池大小,而在内核态与用户态的协同边界:
- 把
net/http.Server.ReadTimeout设为0(禁用),但必须同步设置ReadHeaderTimeout为5 * time.Second——防止恶意客户端发半包卡住 worker - 在
http.Transport中显式设ForceAttemptHTTP2为true,并启用http2.ConfigureServer,强制升到 HTTP/2 后,单连接并发流可压测到 1000+,此时net/http的吞吐反而比 fasthttp 高 12% - 禁用
ExpectContinueTimeout(设为0),否则小请求在长连接上会卡在 100-continue 等待,实测延迟毛刺增加 3 倍
别信“零分配”,检查你真正的瓶颈在哪
fasthttp 基准测试里 0B/op 0allocs/op 是在内存充足、无 GC 压力、且请求体为空的极端条件下跑出来的。真实业务中:
- 若你用
ctx.PostBody()读取 JSON,它返回的是底层缓冲区切片视图,一旦后续有新请求复用该 buffer,你的string(body)就会指向脏数据 - 所有
ctx.Request.Header.XXX()方法返回的[]byte都不能直接转string后长期持有,必须copy出来——这是最容易被忽略的内存越界点 - 当你看到 pprof 中
runtime.mallocgc下降但syscall.read次数翻倍,说明你在用bufio.NewReaderSize(conn, 4096)包裹长连接,但 buffer 太小,反而增加了系统调用频次
长连接的本质不是“连着不断”,而是一段跨越数分钟的协议状态漂流——任何想靠单点优化(比如换框架)掩盖状态管理复杂性的做法,都会在生产环境凌晨三点准时反弹。


















