写点什么

500 行代码手写 docker- 实现硬件资源限制 cgroups

  • 2023-05-30
    广东
  • 本文字数:4351 字

    阅读完需:约 14 分钟

500行代码手写docker-实现硬件资源限制cgroups

(5)500 行代码手写 docker-实现硬件资源限制 cgroups

本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用 go 语言实现一个类似 docker 的容器化功能,最终能够容器化的运行一个进程。


本章的源码已经上传到 github,地址如下:


https://github.com/HobbyBear/tinydocker/tree/chapter5
复制代码


之前我们对容器的网络命名空间,文件系统命名空间都进行了配置,说到底这些都是为了资源更好的隔离,但是他们无法办到对硬件资源使用的隔离,比如,cpu,内存,带宽,而今天要介绍的 cgroups 技术便能够对硬件资源的使用产生隔离。

cgroups 技术简介

cgroups 技术是内核提供的功能,可以通过虚拟文件系统接口对其进行访问和更改。mount 命令可以查看 cgroups 在虚拟文件系统下的挂载目录。


root@ecs-295280:~# mount | grep  cgrouptmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)root@ecs-295280:~#
复制代码


一般默认的挂载目录是在/sys/fs/cgroup 目录下,系统内核在开机时,会默认挂载 cgroup 目录。这样便能通过访问文件的方式对 cgroup 功能进行使用。


在/sys/fs/cgroup/ 目录下,我们看到的每个目录例如 cpu,blkio 被称作 subsystem 子系统,每个子系统下可以设置各自要管理的进程 id。


root@ecs-295280:~# ls /sys/fs/cgroup/blkio    cpu,cpuacct  freezer  net_cls           perf_event  systemdcpu      cpuset       hugetlb  net_cls,net_prio  pids        unifiedcpuacct  devices      memory   net_prio          rdma
复制代码


拿 cpu 这个目录下的文件举例


root@ecs-295280:/sys/fs/cgroup/cpu# lscgroup.clone_children  cpuacct.usage_percpu_sys   cpu.statcgroup.procs           cpuacct.usage_percpu_user  ebpf-agentcgroup.sane_behavior   cpuacct.usage_sys          hostguardcpuacct.stat           cpuacct.usage_user         notify_on_releasecpuacct.usage          cpu.cfs_period_us          release_agentcpuacct.usage_all      cpu.cfs_quota_us           taskscpuacct.usage_percpu   cpu.sharesroot@ecs-295280:/sys/fs/cgroup/cpu# ll -l
复制代码


在 cpu 子系统这个目录下,有两个文件 cgroup.procs,tasks 文件,它们都是用来管理 cgroup 中的进程。但是,它们的使用方式略有不同:


cgroup.procs 文件用于向 cgroup 中添加或删除进程,只需要将进程的 task id 写入该文件即可。


tasks 文件则是用于将整个进程组添加到 cgroup 中。如果将一个进程组的 pid 写入 tasks 文件,则该进程组中的所有进程都会被添加到 cgroup 中。


进程被加入到这个 cgroup 组以后,其使用的 cpu 带宽将会受到 cpu.cfs_quota_us 和 cpu.cfs_period_us 的影响。通过 shell 命令查看他们的内容。


root@ecs-295280:/sys/fs/cgroup/cpu/test# cat cpu.cfs_period_us100000root@ecs-295280:/sys/fs/cgroup/cpu/test# cat cpu.cfs_quota_us-1
复制代码


默认情况下,cpu.cfs_period_us 是 100000,单位是微秒,cpu.cfs_period_us 代表了 cpu 运行一个周期的时长,100000 代表了 100ms,cpu.cfs_quota_us 代表进程所占用的周期时长,-1 代表不限制进程使用 cpu 周期时长,如果 cpu.cfs_quota_us 是 50000(50ms)则代表在 cpu 一个调度周期内,该 cgroup 下的进程最多只能运行半个周期,如果达到了运行周期的限制,那么它必须等待下一个时间片才能继续运行了。

命名行实践下 cgroups 隔离特性

我们来实验下:

对 cpu 使用率进行限制

在 cpu 的一级目录下,是包含了当前系统所有进程,为了不影响它们,我们在 cpu 的一级目录下创建一个 test 目录,然后单独的在 test 目录中的 tasks 文件加入进程 id。


📢📢 ❗️cgroup 的每个子系统是分级的,这个级别体现在目录层级上,默认子目录会继承父目录的属性,子目录也可以通过修改子目录下的文件,来覆盖掉父目录的属性。


root@ecs-295280:/sys/fs/cgroup/cpu/test# pwd/sys/fs/cgroup/cpu/test
复制代码


设置 cpu.cfs_quota_us 为一个时间片的一半,设置 tasks,把当前进程加入到 cgroup 中


root@ecs-295280:/sys/fs/cgroup/cpu/test# cat cpu.cfs_quota_us50000root@ecs-295280:/sys/fs/cgroup/cpu/test# sh -c "echo $$ > tasks"root@ecs-295280:/sys/fs/cgroup/cpu/test# cat tasks6596166314
复制代码


在当前 shell 界面,通过 stress 对 cpu 进行压力测试。我的虚拟机是一个核,我这里直接通过 stress 对这一个 cpu 核进行压测。


root@ecs-295280:/sys/fs/cgroup/cpu/test# stress --cpu 1 --timeout 60
复制代码


启动另一个终端,查看 cpu 占用情况


Tasks:  94 total,   2 running,  92 sleeping,   0 stopped,   0 zombie%Cpu(s): 51.9 us,  0.0 sy,  0.0 ni, 48.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 stMiB Mem :   1982.9 total,    451.3 free,    193.4 used,   1338.2 buff/cacheMiB Swap:      0.0 total,      0.0 free,      0.0 used.   1597.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 66333 root 20 0 3856 100 0 R 50.3 0.0 0:06.00 stress 1 root 20 0 102780 12420 8236 S 0.0 0.6 0:05.93 systemd
复制代码


可以看到的是 cpu 占用率在达到百分之 50 时就不上去了,这正是由于 stress 进程是 bash 进程的子进程,继承了 bash 进程的 cgroup,所以 cpu 使用率受到了限制。

对内存使用率进行限制

再来看看如何通过 cgroup 对内存进行限制,这次我们就应该进入到 memory 这个子系统的目录了,同样我们在其下面创建一个 test 目录。


root@ecs-295280:/sys/fs/cgroup/memory# mkdir testroot@ecs-295280:/sys/fs/cgroup/memory# cd test/root@ecs-295280:/sys/fs/cgroup/memory/test# pwd/sys/fs/cgroup/memory/test
复制代码


然后把当前进程加进去


root@ecs-295280:/sys/fs/cgroup/memory/test# sh -c "echo $$ > tasks"root@ecs-295280:/sys/fs/cgroup/memory/test# cat tasks6596166476
复制代码


设置最大使用内存,memory 目录下限制最大使用内存需要设置 memory.limit_in_bytes 这个文件,默认情况下,它是一个大的离谱的值,我们将它改为 100M


root@ecs-295280:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes9223372036854771712root@ecs-295280:/sys/fs/cgroup/memory/test# vim memory.limit_in_bytesroot@ecs-295280:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes104857600
复制代码


这个时候通过 stress 对内存进行压力测试,我们限制了 100M,但是如果 stress 要求分配 200M 内存,看看能正常分配吗?


root@ecs-295280:/sys/fs/cgroup/memory/test# stress --vm-bytes 200m --vm-keep  -m 1stress: info: [66533] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hddstress: FAIL: [66533] (415) <-- worker 66534 got signal 9stress: WARN: [66533] (417) now reaping child worker processesstress: FAIL: [66533] (451) failed run completed in 0s
复制代码


可以看到的是,程序崩溃了,原因则是由于发生了 oom,因为内存已经被我们限制到了 100M,通过 test 目录下的 memory.oom_control 文件可以看到发生 oom 的次数。


oom_kill_disable 0under_oom 0oom_kill 1
复制代码


oom_kill 为 1 代表发生 oom 后,进程被 kill 掉的次数。


在简单看完 cgroup 如何对 cpu 和内存进行限制以后,看看 golang 代码如何实现。

golang 代码实现 cgroups 配置

在用代码对 cgroup 的操作本质上就是对 cgroup 的文件进行操作。


cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    err = cmd.Start()    if err != nil {      fmt.Println(err)    }
containerName := os.Args[2] if err := cgroups.ConfigDefaultCgroups(cmd.Process.Pid, containerName); err != nil { log.Error("config cgroups fail %s", err) }
if err := network.ConfigDefaultNetworkInNewNet(cmd.Process.Pid); err != nil { log.Error("config network fail %s", err) } cmd.Wait() cgroups.CleanCgroupsPath(containerName)
复制代码


在前面代码的基础上,启动子进程后,父进程把子进程 pid 添加到一个新的 cgroup 中,cgroups.ConfigDefaultCgroups 方法用于实现对 cgroup 的控制,以容器名作为 cgroup 子系统的目录,然后当子进程容器执行完毕后,通过 cgroups.CleanCgroupsPath 去对 cgroup 相关目录进行清理。


func CleanCgroupsPath(containerName string) error {  output, err := exec.Command("cgdelete", "-r", fmt.Sprintf("memory:%s/%s", dockerName, containerName)).Output()  if err != nil {    log.Error("cgdelete fail err=%s output=%s", err, string(output))  }  output, err = exec.Command("cgdelete", "-r", fmt.Sprintf("cpu:%s/%s", dockerName, containerName)).Output()  if err != nil {    log.Error("cgdelete fail err=%s output=%s", err, string(output))  }  return nil}
复制代码


清理 cgroup 的方式我用了 cgdelete 命令 删除掉容器 cgroup 的配置,直接 remove 删除会出现删除失败情况。

总结

这也是我对于手写容器系列的终章,算是对容器原理的一个入门级讲解,其实后续还可以针对它做很多优化,比如实现不同主机上的容器互联,实现容器日志的功能,实现端口映射,实现卷映射功能,这些功能其实都是建立在我们讲的容器原理之上的,懂了原理便能一通百通,希望能给你带来启发。

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020-09-17 加入

还未添加个人简介

评论

发布
暂无评论
500行代码手写docker-实现硬件资源限制cgroups_容器_蓝胖子的编程梦_InfoQ写作社区