runtime.gomaxprocs默认为runtime.numcpu(),但容器中需按cgroup配额(如读取cpu.cfs_quota_us/cpu.cfs_period_us)动态设置;i/o密集型服务设1更稳,cpu密集型建议4–16,盲目调高反增调度开销。

runtime.GOMAXPROCS设多少才不踩坑
它控制的是P(Processor)数量,不是线程数,也不是CPU绑定——设成64不代表能压满64核。默认值是runtime.NumCPU(),但在容器里这值大概率不准,它读的是宿主机核数,不是cgroup限制的可用核数。
常见错误现象:go tool trace里看到大量P长时间idle,但goroutine卡在syscall或chan send/recv;pprof显示runtime.schedule占比异常高。
- HTTP Server、DB客户端这类I/O密集型服务,
runtime.GOMAXPROCS(1)有时更稳——netpoller本就不依赖多P,P多了反而加剧调度竞争 - CPU密集型任务(如图像处理、数值计算),可设为宿主机逻辑核数,但别盲目拉高;实测4–16之间最稳,再高易触发调度器维护开销
- 容器中必须显式设置:读
/sys/fs/cgroup/cpu/cpu.cfs_quota_us和/sys/fs/cgroup/cpu/cpu.cfs_period_us算出可用核数,再调runtime.GOMAXPROCS(n);环境变量GOMAXPROCS=2也行,但别信runtime.NumCPU()
select里加default等于主动制造调度风暴
带default的select在空channel上永远不阻塞,调度器被迫每轮都唤醒goroutine检查就绪状态,结果就是CPU拉满、goroutine疯狂自旋。
这不是bug,是语义使然——你写了“非阻塞”,调度器就得按你说的做。
- 真需要轮询才用
default;否则该阻塞就阻塞,让goroutine进waiting状态,调度器会跳过它 - channel可能长期无数据时,用
time.After配合select做超时退出,比default+time.Sleep省调度资源 - 高频路径上避免
select+default组合,尤其在HTTP handler、消息队列消费者里
hot path上起goroutine是隐形杀手
每次go func() { ... }()都有固定开销:约2KB初始栈 + 调度器登记 + GC元信息。在QPS几千的HTTP handler里每请求起一个goroutine,调度器很快就会被创建/销毁节奏拖垮。
观察runtime.ReadMemStats().NumGoroutine,持续>5k且波动剧烈,基本就是滥用或泄漏。
- 用
semaphore.NewWeighted(10)或带缓冲的channel控并发,别靠“起goroutine”当流量阀 - 闭包捕获大变量会阻止栈收缩,导致内存常驻;能传参就别闭包捕获
- 用
pprof/goroutine?debug=2抓堆栈,确认goroutine是否该结束却没结束
GC频繁会让调度器“失焦”
Go 1.22+的STW已很短,但频繁GC仍干扰调度公平性:mark阶段的mutator assist会让部分goroutine主动让出时间片去帮GC扫描,造成响应毛刺。
GOGC=100是默认值,调低(如50)会更早触发GC,减少单次工作量,但可能增加GC频次——得看分配速率是否真的高。
- 先看
runtime.ReadMemStats().HeapAlloc是否持续增长;不涨就别乱调GOGC - 真正的问题常在对象分配:比如
json.Marshal反复分配临时[]byte,改用预分配的bytes.Buffer或对象池 -
runtime.GC()适合在内存敏感场景手动触发,比如游戏帧循环末尾,但别在HTTP handler里乱用
GOMAXPROCS参数里,但会在go tool trace的“Scheduler latency”和“GC assist”视图里暴露。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











