(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 cgroup
tmpfs 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 systemd
cpu cpuset hugetlb net_cls,net_prio pids unified
cpuacct devices memory net_prio rdma
拿cpu这个目录下的文件举例
root@ecs-295280:/sys/fs/cgroup/cpu# ls
cgroup.clone_children cpuacct.usage_percpu_sys cpu.stat
cgroup.procs cpuacct.usage_percpu_user ebpf-agent
cgroup.sane_behavior cpuacct.usage_sys hostguard
cpuacct.stat cpuacct.usage_user notify_on_release
cpuacct.usage cpu.cfs_period_us release_agent
cpuacct.usage_all cpu.cfs_quota_us tasks
cpuacct.usage_percpu cpu.shares
root@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_us
100000
root@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_us
50000
root@ecs-295280:/sys/fs/cgroup/cpu/test# sh -c "echo $$ > tasks"
root@ecs-295280:/sys/fs/cgroup/cpu/test# cat tasks
65961
66314
在当时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 st
MiB Mem : 1982.9 total, 451.3 free, 193.4 used, 1338.2 buff/cache
MiB 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 test
root@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 tasks
65961
66476
设置最大运用内存,memory目录下约束最大运用内存需要设置memory.limit_in_bytes 这个文件,默许情况下,它是一个大的离谱的值,咱们将它改为100M
root@ecs-295280:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
9223372036854771712
root@ecs-295280:/sys/fs/cgroup/memory/test# vim memory.limit_in_bytes
root@ecs-295280:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
104857600
这个时分经过stress 对内存进行压力测验,咱们约束了100M,可是假如stress要求分配200M内存,看看能正常分配吗?
root@ecs-295280:/sys/fs/cgroup/memory/test# stress --vm-bytes 200m --vm-keep -m 1
stress: info: [66533] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [66533] (415) <-- worker 66534 got signal 9
stress: WARN: [66533] (417) now reaping child worker processes
stress: FAIL: [66533] (451) failed run completed in 0s
能够看到的是,程序溃散了,原因则是由于发生了oom,由于内存已经被咱们约束到了100M,经过test目录下的memory.oom_control文件能够看到发生oom的次数。
oom_kill_disable 0
under_oom 0
oom_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删除会呈现删除失利情况。
总结
这也是我关于手写容器系列的终章,算是对容器原理的一个入门级解说,其实后续还能够针对它做很多优化,比方完成不同主机上的容器互联,完成容器日志的功用,完成端口映射,完成卷映射功用,这些功用其实都是建立在咱们讲的容器原理之上的,懂了原理便能一通百通,希望能给你带来启示。