4

容器环境下 Go 如何获取 CPU 核数

 7 months ago
source link: https://liqiang.io/post/how-go-detect-cpu-core-in-container-11ae1498
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近在看一个库的实现代码时,发现它依赖于查看进程使用的 CPU 核数,然后根据 CPU 核数做一些限制,所以我就顺便看了一下 Go 是如何实现查看 CPU 核心数的。

Golang 实现



  1. [[email protected]]# cat main.go
  2. runtime.NumCPU()
  3. --> runtime/debug.go return int(ncpu)

这里返回了一个全局变量,这个全局变量的初始化是在进程起来的时候设置的,所以注释中也解释了,一旦进程运行之后,如果更改了 CPU 亲和性的配置也不会生效:



  1. [[email protected]]# cat runtime/os_linux.go
  2. func osinit() {
  3. ncpu = getproccount()
  4. ---> func getproccount() int32 {
  5. r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
  6. ... ...
  7. n := int32(0)
  8. for _, v := range buf[:r] {
  9. for v != 0 {
  10. n += int32(v & 1)
  11. v >>= 1
  12. }
  13. }
  14. if n == 0 {
  15. n = 1
  16. }
  17. return n

这里的 sched_getaffinity 不是一个简单的 Go 函数,而是一个系统调用,所以需要从汇编代码中看,但是也很简单,就是将函数入栈,然后调用系统调用:



  1. [[email protected]]# cat runtime/sys_linux_amd64.s
  2. TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0
  3. MOVQ pid+0(FP), DI
  4. MOVQ len+8(FP), SI
  5. MOVQ buf+16(FP), DX
  6. MOVL $SYS_sched_getaffinity, AX
  7. SYSCALL
  8. MOVL AX, ret+24(FP)
  9. RET

sched_getaffinity 系统调用

从 linux 的 man page(https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html) 中可以看到,这个系统调用是返回 cpuset 的信息,通过掩码的方式返回。实际上返回的 cpuset 就是一个位图,如果一个 bit 是 1,那么表示这个 cpu 是可用的,如果是 0 则表示不可用,所以我们从 Go 的代码中可以看到,它遍历返回的 buffer,然后逐位地检查(这个其实有个高效的 CPU 指令可以完成),最终计算当前进程可以使用的 CPU 核数是多少。

我们用系统命令也可以查看容器可以使用的 CPU 核心数:



  1. [[email protected]]# lscpu | egrep -i 'core.*:|socket'
  2. Thread(s) per core: 1
  3. Core(s) per socket: 1
  4. Socket(s): 4

一些简单的小坑

在使用 Docker 的时候,我曾经想限制 CPU 的个数(限制 Docker 容器的 CPU 内存等资源,发现 Docker 有两种不同的选项:

  • 简单:限制 CPU 核数,这个很好理解
  • 复杂:基于 CPU 时间片的限制,docker 是基于 CFS 的调度实现,这是老版本使用的,新版本都推荐使用简单的方式
    • 使用方式:[[email protected]]# docker run --cpu-period=100000 --cpu-quota=200000 表示每个 CPU 使用的时间是 100 ms,这个容器最多使用 200ms(相当于限制了 2 个核,但是不是绝对)

对于第二种限制 CPU 时间片的方式,在容器里面实际上看到的还是所有的 CPU 核心,所以 Go 也会认为他有那么多核心数,这个可能是一个坑需要注意。

一些系统底层知识

cgroups v2 限制 cpuset

cpuset 可以通过 cgroups 简单地限制,例如我这里使用 cgroupv2 为例进行:



  1. [[email protected]]# sudo cgcreate -g cpuset:/liqiang2
  2. [[email protected]]# sudo cgexec -g cpuset:/liqiang2 go run /tmp/main.go
  3. 16
  4. [[email protected]]# echo "0-1" | sudo tee /sys/fs/cgroup/liqiang2/cpuset.cpus
  5. [[email protected]]# sudo cgexec -g cpuset:/liqiang2 go run /tmp/main.go
  6. 2

ok,这里我们知道了,Go 是通过 cpuset 来获取可用的 CPU 核数的,那么 cpuset 是什么?为什么会存在这个东西?我们在理解容器的概念之后,很自然地会认为容器之间资源隔离不是一个很正常的功能吗?这没错,但是实际上 cpuset 在容器流行之前就存在了,再次之前,它常被用于 NUMA 的环境,在一些高性能的服务器中,一个主机上其实包含不只有一个 CPU,你很平常就会看到两个 CPU 的机器,例如我这一台:



  1. [[email protected]]# lscpu | grep -i numa
  2. NUMA node(s): 2
  3. NUMA node0 CPU(s): 0-23,48-71
  4. NUMA node1 CPU(s): 24-47,72-95

那么我们知道 CPU 和内存是通过北桥高速通信线路通信的,那么如何存在多个 CPU 的话,这个线路要如何设计?事实上,在 NUMA 架构中,CPU 和内存是有一个远近关系的,每个 CPU 都会有一些本地内存比较快,对于其他 CPU 的本地内存,访问速度会慢一些,于是,为了程序的运行效率,我们就会有绑定 CPU 和内存的需求了,我们可以将进程绑定在固定的 CPU 上,并且内存也使用对应的一些,这样可以保证我们的进程不会跨 CPU,这样的话就可以去除 NUMA 架构的影响了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK