go的http.server默认卡住一个cpu核,因其运行时仅有一个全局网络轮询器(netpoller)绑定单个os线程执行epoll_wait,所有连接事件均由其统一分发,无法随gomaxprocs扩展。

为什么 net/http.Server 默认会卡住一个 CPU 核
Go 的 http.Server 在 Linux 下默认只用一个 OS 线程执行 epoll_wait,哪怕你设了 runtime.GOMAXPROCS(16) 也无效——因为 Go 运行时只有一个全局网络轮询器(netpoller),所有连接的事件都由它统一分发。这不是 bug,是设计取舍:降低调度开销,但高吞吐场景下容易成为瓶颈。
常见现象是:top 显示单核 100%,其余核心空闲;strace -p $PID -e epoll_wait 只看到一个线程在调用;并发连接上万后延迟陡增。
- 别对
http.Server.Serve启动多个 goroutine,它们共享同一个 listener,不会增加 poller 实例 - 别用
runtime.LockOSThread()绑定 goroutine 到线程,这会让网络回调无法被调度,反而阻塞 - 真正起作用的是进程维度的横向扩展,不是 goroutine 数量
SO_REUSEPORT 必须配 GOMAXPROCS 和 NUMA 绑定
SO_REUSEPORT 是 Linux 3.9+ 提供的机制,允许多个进程(或线程)监听同一端口,内核按负载把新连接分发到不同 socket 队列。但它不等于自动多核并行——若不控制进程与 CPU 的关系,可能跨 NUMA 节点访问内存,延迟翻倍。
实操建议:
- 每个 Go 进程启动前用
numactl --cpunodebind=0 --membind=0 ./server绑定到专属 NUMA 节点 - 进程数建议 = 物理 CPU 核心数(非超线程数),避免上下文切换开销
- 在
net.ListenConfig中启用:Control: func(fd uintptr) { syscall.SetsockoptInt32(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1) } - 前端必须配负载均衡(如 nginx 的
ip_hash或 LVS),否则连接分布不均
禁用 HTTP/2 + 正确设置 Timeout 才能压测出真实性能
HTTP/2 默认开启,它为每个连接维护流状态、HPACK 压缩上下文和优先级树,在短连接 API 场景下 GC 压力激增,且 context.WithTimeout 放在 handler 里根本不能中断底层 read syscall。
Linux系统管理专家,覆盖12大模块:用户权限、SSH、存储、网络、systemd、防火墙、日志监控、备份恢复、TLS证书、Ansible、容器、IaC。提供配置、验证、加固、监控、备份、自动化、故障排查、回滚闭环。关键词:useradd、sudo、sshd_config、chmod、SEL...
关键配置必须写在 http.Server 初始化时:
- 禁用 HTTP/2:
TLSConfig: &tls.Config{NextProtos: []string{"http/1.1"}}(即使没 TLS,也要设这个字段) -
ReadTimeout和WriteTimeout必须设在 server 实例上,它们会直接作用于conn.Read()/conn.Write() - 短连接服务加
IdleTimeout: 5 * time.Second,防TIME_WAIT积压 - 长连接服务别设
ReadTimeout,改用conn.SetDeadline()按需控制
bufio.Reader 复用比调大 socket buffer 更有效
盲目调 conn.SetReadBuffer(1024*1024) 会导致内核缓冲区浪费,尤其千级连接时,256KB × 1000 = 256MB 内存白占;而用户层复用 bufio.Reader 能把每次 ReadString 的临时 []byte 分配压到接近零。
正确姿势:
- Accept 后立刻设 socket 缓冲:
conn.SetReadBuffer(64 * 1024) - 用
sync.Pool管理bufio.Reader和底层 buffer:var readerPool = sync.Pool{New: func() interface{} { return bufio.NewReaderSize(bytes.NewReader(nil), 4096) }} - 每次从池取 reader 后调
reader.Reset(conn),别 new 新实例 - buffer 大小选 4KB 或 8KB,太小频繁 syscalls,太大局部性差
最易被忽略的是:超大并发下,问题往往不出在 Go 代码逻辑,而出在连接生命周期管理——比如忘了关连接、没设 IdleTimeout、复用 bufio.Reader 时没 reset 到新 conn,这些都会让 fd 和内存缓慢泄漏,压测跑着跑着就 OOM 或卡死。










