从零开始写 Docker(十九)---增加 cgroup v2 支持

本文为从零开始写 Docker 系列第十九篇,添加对 cgroup v2 的支持。


完整代码见:https://github.com/lixd/mydocker

欢迎 Star

推荐阅读以下文章对 docker 基本实现有一个大致认识:


开发环境如下:

bash 复制代码
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

Bash 复制代码
stat -fc %T /sys/fs/cgroup/

如果输出是cgroup2fs 那就是 V2,就像这样

Bash 复制代码
root@tezn:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs

如果输出是tmpfs 那就是 V1,就像这样

Bash 复制代码
[root@docker cgroup]# stat -fc %T /sys/fs/cgroup/
tmpfs

Go 实现如下:

go 复制代码
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

go 复制代码
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 中添加具体限制即可,就像这样:

Bash 复制代码
echo 5000 10000 > cpu.max

含义是在10000的CPU时间周期内,有5000是分配给本cgroup的,也就是本cgroup管理的进程在单核CPU上的使用率不会超过50%

具体实现如下:

go 复制代码
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 即可

bash 复制代码
echo 1033 > cgroup.procs

Go 实现如下:

go 复制代码
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 下的子目录即可移除

go 复制代码
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 版本即可

bash 复制代码
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 环境进行测试

bash 复制代码
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

bash 复制代码
{"level":"info","msg":"use cgroup v1","time":"2024-04-14T13:23:19+08:00"}

执行以下命令测试memory分配

Bash 复制代码
yes > /dev/null

可以看到,过会就被 OOM Kill 了

Bash 复制代码
/ # yes > /dev/null
Killed

执行以下命令 跑满 cpu

Bash 复制代码
while : ; do : ; done &

确实被限制到 10%了

Bash 复制代码
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 配置

Bash 复制代码
vi /etc/default/grub

内容大概是这样的:

Bash 复制代码
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进行修改

Bash 复制代码
GRUB_CMDLINE_LINUX="quiet splash systemd.unified_cgroup_hierarchy=1"

然后执行以下命令更新 GRUB 配置

Bash 复制代码
sudo update-grub

最后查看一下启动参数,确认配置修改上了

Bash 复制代码
cat /boot/grub/grub.cfg | grep "systemd.unified_cgroup_hierarchy=1"

然后就是重启

Bash 复制代码
reboot

重启后查看,不出意外切换到 cgroups v2 了

Bash 复制代码
root@cgroupv2:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs

测试

bash 复制代码
./mydocker run -mem 10m -cpu 10 -it -name cgroupv2 busybox /bin/sh
bash 复制代码
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

bash 复制代码
{"level":"info","msg":"use cgroup v2","time":"2024-04-14T13:26:32+08:00"}

执行同样的测试,效果一致,说明 cgroup v2 使用正常。

执行以下命令测试memory分配

Bash 复制代码
yes > /dev/null

可以看到,过会就被 OOM Kill 了

Bash 复制代码
/ # yes > /dev/null
Killed

执行以下命令 跑满 cpu

Bash 复制代码
while : ; do : ; done &

确实被限制到 10%了

Bash 复制代码
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

欢迎关注~


【从零开始写 Docker 系列】 持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


相关代码见 feat-cgroup-v2 分支,测试脚本如下:

需要提前在 /var/lib/mydocker/image 目录准备好 busybox.tar 文件,具体见第四篇第二节。

bash 复制代码
# 克隆代码
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
相关推荐
Dream of maid20 分钟前
Linux(下)
linux·运维·服务器
齐鲁大虾23 分钟前
统信系统UOS常用命令集
linux·运维·服务器
Benszen41 分钟前
Docker容器化技术实战指南
运维·docker·容器
ZzzZZzzzZZZzzzz…1 小时前
Nginx 平滑升级:从 1.26.3 到 1.28.0,用户无感知
linux·运维·nginx·平滑升级·nginx1.26.3·nginx1.28.0
Hommy881 小时前
【开源剪映小助手】Docker 部署
docker·容器·开源·github·aigc
一叶知秋yyds2 小时前
Ubuntu 虚拟机安装 OpenClaw 完整流程
linux·运维·ubuntu·openclaw
斯普信云原生组3 小时前
Prometheus 环境监控虚机 Redis 方案(生产实操版)
运维·docker·容器
喵了几个咪3 小时前
如何在 Superset Docker 容器中安装 MySQL 驱动
mysql·docker·容器·superset
工具罗某人3 小时前
docker compose部署kafka集群搭建
docker·容器·kafka
楠奕4 小时前
CentOS7安装GoldenDB单机搭建及常见报错解决方案
linux·运维·服务器