從零開始寫 Docker(十九)---增加 cgroup v2 支援

探索云原生發表於2024-07-24

feat-cgroup-v2.png

本文為從零開始寫 Docker 系列第十九篇,新增對 cgroup v2 的支援。


完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star

推薦閱讀以下文章對 docker 基本實現有一個大致認識:

  • 核心原理深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基於 namespace 的檢視隔離探索 Linux Namespace:Docker 隔離的神奇背後
  • 基於 cgroups 的資源限制
    • 初探 Linux Cgroups:資源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系統:資源精細管理
    • Docker 與 Linux Cgroups:資源隔離的魔法之旅
  • 基於 overlayfs 的檔案系統Docker 魔法解密:探索 UnionFS 與 OverlayFS
  • 基於 veth pair、bridge、iptables 等等技術的 Docker 網路揭秘 Docker 網路:手動實現 Docker 橋接網路

開發環境如下:

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

stat -fc %T /sys/fs/cgroup/

如果輸出是cgroup2fs 那就是 V2,就像這樣

root@tezn:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs

如果輸出是tmpfs 那就是 V1,就像這樣

[root@docker cgroup]# stat -fc %T /sys/fs/cgroup/
tmpfs

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

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 中新增具體限制即可,就像這樣:

echo 5000 10000 > cpu.max

含義是在10000的CPU時間週期內,有5000是分配給本cgroup的,也就是本cgroup管理的程序在單核CPU上的使用率不會超過50%

具體實現如下:

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 即可

echo 1033 > cgroup.procs

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 下的子目錄即可移除

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 版本即可

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 環境進行測試

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

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

執行以下命令測試memory分配

yes > /dev/null

可以看到,過會就被 OOM Kill 了

/ # yes > /dev/null
Killed

執行以下命令 跑滿 cpu

while : ; do : ; done &

確實被限制到 10%了

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 配置

vi /etc/default/grub

內容大概是這樣的:

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進行修改

GRUB_CMDLINE_LINUX="quiet splash systemd.unified_cgroup_hierarchy=1"

然後執行以下命令更新 GRUB 配置

sudo update-grub

最後檢視一下啟動引數,確認配置修改上了

cat /boot/grub/grub.cfg | grep "systemd.unified_cgroup_hierarchy=1"

然後就是重啟

reboot

重啟後檢視,不出意外切換到 cgroups v2 了

root@cgroupv2:~# stat -fc %T /sys/fs/cgroup/
cgroup2fs

測試

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

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

執行同樣的測試,效果一致,說明 cgroup v2 使用正常。

執行以下命令測試memory分配

yes > /dev/null

可以看到,過會就被 OOM Kill 了

/ # yes > /dev/null
Killed

執行以下命令 跑滿 cpu

while : ; do : ; done &

確實被限制到 10%了

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 檔案,具體見第四篇第二節。

# 克隆程式碼
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

相關文章