HTTP/2默认开启会拖垮短连接服务,因其维护多流状态、header table等显著增加GC压力和调度开销;必须在Server级禁用HTTP/2、设置Read/Write/IdleTimeout、池化bufio.Reader并使GOMAXPROCS匹配容器CPU限制。

HTTP/2 默认开启会拖垮短连接服务
Go 的 http.Server 在 TLS 场景下默认启用 HTTP/2,这在长连接、流式响应场景下有益,但在 API 网关、函数计算等短连接高频场景中,反而成为性能毒丸。每个 HTTP/2 连接需维护多个 stream 状态、header table、flow control window,导致 GC 压力陡增、内存碎片化、goroutine 调度延迟上升。
实操建议:
- 显式禁用 HTTP/2:在
http.Server初始化时设置TLSConfig.NextProtos = []string{"http/1.1"} - 不要依赖
http2.ConfigureServer或环境变量关闭——它只影响 server 端协商,不阻止 client 发起 HTTP/2 请求 - 验证是否生效:用
curl -v https://your.host/ 2>&1 | grep "HTTP/2",或抓包看 ALPN 协商结果
ReadTimeout 设在 handler 里根本没用
很多人在 handler 中用 context.WithTimeout 或 time.AfterFunc 想控制读超时,但这是错觉——它只能 cancel 后续逻辑,无法中断底层 conn.Read() syscall。连接卡在内核 recv buffer 等待数据时,goroutine 仍处于 running 状态,持续占用调度器资源。
正确做法必须落在 http.Server 实例上:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
ReadTimeout和WriteTimeout必须设为具体数值(如5 * time.Second),而非零值或未设置 -
IdleTimeout对短连接尤其关键:设为 ≤ 5s,避免大量连接卡在TIME_WAIT状态耗尽端口 - 不要混用
SetReadDeadline:它需每次读前手动调用,易遗漏;ReadTimeout是自动注入的,更可靠
bufio.Reader 不复用等于主动触发 GC 尖峰
每建立一个连接就 new bufio.Reader,等于每秒上千次堆分配。哪怕缓冲区只有 4KB,千级并发下也会让 mallocgc 占用 CPU top 3,P99 延迟毛刺明显。
真正有效的做法是池化 + 预分配:
- 定义全局
sync.Pool,New函数返回bufio.NewReaderSize(conn, 4096)的包装结构体(含 conn 和 buffer) - Accept 后立即调用
conn.SetReadBuffer(64 * 1024),避免内核缓冲区过小导致多次拷贝 - 从 pool 获取 reader 后,用
reader.Reset(conn)复用底层 buffer,而非重新 make([]byte) - 切忌直接复用同一个
bufio.Reader实例处理多个连接——reader 内部状态(如 buffered data、err)不隔离
SO_REUSEPORT 不配 GOMAXPROCS 就是白开
SO_REUSEPORT 能让多个 goroutine(或进程)监听同一端口,缓解 accept 队列争抢,但它只解决“谁来 accept”,不解决“accept 后谁来 handle”。若 GOMAXPROCS 仍等于宿主机 CPU 核数(比如 32),而容器只分配了 4 核,那 32 个 P 会疯狂抢 4 个 M,上下文切换飙升,吞吐不升反降。
必须同步调整:
- 启动时读取
cgroups cpu.max(Linux 5.13+)或/sys/fs/cgroup/cpu/cpu.cfs_quota_us,动态设runtime.GOMAXPROCS(n) - 或直接通过环境变量
GOMAXPROCS=4启动,确保 P 数与容器 CPU limit 一致 -
SO_REUSEPORT需配合net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { syscall.SetsockoptInt(c, syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1) }) }}显式启用
真实瓶颈从来不在 syscall 本身,而在连接生命周期里那些被忽略的内存分配、状态管理与调度适配。越早把 bufio 池化、把 ReadTimeout 放到 server 层、把 GOMAXPROCS 对齐容器限制,越少在压测中途去翻 pprof 火焰图。


















