一、前言

如标题go服务运行中容器中,获取CPU参数有误会发生什么?我想大家第一感觉就是想到GMP模型,分析一下:

  1. G多了会发生什么?新创建的G首先放入本地队列,如果本地队列满了,放全局队列。
  2. M多了会发生什么?M空闲的话,就会空闲,会被回收或者睡眠。
  3. P多了会发生什么?P实际与CPU核数是有关联的,如果本身机器64核,容器cgroup对资源进行隔离,最终容器使用的核数可能,比如4核。结果就导致P变多了,就导致运行调度器的物理线程也会变多,从而增加上下文切换的开销,也就是sys cpu变高。

具体影响来自网友真实的环境:

1.1 Uber测试

uber对容器下获取CPU核数进行压测,数据如下:

GOMAXPROCSRPSP50 (ms)P99.9 (ms)
128,893.181.4619.70
2 (equal to quota)44,715.070.8426.38
344,212.930.6630.07
441,071.150.5742.94
833,111.690.4364.32
Default (24)22,191.400.4576.19

当GOMAXPROCS增加到CPU配额以上时,我们看到P50略有下降,但显著增加到P99。我们还看到,处理的RPS(Request per second,每秒请求数)总量也有所下降。

二、如何获取容器下真实的CPU核数?

目前 Go 官方并无好的方式来规避在容器里获取不到真正可使用的核心数这一问题,而 Uber 提出了一种 Workaround 方法,利用 uber-go/automaxprocs 这一个包,可以在运行时根据 cgroup 为容器分配的CPU资源限制数来修改 GOMAXPROCS。

2.1 Uber库

2.2 使用案例

2.3 automaxprocs解决问题

线上容器里的服务通常都对 CPU 资源做了限制,例如默认的 4C。但是在容器里通过 lscpu 仍然能看到宿主机的所有 CPU 核心:

automaxprocs自动识别cgroup为容器分配的cpu限制,来纠正gomaxprcos,保证go服务获取到真实的CPU核数。

2.4 原理

包级别的 init() 函数(代码位置在 automaxpROCs/automaxpROCs.go)实现了导入这个包即可产生作用:

核心函数就是 maxpROCs.Set();这个函数会从当前的 cgroups 里获取设置的 CPU quota,然后转换为合适的 GOMAXPROCS。

接下来获取进程的 cgroup 信息和mountinfo信息,然后通过得到进程对应的 cpu 对应 subsystem 的 CGroup path。得到cGroup的目录路径,我们就得到如下信息:

  1. cfs.cpu_period_us 文件记录了调度周期,单位是 us;默认值一般是 100'000,即 100 ms
  2. cfs.cpu_quota_us 记录了每个调度周期进程允许使用 cpu 的量,单位也是 us。值为 -1 表示无限制;对于 4C 的容器,这个值一般是 400'000

quota 和 period 的比值就是 docker 为容器设置的 CPU 核数配置。这个值也是 automaxpROCs 为 runtime.GOMAXPROCS() 设置的值。

大家感兴趣的可以去看看源码。