gomaxprocs 必须与容器 cpu 配额对齐,否则会因 p 过多引发调度抖动;单进程无法分散 epoll_wait,网络 i/o 天然受限于全局单轮询器,仅 cpu 密集型任务可通过合理分块真正并行压满多核。

GOMAXPROCS 必须设对,但仅设对还不够——Go 单进程无法让 epoll_wait 分散到多个线程,网络 I/O 层天然单点瓶颈;CPU 密集任务才能真正并行压满多核,且必须按核拆分、避免小粒度调度开销。
为什么 runtime.GOMAXPROCS(0) 返回 1 就危险
它说明 Go 运行时只被允许用 1 个逻辑处理器(P),所有 goroutine 都挤在同一个 OS 线程上跑。哪怕机器有 64 核,也只用得上 1 个。
- 检查方式:
fmt.Println(runtime.GOMAXPROCS(0))—— 若输出 1,立刻干预 - Docker 容器中常见:cgroups 限制了可用 CPU,
runtime.NumCPU()仍返回宿主机核数,需读/sys/fs/cgroup/cpu/cpu.cfs_quota_us手动校准 - 环境变量优先级高于代码:启动前设
GOMAXPROCS=8比在main()里调用更早生效 - 设完别再改:多次调用
runtime.GOMAXPROCS(n)可能触发运行时重调度抖动
CPU 密集任务怎么分才不白忙
开 100 个 goroutine 算斐波那契,实际只有 GOMAXPROCS 个在真跑,其余排队等 P。调度开销反超收益。
- 按物理核心数切块:8 核就分 8 份,每份工作量要够大(比如 >10ms 计算),避免频繁抢占
- 别用无缓冲 channel 汇总结果:100 个 goroutine 往同一个
chan int写会锁竞争,改用sync.WaitGroup+ 全局 slice 或atomic.AddInt64 -
runtime.LockOSThread()在纯计算中基本没用,还可能卡死(比如误调net.Conn.Read) - 验证是否真并行:用
go tool pprof -http=:8080 cpu.pprof看flat时间是否均匀分布,而非只盯 goroutine 数
为什么 http.Server.Serve 多开 goroutine 不能提升吞吐
你写了 16 个 go srv.Serve(l),但底层 epoll_wait 始终由一个 OS 线程执行——这是 Go 运行时设计使然,不是 bug。
- 所有
net.Listener共享全局网络轮询器(netpoller),它绑定在 M0 线程上 -
runtime.LockOSThread()对此无效:它锁的是 goroutine 和 M 的绑定,但轮询器线程是全局唯一的,无法“复制” - 第二个
srv.Serve()调用会立即返回http: Server closed错误(监听器已被占用),若没检查err就忽略,会误以为“起了多个服务” - 真实高吞吐场景(如 10GbE)下,单 poller 成为瓶颈,CPU 利用率必然集中在单核
单进程内还能做点什么补救
不能绕过单 poller,但可以减少它成为瓶颈的频率和影响范围。
- 把耗时长的业务逻辑(JSON 序列化、模板渲染、加解密)从
http.HandlerFunc中剥离,用 goroutine 异步处理,让轮询器线程尽快回到epoll_wait - 对连接池、TLS 握手等关键路径启用
sync.Pool,降低 GC STW 时间 —— STW 期间所有 P 暂停,间接削弱并行效率 - 用
net/http.Server.ReadTimeout/WriteTimeout防止慢连接长期占着轮询器线程 - NUMA 敏感场景下,用
numactl --cpunodebind=0 --membind=0 ./server启动,避免跨节点内存访问拖慢 poller 回调
GOMAXPROCS + 合理分块,网络 I/O 靠进程级水平扩展。后者没法在单进程中 hack 出来——这不是技巧问题,是架构边界。











