在容器化场景中会遇到各种语言获取CPU数、内存大小的问题。因为 docker 容器内资源视图隔离未实现,因此会对容器内程序造成一定的误解。比如在一台 40C 64G 的机器上 运行在 2C 4G 的容器。容器内如果没有资源视图隔容器内进程就会误以为自己实际拥有的资源是 40C 64G。但实际上并不是。为此LXCFS出现了。他能解决绝大多数的资源隔离视图的问题。
但 Golang 却存在问题。在 LXCFS 隔离视图后 runtime.NumCPU() 获取的 CPU 数量和实际 lscpu 命令展示的不同。下面简短分析下。
// 示例 1
package main
import "runtime"
func main() {
print(runtime.NumCPU())
}
runtime.NumCPU() 返回 ncpu 数,ncpu 数是个操作系统/平台相关的。
不同操作系统的实现不同,但都是在 osinit 函数内赋值的。
首先以通用Liunx为例看下
func osinit() {
ncpu = getproccount()
physHugePageSize = getHugePageSize()
}
getproccount 函数返回当前进程可以获取的CPU数。
func getproccount() int32 {
// This buffer is huge (8 kB) but we are on the system stack
// and there should be plenty of space (64 kB).
// Also this is a leaf, so we're not holding up the memory for long.
// See golang.org/issue/11823.
// The suggested behavior here is to keep trying with ever-larger
// buffers, but we don't have a dynamic memory allocator at the
// moment, so that's a bit tricky and seems like overkill.
const maxCPUs = 64 * 1024
var buf [maxCPUs / 8]byte
r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
if r < 0 {
return 1
}
n := int32(0)
for _, v := range buf[:r] {
for v != 0 {
n += int32(v & 1)
v >>= 1
}
}
if n == 0 {
n = 1
}
return n
}
代码已经很清晰,通用Linux是通过sched_getaffinity系统调用获取当前执行进程的CPU亲和性获取CPU掩码位的。我们抓取上述示例1的代码的系统调用如下。
root@n22-066-066:~# strace -ff ./test1
... 省略无关代码 ...
arch_prctl(ARCH_SET_FS, 0x4c5650) = 0
sched_getaffinity(0, 8192, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]) = 64
... 省略无关代码 ...
已经很清楚 Golang 在 Linux 上的实现了。NumCPU() 函数只是返回当前进程可以执行的 CPU 数,函数本身是没有问题的。
但 NumCPU 并不能作为程序真实可以运行的 CPU 数量,因为没有考虑到进程的 CFS Quota。
如果 golang 在计算 ncpu 的时候能够把 CPU ONLINE 逻辑也考虑进去这个问题解决起来就很容易了。lxcfs即可解决。因为 CPU ONLINE 是读取 /sys/devices/system/cpu/online
文件。lxcfs 恰好已经处理过这个接口文件。
CPU ONLINE 可以参考Nginx的实现。
// source: nginx-1.16.1/src/os/unix/ngx_posix_init.c line:58
#if (NGX_HAVE_SC_NPROCESSORS_ONLN)
if (ngx_ncpu == 0) {
ngx_ncpu = sysconf(_SC_NPROCESSORS_ONLN);
}
#endif
if (ngx_ncpu < 1) {
ngx_ncpu = 1;
}
但这种修改需要修改 runtime 底层代码。过于暴力。考虑考虑从应用层设置 runtime.GOMAXPROCS()
这个实现方法就很多了。第一反应就是实现一个单文件的库。获取 runtime.NumCPU() 后在获取一下当前进程(/proc/self/cgroup 下 cpuquota)的CPU Quota 值。然后取min值set到 runtime.GOMAXPROCS() 函数中,然后放到main函数第一行引用。这里不考虑 cgroup 的 cpuset 因为他和 sched_getaffinity 是一回事儿。
Uber 已经实现相关逻辑。可以参考这个库:https://github.com/uber-go/automaxprocs