500 行代码手写 docker- 实现硬件资源限制 cgroups
(5)500 行代码手写 docker-实现硬件资源限制 cgroups
本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用 go 语言实现一个类似 docker 的容器化功能,最终能够容器化的运行一个进程。
本章的源码已经上传到 github,地址如下:
之前我们对容器的网络命名空间,文件系统命名空间都进行了配置,说到底这些都是为了资源更好的隔离,但是他们无法办到对硬件资源使用的隔离,比如,cpu,内存,带宽,而今天要介绍的 cgroups 技术便能够对硬件资源的使用产生隔离。
cgroups 技术简介
cgroups 技术是内核提供的功能,可以通过虚拟文件系统接口对其进行访问和更改。mount 命令可以查看 cgroups 在虚拟文件系统下的挂载目录。
一般默认的挂载目录是在/sys/fs/cgroup 目录下,系统内核在开机时,会默认挂载 cgroup 目录。这样便能通过访问文件的方式对 cgroup 功能进行使用。
在/sys/fs/cgroup/ 目录下,我们看到的每个目录例如 cpu,blkio 被称作 subsystem 子系统,每个子系统下可以设置各自要管理的进程 id。
拿 cpu 这个目录下的文件举例
在 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 命令查看他们的内容。
默认情况下,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 的每个子系统是分级的,这个级别体现在目录层级上,默认子目录会继承父目录的属性,子目录也可以通过修改子目录下的文件,来覆盖掉父目录的属性。
设置 cpu.cfs_quota_us 为一个时间片的一半,设置 tasks,把当前进程加入到 cgroup 中
在当前 shell 界面,通过 stress 对 cpu 进行压力测试。我的虚拟机是一个核,我这里直接通过 stress 对这一个 cpu 核进行压测。
启动另一个终端,查看 cpu 占用情况
可以看到的是 cpu 占用率在达到百分之 50 时就不上去了,这正是由于 stress 进程是 bash 进程的子进程,继承了 bash 进程的 cgroup,所以 cpu 使用率受到了限制。
对内存使用率进行限制
再来看看如何通过 cgroup 对内存进行限制,这次我们就应该进入到 memory 这个子系统的目录了,同样我们在其下面创建一个 test 目录。
然后把当前进程加进去
设置最大使用内存,memory 目录下限制最大使用内存需要设置 memory.limit_in_bytes 这个文件,默认情况下,它是一个大的离谱的值,我们将它改为 100M
这个时候通过 stress 对内存进行压力测试,我们限制了 100M,但是如果 stress 要求分配 200M 内存,看看能正常分配吗?
可以看到的是,程序崩溃了,原因则是由于发生了 oom,因为内存已经被我们限制到了 100M,通过 test 目录下的 memory.oom_control 文件可以看到发生 oom 的次数。
oom_kill 为 1 代表发生 oom 后,进程被 kill 掉的次数。
在简单看完 cgroup 如何对 cpu 和内存进行限制以后,看看 golang 代码如何实现。
golang 代码实现 cgroups 配置
在用代码对 cgroup 的操作本质上就是对 cgroup 的文件进行操作。
在前面代码的基础上,启动子进程后,父进程把子进程 pid 添加到一个新的 cgroup 中,cgroups.ConfigDefaultCgroups 方法用于实现对 cgroup 的控制,以容器名作为 cgroup 子系统的目录,然后当子进程容器执行完毕后,通过 cgroups.CleanCgroupsPath 去对 cgroup 相关目录进行清理。
清理 cgroup 的方式我用了 cgdelete 命令 删除掉容器 cgroup 的配置,直接 remove 删除会出现删除失败情况。
总结
这也是我对于手写容器系列的终章,算是对容器原理的一个入门级讲解,其实后续还可以针对它做很多优化,比如实现不同主机上的容器互联,实现容器日志的功能,实现端口映射,实现卷映射功能,这些功能其实都是建立在我们讲的容器原理之上的,懂了原理便能一通百通,希望能给你带来启发。
版权声明: 本文为 InfoQ 作者【蓝胖子的编程梦】的原创文章。
原文链接:【http://xie.infoq.cn/article/a1235d70e695aaf3e99fd3bc8】。文章转载请联系作者。
评论