必须禁用http/2并在server级设置readtimeout和writetimeout,因http/2默认开启会为每个连接维护多路复用流状态,高并发短连接场景下显著增加gc压力和goroutine调度开销。

http.Server 必须禁用 HTTP/2 并设超时在 Server 级
HTTP/2 默认开启会为每个连接维护多路复用流状态,在高并发短连接场景(如 API 网关)下显著抬高 GC 压力和调度开销。它不是“更先进”,而是“更重”——尤其当你的服务平均连接生命周期
ReadTimeout 和 WriteTimeout 设在 handler 里(比如用 context.WithTimeout)完全无效:它们只能 cancel 后续 Go 代码执行,无法中断底层 conn.Read() syscall。必须直接设在 http.Server 实例上:
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 5 * time.Second, // 防止 TIME_WAIT 积压
TLSConfig: &tls.Config{
NextProtos: []string{"http/1.1"}, // 强制降级
},
}
-
IdleTimeout ≤ 5s对短连接服务是硬性建议;长连接可放宽,但需配合连接池管理 - 若用 TLS,
NextProtos必须显式指定["http/1.1"],否则 Go 1.21+ 仍可能协商出 HTTP/2 - 不要依赖
http.DefaultServeMux或未配置的http.ListenAndServe,它们默认启用 HTTP/2 且无超时
bufio.Reader 必须池化,且 buffer 大小要固定
每次新连接都 bufio.NewReader(conn) 是性能杀手:内部 []byte 缓冲区会逃逸到堆,高频创建触发 GC 尖峰。更糟的是,默认 4KB 缓冲在千级并发下极易被内核 socket 缓冲区耗尽,导致频繁拷贝。
正确做法是预分配 + sync.Pool 复用:
var readerPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 4096)
return bufio.NewReaderSize(bytes.NewReader(buf), 4096)
},
}
<p>// Accept 后
conn, _ := listener.Accept()
conn.SetReadBuffer(64 <em> 1024) // 调整内核接收缓冲区
conn.SetWriteBuffer(64 </em> 1024)</p><p>reader := readerPool.Get().(*bufio.Reader)
reader.Reset(conn) // 注意:不是 NewReaderSize(conn, size),Reset 才复用底层 buffer</p>
-
Reset()是关键,它把新conn绑定到已有bufio.Reader实例,避免 new 分配 - 缓冲区大小选 4KB–64KB,取决于典型请求体大小;超过 64KB 可能浪费内存,且对大多数 API 无收益
- 不要在 handler 内反复调
reader.Read()时传不同长度的[]byte—— 预分配固定大小 slice 并复用
SO_REUSEPORT 有用,但只解决 accept 队列争抢
Linux 3.9+ 的 SO_REUSEPORT 允许多个 Go 进程(或同一进程多个 listener)绑定同一端口,内核按 CPU 核心分发连接,缓解单 listener 的锁竞争。但它不解决 goroutine 调度倾斜问题——你仍可能有 80% 的连接落到少数几个 P 上。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
启用方式:
lc := net.ListenConfig{
Control: func(fd uintptr) error {
return syscall.SetsockoptInt32(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
},
}
listener, _ := lc.Listen(context.Background(), "tcp", ":8080")
- 必须搭配
runtime.GOMAXPROCS设置为 CPU 核心数,否则多 listener 无意义 - 单进程多 listener 比多进程更易管理,但需确保每个 listener 启动独立 goroutine 调用
Accept - 若用容器部署,确认宿主机内核 ≥ 3.9 且未被安全策略禁用
SO_REUSEPORT
永远用 SetReadDeadline 替代 time.AfterFunc
阻塞在 conn.Read() 上的 goroutine 不会自动释放,哪怕 context 已 cancel。Go 的 netpoll 机制只响应 deadline,不响应 context signal。
错误写法:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
go func() { defer cancel() }() // 无效!不能中断 read syscall
n, err := conn.Read(buf)
正确写法(每次读前设):
err := conn.SetReadDeadline(time.Now().Add(5 * time.Second))
if err != nil {
return err
}
n, err := conn.Read(buf)
if err == net.ErrDeadlineExceeded {
// 超时,关闭连接或重试
return err
}
- 不要在连接建立后只设一次 deadline——HTTP/1.1 keep-alive 下后续读仍需重设
- WriteDeadline 同理,尤其在流式响应或大文件传输中
- 如果用
io.Copy,它内部不处理 deadline,必须包装成带 deadline 的io.Reader
实际中最容易被忽略的点是:所有优化的前提是连接生命周期可控。如果你的服务大量存在空闲长连接、客户端不发 FIN、或中间代理(如 Nginx)悄悄复用连接却不透传 timeout,再好的缓冲池和 SO_REUSEPORT 也救不了。先用 netstat -s | grep -i "tcp.*time" 看 TIME_WAIT 和 FIN 状态分布,再谈优化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










