从零开始写 Docker(十九)---增加 cgroup v2 支持
本文为从零开始写 Docker 系列第十九篇,添加对 cgroup v2 的支持。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
推荐阅读以下文章对 docker 基本实现有一个大致认识:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
- 基于 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
从零开始写 Docker(十九)---增加 cgroup v2 支持的更多相关文章
- Docker(十九)-Docker监控容器资源的占用情况
启动一个容器并限制资源 启动一个centos容器,限制其内存为1G ,可用cpu数为2 [root@localhost ~]# docker run --name os1 -it -m 1g --cp ...
- 从零开始学安全(十九)●PHP数组函数
$temp= array(1,2,3,,,,) 创建一个数组赋值给temp $id=range(1,6,2); 成长值 1到6 跨度为2 就是3个长度数组 也可以是字符“a” &quo ...
- (NO.00001)iOS游戏SpeedBoy Lite成形记(二十九):增加排行榜功能2
接下来回到Xcode中,首先在PopupLayer.m中添加justClose方法: -(void)justClose{ [self.gameScene removePopup]; } 然后在Game ...
- Java从零开始学三十九(对象序列化)
一.序列化 将对象的状态存储到特定存储介质中的过程 对象序列化,就是把一个对象变为二进制的数据流的一种方法,通过对象序列化可以方便的实现对象的传输或存储. 序列化保存对象的“全景图”,构建对象的“ ...
- Java从零开始学二十九(大数操作(BigIntger、BigDecimal)
一.BigInteger 如果在操作的时候一个整型数据已经超过了整数的最大类型长度long的话,则此数据就无法装入,所以,此时要使用BigInteger类进行操作. 不可变的任意精度的整数.所有操作中 ...
- Flask 教程 第十九章:Docker容器上的部署
本文翻译自The Flask Mega-Tutorial Part XIX: Deployment on Docker Containers 这是Flask Mega-Tutorial系列的第十九部分 ...
- 从零开始学习 Docker
这篇文章是我学习 Docker 的记录,大部分内容摘抄自 <<Docker - 从入门到实践>> 一书,并非本人原创.学习过程中整理成适合我自己的笔记,其中也包含了我自己的 ...
- NeHe OpenGL教程 第十九课:粒子系统
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
- 使用Typescript重构axios(二十九)——添加baseURL
0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...
- NeHe OpenGL教程 第三十九课:物理模拟
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
随机推荐
- 七年之痒!一个 PHP 程序员职业生涯的自述
大家好,我是码农先森. 今年刚好是我毕业的第七个年头,在婚姻感情当中都有一种「七年之痒」的说法,这次我把这个词「七年之痒」用一次在我的职业生涯复盘上.七年前我从告别校园,踏入互联网编程行业,七年后我依 ...
- wordpress 折腾记
今天我看到一篇个人博客,我想建个人网站的心又动了. 虽说博客园已经很符合我的预期了,但我还是一直很想做一个个人网站做一些个性化的东西,今天试试用用wordpress搭建一个wordpress网站 介绍 ...
- js整数类型
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8 ...
- 设定cookie 获取cookie数据的转换
1,cookie必须是键值对形式的 键名=数值 而且必须是 字符串格式 document.cookie = 'nam ...
- OOP第二阶段题集总结
一.前言 知识点:考察继承和多态为多,其中还涉及迭代器的使用,在每个题集中都有一个综合性题目设计多方面知识点考试,有List类和HashMap的使用以及正则表达式的运用,并且注重考查设计,理解类与类之 ...
- 配置系统未能初始化。“System.Transactions.Diagnostics.DiagnosticTrace”的类型初始值设定项引发异常。
配置系统未能初始化."System.Transactions.Diagnostics.DiagnosticTrace"的类型初始值设定项引发异常. 1.是检查当前程序的 App.c ...
- redis安全篇
redis被攻击,作为突破口,服务器惨遭毒手的事太常见了. 大多数云服务器被攻击,都是redis,mongodb等数据库被入侵. 因此修改端口,密码,以及注意bind运行地址,是必须. 思考是否要暴露 ...
- 简单易懂的JSON框架
分享一个由本人编写的JSON框架. JSON反序列化使用递归方式来解析JSON字符串,不使用任何第三方JAR包,只使用JAVA的反射来创建对象(必须要有无参构造器),赋值,编写反射缓存来提升性 ...
- 短链接口设计&禁用Springboot执行器端点/env的安全性
短链接口设计 //短链接服务 跳转方式,实现短链接转长链接的请求. @GetMapping("/{code}") public String redirectUrl(@PathVa ...
- Arduino实现温湿度传感器以及数据上传到云(乐维互联)
0 准备材料 0.1 ESP-01S 引脚及定义 官方定义: 序号 pin 功能 1 GND 地线 2 IO0/GPIO0 工作模式选择:①悬空:Flash Boot,工作模式 ②下拉:UART Do ...