runtime.NumCPU() 返回宿主机核数是正常现象,因其在Linux上调用sched_getaffinity仅感知cpuset绑定,不读取cgroup的--cpus配额,故即使docker run --cpus=1.5仍返回32等宿主机逻辑CPU总数。

容器里 runtime.NumCPU() 返回宿主机核数是正常现象
Go 程序启动时默认用 runtime.NumCPU() 初始化 GOMAXPROCS,而这个函数在 Linux 上调用 sched_getaffinity,只感知 CPU 绑定(--cpuset-cpus),不读 cgroup 的 --cpus 配额。所以即使你 docker run --cpus=1.5,runtime.NumCPU() 仍返回宿主机逻辑 CPU 总数(比如 32),不是 1 或 2。
这不是 bug,是设计使然:Go 运行时没有义务自动适配容器调度限制。后果很直接——P 数远超实际可用核,goroutine 在受限核上激烈争抢,docker stats 显示 CPU% 接近 150%,但吞吐卡住,pprof 里满屏 runtime.mcall 和 semacquire 等待。
验证方式很简单:
- 进容器执行
go env GOMAXPROCS,看初始值 - 对比
cat /sys/fs/cgroup/cpu.max(cgroup v2)或cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us(v1) - 再跑一句
go run -e 'package main; import "runtime"; func main() { println(runtime.NumCPU()) }'
手动设 GOMAXPROCS 后没生效?先查有没有被覆盖
GOMAXPROCS 是运行时可变的,且无只读保护。很多问题不是“设不了”,而是“刚设完就被第三方库或自己代码覆盖了”。常见覆盖点:
- 第三方 SDK(如 OpenTelemetry agent、Prometheus metrics collector)在
init()阶段偷偷调用runtime.GOMAXPROCS - 项目里某处
import _ "some/pkg/that/calls/gomaxprocs" - Dockerfile 中用
ENV GOMAXPROCS=4,但代码里又写了runtime.GOMAXPROCS(2)—— 后者胜出
最简单验证方法:在 main() 开头加一行 fmt.Println("actual GOMAXPROCS:", runtime.GOMAXPROCS(0))。如果输出不是你预期的值,说明已被覆盖。
从 cgroup 读配额算整数核数,别信小数
--cpus=1.5 对应 cgroup v2 的 cpu.max = "150000 100000",v1 是 cpu.cfs_quota_us = 150000 + cpu.cfs_period_us = 100000。直接除得 1.5,但 GOMAXPROCS 必须是整数 —— 设 1.5 会向下取整为 1,但更关键的是:小数 P 值会引发调度抖动,Go 官方明确建议设整数上限。
计算逻辑应为:
- 若
cfs_quota_us == -1(不限制),fallback 到runtime.NumCPU() - 否则:
quota / period→ 向下取整 → 至少为 1 - Alpine 镜像注意路径:cgroup v2 下优先读
/sys/fs/cgroup/cpu.max,不是旧路径
示例片段(不依赖第三方):
if quota, err := os.ReadFile("/sys/fs/cgroup/cpu.max"); err == nil {
fields := strings.Fields(string(quota))
if len(fields) == 2 && fields[0] != "max" {
if q, _ := strconv.ParseInt(fields[0], 10, 64); q > 0 {
if p, _ := strconv.ParseInt(fields[1], 10, 64); p > 0 {
n := int(q / p)
if n < 1 {
n = 1
}
runtime.GOMAXPROCS(n)
}
}
}
}
用 uber-go/automaxprocs 是最快落地方案
自己解析 cgroup 虽可控,但要处理 v1/v2 差异、挂载点查找、错误 fallback,容易漏边角 case。Uber 的 automaxprocs 就是为此而生:导入即生效,init 阶段自动探测并调用 runtime.GOMAXPROCS。
使用只要两步:
go get -u go.uber.org/automaxprocs- 在
main.go的 imports 里加一行:import _ "go.uber.org/automaxprocs"
它内部做了三件事:找对 cgroup mount point、读 cpu.max 或 cpu.cfs_quota_us、向下取整后设值。Go 1.19+ 原生支持 cgroup v2,旧版本也能兼容。唯一要注意:它必须在 main() 执行前完成,所以不能放在子包或条件 import 里。
复杂点在于 Kubernetes Downward API 注入的 resources.limits.cpu 是字符串(如 "1500m"),而 cgroup 文件给的是微秒级数值,二者单位不同——automaxprocs 不走 Downward API,它只信 cgroup fs,这点反而更可靠。


















