本文为从零开始写 Docker 系列第十九篇,添加对 cgroup v2 的支持。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
推荐阅读以下文章对 docker 基本实现有一个大致认识:
开发环境如下:
1
2
3
4
5
6
7
8
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 用户
1. 概述
本篇主要添加对 cgroup v2 的支持,自动识别当前系统 cgroup 版本。
2. 实现
判断 cgroup 版本
通过下面这条命令来查看当前系统使用的 Cgroups V1 还是 V2
1
stat -fc %T /sys/fs/cgroup/
如果输出是cgroup2fs
那就是 V2,就像这样
1
2
root@tezn:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs
如果输出是tmpfs
那就是 V1,就像这样
1
2
[ root@docker cgroup] # stat -fc %T /sys/fs/cgroup/
tmpfs
Go 实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const (
unifiedMountpoint = "/sys/fs/cgroup"
)
var (
isUnifiedOnce sync . Once
isUnified bool
)
// IsCgroup2UnifiedMode returns whether we are running in cgroup v2 unified mode.
func IsCgroup2UnifiedMode () bool {
isUnifiedOnce . Do ( func () {
var st unix . Statfs_t
err := unix . Statfs ( unifiedMountpoint , & st )
if err != nil && os . IsNotExist ( err ) {
// For rootless containers, sweep it under the rug.
isUnified = false
return
}
isUnified = st . Type == unix . CGROUP2_SUPER_MAGIC
})
return isUnified
}
cgroup v2 支持
使用 cgroup v2 过程和 v1 基本一致
1)创建子 cgroup 2)配置 cpu、memory 等 Subsystem 3)配置需要限制的进程 创建子 cgroup
创建子 cgroup,则是在 cgroup 根目录下创建子目录即可,对 cgroup v2 来说,根目录就是 /sys/fs/cgroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const UnifiedMountpoint = "/sys/fs/cgroup"
// getCgroupPath 找到cgroup在文件系统中的绝对路径
/*
实际就是将根目录和cgroup名称拼接成一个路径。
如果指定了自动创建,就先检测一下是否存在,如果对应的目录不存在,则说明cgroup不存在,这里就给创建一个
*/
func getCgroupPath ( cgroupPath string , autoCreate bool ) ( string , error ) {
// 不需要自动创建就直接返回
cgroupRoot := UnifiedMountpoint
absPath := path . Join ( cgroupRoot , cgroupPath )
if ! autoCreate {
return absPath , nil
}
// 指定自动创建时才判断是否存在
_ , err := os . Stat ( absPath )
// 只有不存在才创建
if err != nil && os . IsNotExist ( err ) {
err = os . Mkdir ( absPath , constant . Perm0755 )
return absPath , err
}
return absPath , errors . Wrap ( err , "create cgroup" )
}
配置 Subsystem
以 cpu 为例,只需要在 cpu.max 中添加具体限制即可,就像这样:
1
echo 5000 10000 > cpu.max
含义是在10000的CPU时间周期内,有5000是分配给本cgroup的,也就是本cgroup管理的进程在单核CPU上的使用率不会超过50%
具体实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func ( s * CpuSubSystem ) Set ( cgroupPath string , res * resource . ResourceConfig ) error {
if res . CpuCfsQuota == 0 {
return nil
}
subCgroupPath , err := getCgroupPath ( cgroupPath , true )
if err != nil {
return err
}
// cpu.cfs_period_us & cpu.cfs_quota_us 控制的是CPU使用时间,单位是微秒,比如每1秒钟,这个进程只能使用200ms,相当于只能用20%的CPU
// v2 中直接将 cpu.cfs_period_us & cpu.cfs_quota_us 统一记录到 cpu.max 中,比如 5000 10000 这样就是限制使用 50% cpu
if res . CpuCfsQuota != 0 {
// cpu.cfs_quota_us 则根据用户传递的参数来控制,比如参数为20,就是限制为20%CPU,所以把cpu.cfs_quota_us设置为cpu.cfs_period_us的20%就行
// 这里只是简单的计算了下,并没有处理一些特殊情况,比如负数什么的
if err = os . WriteFile ( path . Join ( subCgroupPath , "cpu.max" ), [] byte ( fmt . Sprintf ( "%s %s" , strconv . Itoa ( PeriodDefault / Percent * res . CpuCfsQuota ), PeriodDefault )), constant . Perm0644 ); err != nil {
return fmt . Errorf ( "set cgroup cpu share fail %v" , err )
}
}
return nil
}
配置需要限制的进程
只需要将 pid 写入 cgroup.procs 即可
1
echo 1033 > cgroup.procs
Go 实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func ( s * CpuSubSystem ) Apply ( cgroupPath string , pid int ) error {
return applyCgroup ( pid , cgroupPath )
}
func applyCgroup ( pid int , cgroupPath string ) error {
subCgroupPath , err := getCgroupPath ( cgroupPath , true )
if err != nil {
return errors . Wrapf ( err , "get cgroup %s" , cgroupPath )
}
if err = os . WriteFile ( path . Join ( subCgroupPath , "cgroup.procs" ), [] byte ( strconv . Itoa ( pid )),
constant . Perm0644 ); err != nil {
return fmt . Errorf ( "set cgroup proc fail %v" , err )
}
return nil
}
移除
删除 cgroup 下的子目录即可移除
1
2
3
4
5
6
7
func ( s * CpuSubSystem ) Remove ( cgroupPath string ) error {
subCgroupPath , err := getCgroupPath ( cgroupPath , false )
if err != nil {
return err
}
return os . RemoveAll ( subCgroupPath )
}
兼容V1和V2
只需要在创建 CgroupManager 时判断当前系统 cgroup 版本即可
1
2
3
4
5
6
7
8
func NewCgroupManager( path string) CgroupManager {
if IsCgroup2UnifiedMode() {
log.Infof( "use cgroup v2" )
return NewCgroupManagerV2( path)
}
log.Infof( "use cgroup v1" )
return NewCgroupManagerV1( path)
}
3. 测试
cgroup v1
到 cgroup v1 环境进行测试
1
2
3
4
5
6
7
8
9
10
11
root@mydocker:~/mydocker# ./mydocker run -mem 10m -cpu 10 -it -name cgroupv1 busybox /bin/sh
{ "level" :"info" ,"msg" :"createTty true" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"resConf:\u0026{10m 10 }" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"lower:/var/lib/mydocker/overlay2/3845479957/lower image.tar:/var/lib/mydocker/image/busybox.tar" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/3845479957/lower,upperdir=/var/lib/mydocker/overlay2/3845479957/upper,workdir=/var/lib/mydocker/overlay2/3845479957/work /var/lib/mydocker/overlay2/3845479957/merged]" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"use cgroup v1" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"error" ,"msg" :"apply subsystem:cpuset err:set cgroup proc fail write /sys/fs/cgroup/cpuset/mydocker-cgroup/tasks: no space left on device" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"command all is /bin/sh" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"init come on" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"Current location is /var/lib/mydocker/overlay2/3845479957/merged" ,"time" :"2024-04-14T13:23:19+08:00" }
{ "level" :"info" ,"msg" :"Find path /bin/sh" ,"time" :"2024-04-14T13:23:19+08:00" }
根据日志可知,当前使用的时 cgroup v1
1
{ "level" :"info" ,"msg" :"use cgroup v1" ,"time" :"2024-04-14T13:23:19+08:00" }
执行以下命令测试memory分配
可以看到,过会就被 OOM Kill 了
1
2
/ # yes > /dev/null
Killed
执行以下命令 跑满 cpu
1
while : ; do : ; done &
确实被限制到 10%了
1
2
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1212 root 20 0 1332 68 4 R 9.9 0.0 0:02.30 sh
cgroup v2
到 cgroup v2 环境进行测试,或者参考以下步骤切换到 v2 版本。
切换到 cgroup v2
你还可以通过修改内核 cmdline 引导参数在你的 Linux 发行版上手动启用 cgroup v2。
如果你的发行版使用 GRUB,则应在 /etc/default/grub
下的 GRUB_CMDLINE_LINUX
中添加 systemd.unified_cgroup_hierarchy=1
, 然后执行 sudo update-grub
。
编辑 grub 配置
内容大概是这样的:
1
2
3
4
5
6
GRUB_DEFAULT = 0
GRUB_TIMEOUT_STYLE = hidden
GRUB_TIMEOUT = 0
GRUB_DISTRIBUTOR = ` lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT = "quiet splash"
GRUB_CMDLINE_LINUX = ""
对最后一行GRUB_CMDLINE_LINUX
进行修改
1
GRUB_CMDLINE_LINUX = "quiet splash systemd.unified_cgroup_hierarchy=1"
然后执行以下命令更新 GRUB 配置
最后查看一下启动参数,确认配置修改上了
1
cat /boot/grub/grub.cfg | grep "systemd.unified_cgroup_hierarchy=1"
然后就是重启
重启后查看,不出意外切换到 cgroups v2 了
1
2
root@cgroupv2:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs
测试
1
./mydocker run -mem 10m -cpu 10 -it -name cgroupv2 busybox /bin/sh
1
2
3
4
5
6
7
8
9
10
root@mydocker:~/mydocker# ./mydocker run -mem 10m -cpu 10 -it -name cgroupv2 busybox /bin/sh
{ "level" :"info" ,"msg" :"createTty true" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"resConf:\u0026{10m 10 }" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"lower:/var/lib/mydocker/overlay2/3526930704/lower image.tar:/var/lib/mydocker/image/busybox.tar" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/3526930704/lower,upperdir=/var/lib/mydocker/overlay2/3526930704/upper,workdir=/var/lib/mydocker/overlay2/3526930704/work /var/lib/mydocker/overlay2/3526930704/merged]" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"use cgroup v2" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"init come on" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"command all is /bin/sh" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"Current location is /var/lib/mydocker/overlay2/3526930704/merged" ,"time" :"2024-04-14T13:26:32+08:00" }
{ "level" :"info" ,"msg" :"Find path /bin/sh" ,"time" :"2024-04-14T13:26:32+08:00" }
根据日志可知,当前使用的时 cgroup v2
1
{ "level" :"info" ,"msg" :"use cgroup v2" ,"time" :"2024-04-14T13:26:32+08:00" }
执行同样的测试,效果一致,说明 cgroup v2 使用正常。
执行以下命令测试memory分配
可以看到,过会就被 OOM Kill 了
1
2
/ # yes > /dev/null
Killed
执行以下命令 跑满 cpu
1
while : ; do : ; done &
确实被限制到 10%了
1
2
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1212 root 20 0 1332 68 4 R 9.9 0.0 0:02.30 sh
4. 小结
本文主要为 mydocker 添加了 cgroup v2 的支持,根据系统 cgroup 版本自适应切换。
完整代码见:https://github.com/lixd/mydocker
欢迎关注~
相关代码见 feat-cgroup-v2
分支,测试脚本如下:
需要提前在 /var/lib/mydocker/image 目录准备好 busybox.tar 文件,具体见第四篇第二节。
1
2
3
4
5
6
7
8
# 克隆代码
git clone -b feat-cgroup-v2 https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试
./mydocker run -mem 10m -cpu 10 -it -name cgroupv2 busybox /bin/sh