(5)500行代碼手寫docker-實現(xiàn)硬件資源限制cgroups
本系列教程主要是為了弄清楚容器化的原理,紙上得來終覺淺,絕知此事要躬行,理論始終不及動手實踐來的深刻,所以這個系列會用go語言實現(xiàn)一個類似docker的容器化功能,最終能夠容器化的運行一個進程。
本章的源碼已經(jīng)上傳到github,地址如下:
https://github.com/HobbyBear/tinydocker/tree/chapter5
之前我們對容器的網(wǎng)絡(luò)命名空間,文件系統(tǒng)命名空間都進行了配置,說到底這些都是為了資源更好的隔離,但是他們無法辦到對硬件資源使用的隔離,比如,cpu,內(nèi)存,帶寬,而今天要介紹的cgroups技術(shù)便能夠?qū)τ布Y源的使用產(chǎn)生隔離。
cgroups技術(shù)簡介
cgroups技術(shù)是內(nèi)核提供的功能,可以通過虛擬文件系統(tǒng)接口對其進行訪問和更改。mount 命令可以查看cgroups在虛擬文件系統(tǒng)下的掛載目錄。
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 目錄下,系統(tǒng)內(nèi)核在開機時,會默認掛載cgroup目錄。這樣便能通過訪問文件的方式對cgroup功能進行使用。
在/sys/fs/cgroup/ 目錄下,我們看到的每個目錄例如cpu,blkio被稱作subsystem子系統(tǒng),每個子系統(tǒng)下可以設(shè)置各自要管理的進程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子系統(tǒng)這個目錄下,有兩個文件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命令查看他們的內(nèi)容。
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一個調(diào)度周期內(nèi),該cgroup下的進程最多只能運行半個周期,如果達到了運行周期的限制,那么它必須等待下一個時間片才能繼續(xù)運行了。
命名行實踐下cgroups隔離特性
我們來實驗下:
對cpu使用率進行限制
在cpu的一級目錄下,是包含了當(dāng)前系統(tǒng)所有進程,為了不影響它們,我們在cpu的一級目錄下創(chuàng)建一個test目錄,然后單獨的在test目錄中的tasks文件加入進程id。
???? ??cgroup的每個子系統(tǒng)是分級的,這個級別體現(xiàn)在目錄層級上,默認子目錄會繼承父目錄的屬性,子目錄也可以通過修改子目錄下的文件,來覆蓋掉父目錄的屬性。
root@ecs-295280:/sys/fs/cgroup/cpu/test# pwd
/sys/fs/cgroup/cpu/test
設(shè)置cpu.cfs_quota_us為一個時間片的一半,設(shè)置tasks,把當(dāng)前進程加入到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
在當(dāng)前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使用率受到了限制。
對內(nèi)存使用率進行限制
再來看看如何通過cgroup對內(nèi)存進行限制,這次我們就應(yīng)該進入到memory這個子系統(tǒng)的目錄了,同樣我們在其下面創(chuàng)建一個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
然后把當(dāng)前進程加進去
root@ecs-295280:/sys/fs/cgroup/memory/test# sh -c "echo $$ > tasks"
root@ecs-295280:/sys/fs/cgroup/memory/test# cat tasks
65961
66476
設(shè)置最大使用內(nèi)存,memory目錄下限制最大使用內(nèi)存需要設(shè)置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 對內(nèi)存進行壓力測試,我們限制了100M,但是如果stress要求分配200M內(nèi)存,看看能正常分配嗎?
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
可以看到的是,程序崩潰了,原因則是由于發(fā)生了oom,因為內(nèi)存已經(jīng)被我們限制到了100M,通過test目錄下的memory.oom_control文件可以看到發(fā)生oom的次數(shù)。
oom_kill_disable 0
under_oom 0
oom_kill 1
oom_kill 為1代表發(fā)生oom后,進程被kill掉的次數(shù)。
在簡單看完cgroup如何對cpu和內(nèi)存進行限制以后,看看golang代碼如何實現(xiàn)。
golang代碼實現(xiàn)cgroups配置
在用代碼對cgroup的操作本質(zhì)上就是對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)
在前面代碼的基礎(chǔ)上,啟動子進程后,父進程把子進程pid添加到一個新的cgroup中,cgroups.ConfigDefaultCgroups方法用于實現(xiàn)對cgroup的控制,以容器名作為cgroup子系統(tǒng)的目錄,然后當(dāng)子進程容器執(zhí)行完畢后,通過cgroups.CleanCgroupsPath去對cgroup相關(guān)目錄進行清理。
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刪除會出現(xiàn)刪除失敗情況。文章來源:http://www.zghlxwxcb.cn/news/detail-463089.html
總結(jié)
這也是我對于手寫容器系列的終章,算是對容器原理的一個入門級講解,其實后續(xù)還可以針對它做很多優(yōu)化,比如實現(xiàn)不同主機上的容器互聯(lián),實現(xiàn)容器日志的功能,實現(xiàn)端口映射,實現(xiàn)卷映射功能,這些功能其實都是建立在我們講的容器原理之上的,懂了原理便能一通百通,希望能給你帶來啟發(fā)。文章來源地址http://www.zghlxwxcb.cn/news/detail-463089.html
到了這里,關(guān)于500行代碼手寫docker-實現(xiàn)硬件資源限制cgroups的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!