本文是 Docker核心技术 系列文章:Docker原理之Cgroups,其他文章快捷链接如下:
- 应用架构演进
- 容器技术要解决哪些问题
- Docker的基本使用
- Docker是如何实现的
- Docker核心技术:Docker原理之Namespace
- Docker核心技术:Docker原理之Cgroups(本文)
- Docker核心技术:Docker原理之Union文件系统
4.4.Cgroups:资源管控
4.4.1.Cgroups是什么
-
Cgroups是谷歌Borg系统内部做资源管控的,后来将之提供到了Linux Kernel中
-
Linux内核中,Cgroups实现
- 在一个task中,还有另外一个属性 cgroups
4.4.2.Cgroups工作机制
-
Cgroups实际上就是一个文件系统,在Linux的
/sys/fs/cgroup
下 -
Cgroup本身又有很多的子系统,包括cpu子系统
/sys/fs/cgroup/cpu
、memory子系统/sys/fs/cgroup/memory
等等,每个子系统中又包括各种配置文件,配置子系统的参数-
比如cpu子系统中,包含多个配置文件,负责控制cpu绝对值、cpu相对值、cpu limit等等
-
如果想要控制一个进程的cpu绝对值,就修改cgroup目录下相应的文件,就可以立即做到cpu的限额
-
-
如果是容器,每个容器都有自己的cgroup
-
【🌟】cgroups的结构:树形结构
- /sys/fs/cgroup
- cpu子系统
- 当前子系统的cpu配置文件
- 每个目录:又是关联到cpu子系统 的 子系统,目录内部又会包含当前子系统的cpu配置文件
- memory子系统
- ...
- cpu子系统
- /sys/fs/cgroup
4.4.3.Cgroups包含哪些子系统
4.4.4.Cgroups CPU子系统
- cpu子系统下,有很多配置,常用的配置文件如下:
- cpu.shares:设置cpu的相对值
- 假设一个主机有3个CPU,创建了2个cgroup
- Cgroup1 中 cpu.shares 写512,cgroup2 中 cpu.shares写1024
- 意味着,这两个cgroup,可以按照
512: 1024=1: 2
的比例分摊cpu的时间,即Cgroup1 会分到1个CPU、Cgroup2 会分到2个CPU - 不过1:2只是一个相对值,如果Cgroup2 中压根没有进程,则 Cgroup1中的进程,就可以使用超过1个CPU
- cpu.cfs_period_us、:cpu.cfs_quota_us:相互配合,设置cpu的绝对值
- cpu.cfs_period_us:控制cpu时间周期的长度,默认是10万,100000
- cpu.cfs_quota_us:控制当前cgroup能拿到一个cpu时间周期的多少。
- 默认是-1,即不限制当前cgroups下进程可使用的cpu绝对值
- 可如果你设置 cpu.cfs_period_us为 10万,cpu.cfs_quota_us为 1万,则表示 当前cgroup中的进程,一共可以得到
1万/10万=0.1个CPU
- 这是一个绝对值,当前cgroup下的 所有进程 占用cpu的总时间,不可超过0.1个CPU时间
- cpuacct.usage:进行cgroup及其子cgroup下的,cpu使用情况的统计分析,可用做监控
- cpu.shares:设置cpu的相对值
4.4.5.拓展:Linux中多个进程如何共享cpu时间片
4.4.5.1.Linux内核的调度器
- Linux Kernel 2.6.23 以后,默认的是CFS(Completely Fair Scheduler)完全公平调度器
4.4.5.2.CFS调度器
4.4.5.3.vruntime红黑树
- 进程初始化时,为进程初始化它的vruntime值,插入树中,最小值放在左边,最大值放右边
- 每次进程调度,都拿最左边的那个进程去运行。
- 每次调度之后,时钟周期会为被调度进程重新计算一遍vruntime,公式:vruntime=实际运行时间*1024/进程权重
- 进程权重相当于cpu_share的那个比例值
- 被调度的进程,vruntime会一直涨;未被调度的进程,vruntime就不会变
- 当被调度的进程,vruntime不再是最小的了,红黑树结构就会发生变化,将下次应该调度的进程放在最左边
- 按照这样的模式,就可以 按照进程权重的设置,为每个进程分配 公平的cpu时间
4.4.5.4.CFS进程调度在Linux Kernel中的实现
- 感兴趣的可以深入
4.4.6.Cgroup Memory子系统
-
memory子系统下,有很多配置,常用的配置文件如下:
- memory.soft_limit_in_bytes:内存软限制
- memory.limit_in_bytes:内存硬限制,进程使用超过就会OOM
- memory.oom_control :当发生OOM时,对进程执行什么操作,默认是 kill 进程。memory.oom_control中默认包含3个值:
- oom_kill_disable 0:该参数表示是否禁用 OOM 杀死进程的功能。如果值为 0,则表示未禁用,即允许内核在 OOM 事件发生时杀死进程以释放内存。如果值为 1,则表示已禁用,即内核不会杀死进程
- under_oom 0:该参数表示是否处于 OOM 状态。如果值为 0,则表示系统当前未处于 OOM 状态。如果值为 1,则表示系统当前处于 OOM 状态
- oom_kill 0:该参数表示是否已经执行了 OOM 杀死进程的操作。如果值为 0,则表示尚未执行 OOM 杀死进程的操作。如果值为 1,则表示已经执行了 OOM 杀死进程的操作
4.4.7.Cgroup CPU 子系统练习
- 练习:使用Cgroups控制进程的资源开销
-
进入主机的 /sys/fs/cgroup/cpu目录下,mkdir cpudemo
- 相当于我们创建了一个新的cgroup,并将其与 CPU 子系统相关联
- linux会自动为cpudemo目录中,创建cpu的各种配置文件
-
准备一个死循环的程序,用来占用cpu资源
- 该程序会吃掉两个cpu
gopackage main func main(){ // 协程1:死循环会吃掉一个cpu go func(){ for { } }() // 主协程:死循环也会吃掉一个cpu for { } }
-
将上面的程序写入一个main.go,然后build一下
sh[root@VM-226-235-tencentos ~/zgy/code/test_cpu]# go build main.go [root@VM-226-235-tencentos ~/zgy/code/test_cpu]# ls main main.go
-
在终端前台执行一下这个程序,终端会卡住,因为是死循环,没有任何输出
-
另外打开一个终端,使用top命令查看现在的cpu使用情况
- 可以看到,main程序果然吃掉了2个CPU
- main程序的pid为:19963
-
现在我们去cpudemo cgroup下,为这个进程设置CPU限额
sh[root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cd /sys/fs/cgroup/cpu/cpudemo # 将main程序的pid,写入cgroup.procs,表示将main进程加入这个cgroup控制 [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 19963 > cgroup.procs [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cgroup.procs 19963 # 使用绝对值控制cpu使用额度,cpu.cfs_period_us默认是10万,向cpu.cfs_quota_us写入1万,则main进程的cpu额度将会被限制在0.1个CPU [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 10000 > cpu.cfs_quota_us [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cpu.cfs_period_us 100000 [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cpu.cfs_quota_us 10000
-
再次top查看 main进程的cpu使用,果然被限制在0.1附近
-
可以用同样的方法,将cpu.cfs_quota_us值改成1.5万,则cpu将会被限制在0.15个
-
删除子系统
-
注意,直接使用rm是删不掉cgroup子系统的
-
需要先安装cgroup-tools的工具,然后执行cgdelete命令,才可以删除子系统
sh# ubuntu下用apt,centos下用yum apt install cgroup-tools cd /sys/fs/cgroup/cpu cgdelete cpu:cpudemo
-
4.4.8.Cgroup Memory子系统练习
-
进入主机的 /sys/fs/cgroup/memory目录下,mkdir memorydemo
- 相当于我们创建了一个新的cgroup,并将其与 Memory 子系统相关联
- linux会自动为memorydemo目录中,创建memory的各种配置文件
-
准备一个死循环的程序,用来占用memory资源。
-
程序效果:
-
该程序每隔1分钟就申请100MB内存,并将其填充为字符 'A'。共申请10次,最多申请1000MB。
-
可以通过 watch 命令,定期执行给定的命令并显示其输出,监视内存使用情况
shwatch 'ps -aux | grep malloc | grep -v grep'
watch
命令:watch
是一个 Linux 命令,用于定期执行给定的命令并显示其输出。ps aux
命令:ps
是一个用于查看当前运行进程的命令。aux
选项用于显示所有用户的所有进程,并以紧凑的格式显示进程的信息。grep malloc
命令:grep
是一个用于在文本中搜索指定模式的命令。在这里,它用于过滤出包含 "malloc" 字符串的行,即查找正在运行的进程中使用malloc
函数进行内存分配的进程。grep -v grep
命令:-v
选项用于反向匹配,即排除包含 "grep" 字符串的行。这是为了避免在结果中显示grep
命令本身的进程。
-
然后我们通过修改cgroup下的配置文件,将该进程的内存使用量,限制在100MB,超过时会OOM。
-
-
创建一个main.go文件
gopackage main //#cgo LDFLAGS: //char* allocMemory(); import "C" import ( "fmt" "time" ) func main() { // only loop 10 times to avoid exhausting the host memory holder := []*C.char{} for i := 1; i <= 10; i++ { fmt.Printf("Allocating %dMb memory, raw memory is %d\n", i*100, i*100*1024*1025) // hold the memory, otherwise it will be freed by GC holder = append(holder, (*C.char)(C.allocMemory())) time.Sleep(time.Minute) } }
-
创建一个 malloc.c 文件
c#include <stdlib.h> #include <stdio.h> #include <string.h> #define BLOCK_SIZE (100*1024*1024) char* allocMemory() { char* out = (char*)malloc(BLOCK_SIZE); memset(out, 'A', BLOCK_SIZE); return out; }
-
创建一个Makefile
makefilebuild: CGO_ENABLED=1 GOOS=linux CGO_LDFLAGS="-static" go build
-
-
程序写完,make build一下
sh[root@VM-226-235-tencentos ~/zgy/code/test_mem]# go mod init example.com/memory-test [root@VM-226-235-tencentos ~/zgy/code/test_mem]# go mod tidy [root@VM-226-235-tencentos ~/zgy/code/test_mem]# make build [root@VM-226-235-tencentos ~/zgy/code/test_mem]# ls go.mod main.go Makefile malloc.c memory-test
-
在终端前台执行一下这个程序,终端会卡住,因为进程每次循环都会sleep 1min
-
另外打开一个终端,使用
watch 'ps -aux | grep malloc | grep -v grep'
命令查看现在的 malloc 内部的进程情况- 可以看到,有一个进程memory-test,果然malloc了100MB的内存
- memory-test进程的pid为:13433
-
现在我们去memorydemo cgroup下,为这个进程设置Memory限额
sh[root@VM-226-235-tencentos /sys/fs/cgroup/memory/memorydemo]# cd /sys/fs/cgroup/memory/memorydemo # 将memory-test进程的pid,写入cgroup.procs,表示将 memory-test 进程加入这个cgroup控制 [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 13433 > cgroup.procs [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat cgroup.procs 13433 # 向 memory.limit_in_bytes 写入 104960000,大约是100MB [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# echo 104960000 > memory.limit_in_bytes [root@VM-226-235-tencentos /sys/fs/cgroup/cpu/cpudemo]# cat memory.limit_in_bytes 104960000
-
在程序执行的终端可以看到,进程已经被kill掉了,因为申请内存超过了100MB,被执行OOM了。默认的OOM策略就是kill process
4.4.9.kubernetes Pod的cgroup怎么查看
- 方法一:进入pod内部查看
- 进入pod内部,在 /sys/fs/cgroup下面,可以看到当前pod的所有 cgroup设置
- 比如 /sys/fs/cgroup/cpu 中,
cpu.cfs_period_us、cpu.cfs_quota_us
就以绝对值的方式,设置了pod的 cpu limit - 再比如,在 /sys/fs/cgroup/memory 中,
memory.limit_in_bytes
就设置了pod的 memory limit
- 方法二:在pod所在主机上查看
- 进入主机的 /sys/fs/cgroup,每一个子系统中,都有一个kubepods的目录,里面存储着所有pod的cgroup
- kubepods里面,还有一个burstable目录,这就是所有pod cgroup的存储位置
- 比如,查看一个pod的cpu cgroup,就应该进入:
/sys/fs/cgroup/cpu/kubepods/burstable/podxxxx-xxxx/
- 其中 podxxxx-xxxx 表示pod的uid,可以使用kubectl get pods podxxx -oyaml找到
4.4.10.kubelet启动时,报错cgroup driver不一致
-
因此,启动kubelet时,有时会报错:cgroup driver不一致,kubelet启动失败。原因如下:
- kubelet默认使用 systemd 作为 cgroup driver
- 如果你本地还装了docker,且docker默认使用的是 cgroupfs 作为 cgroup driver。
- 二者cgroup driver不同,kubelet出于保护,会禁止启动
-
解决方法:将docker的cgroup driver改成和kubelet一样,比如将docker的cgroup driver改成systemd,操作如下: