0.前言
首先要知道一個運作的容器,其實就是一個受到隔離和資源限制的Linux程序——對,它就是一個程序。前面我們讨論了Docker容器實作隔離用到的技術Linux namespace,本篇我們來讨論容器實作資源限制的技術 Linux CGroups。
1.關于Linux CGroups
Linux Cgroups的全稱是Linux Control Groups。它最主要的作用,就是限制一個程序組能夠使用的資源上限,包括CPU、記憶體、磁盤、網絡帶寬等等。此外,還能夠對程序進行優先級設定,以及将程序挂起和恢複等操作。
在Linux中,Cgroups給使用者暴露出來的操作接口是檔案系統,即它以檔案和目錄的方式組織在作業系統的/sys/fs/cgroup路徑下。在我的centos伺服器下,用mount指令把它們展示出來:
//CentOS Linux release 7.5.1804
$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
可以看到,在/sys/fs/cgroup下面有很多諸如cpuset、cpu、 memory這樣的子目錄,也叫子系統。這些都是我這台機器目前可以被Cgroups進行限制的資源種類。而在子系統對應的資源種類下,你就可以看到該類資源具體可以被限制的方法。比如,對CPU子系統來說,我們就可以看到如下幾個配置檔案:
$ ls /sys/fs/cgroup/cpu
cgroup.clone_children cgroup.sane_behavior cpu.rt_period_us cpu.stat cpuacct.usage_percpu system.slice
cgroup.event_control cpu.cfs_period_us cpu.rt_runtime_us cpuacct.stat notify_on_release tasks
cgroup.procs cpu.cfs_quota_us cpu.shares cpuacct.usage release_agent user.slice
2.舉個例子(CPU限制)
1) 配置你的控制組
$ cd /sys/fs/cgroup/cpu
$ mkdir testlimit
$ ls testlimit/
cgroup.clone_children cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat cpuacct.usage notify_on_release
cgroup.event_control cpu.cfs_period_us cpu.rt_period_us cpu.shares cpuacct.stat cpuacct.usage_percpu tasks
$ cat /sys/fs/cgroup/cpu/testlimit/cpu.cfs_quota_us
-1
$ cat /sys/fs/cgroup/cpu/testlimit/cpu.cfs_period_us
100000
你建立的這個目錄testlimit就稱為一個“控制組”。你會發現,作業系統會在你新建立的目錄下,自動生成該子系統對應的資源限制檔案。可以看到testlimit控制組裡的CPU quota還沒有任何限制(即:-1),CPU period則是預設的100000us
配置一個隻能使用30%cpu的限制,即長度為cfs_period的一段時間内,隻能被配置設定到總量為cfs_quota的CPU時間。
$ echo 30000 > /sys/fs/cgroup/cpu/testlimit/cpu.cfs_quota_us
至此我們的testlimit控制組就配置好了,它限制程序在100000us裡隻能使用30000us的cpu時間。隻是目前沒有将它應用于任何程序。
2) 執行腳本
$ while : ; do : ; done &
[1] 4477
該腳本執行了一個無限循環,可以把cpu吃到100%,可以看到它在背景的程序id是4477,後面限制的時候我們會用到。
通過top檢視cpu使用情況:
%Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
3) 使用cgroups限制該程序的cpu
執行如下指令,使用剛才配置好的testlimit控制組,限制上面4477号程序的cpu:
$ echo 4477 > /sys/fs/cgroup/cpu/testlimit/tasks
再次通過top檢視cpu使用情況:
%Cpu0 : 30.1 us, 3.0 sy, 0.0 ni, 65.5 id, 1.0 wa, 0.0 hi, 0.3 si, 0.0 st
可以看到使用剛才建立的testlimit控制組,将cpu被限制到了30%左右。
4) 啟動一個容器加上CPU時鐘周期限制
接下來,我們啟動一個容器,并加上cpu限制,然後看看cgroups對應的目錄裡有沒有該容器的限制。
$ docker run -td --cpu-period 100000 --cpu-quota 200000 busybox /bin/sh -c "while : ; do : ; done"
c3e3fb30f3cbdcc707dff9f5937018c0ac6b07002d80656760026111c569ca4f
//檢視該容器的程序id: 26430
$ ps -x |grep '/bin/sh'
26430 pts/0 Rs+ 20:52 /bin/sh -c while : ; do : ; done
//檢視cgroups
$ cat /sys/fs/cgroup/cpu/docker/c3e3fb30f3cbdcc707dff9f5937018c0ac6b07002d80656760026111c569ca4f/cpu.cfs_period_us
100000
$ cat /sys/fs/cgroup/cpu/docker/c3e3fb30f3cbdcc707dff9f5937018c0ac6b07002d80656760026111c569ca4f/cpu.cfs_quota_us
200000
$ cat /sys/fs/cgroup/cpu/docker/c3e3fb30f3cbdcc707dff9f5937018c0ac6b07002d80656760026111c569ca4f/tasks
26430
$ top
%Cpu0 : 50.8 us, 49.2 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
如上所示,可以通過啟動容器時傳回的容器ID在cgroups中找到對應的限制。
3.對照Docker源碼
// New creates and initializes a new containerd server
func New(ctx context.Context, config *Config) (*Server, error) {
//...
if err := apply(ctx, config); err != nil {
return nil, err
}
//...
}
// apply sets config settings on the server process
func apply(ctx context.Context, config *Config) error {
if config.OOMScore != 0 {
log.G(ctx).Debugf("changing OOM score to %d", config.OOMScore)
if err := sys.SetOOMScore(os.Getpid(), config.OOMScore); err != nil {
log.G(ctx).WithError(err).Errorf("failed to change OOM score to %d", config.OOMScore)
}
}
if config.Cgroup.Path != "" {
cg, err := cgroups.Load(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path))
if err != nil {
if err != cgroups.ErrCgroupDeleted {
return err
}
if cg, err = cgroups.New(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path), &specs.LinuxResources{}); err != nil {
return err
}
}
if err := cg.Add(cgroups.Process{
Pid: os.Getpid(),
}); err != nil {
return err
}
}
return nil
}
在上面代碼中,建立容器時會調用apply接口,裡面的cgroups.Load調用就會去加載cgroups,cg.Add把建立的容器程序加入到cgroups task中。
4.下一代Linux Cgroups
在Kernel 3.16後,引入了一個叫__DEVEL__sane_behavior的特性(還在開發試驗階段),它可以把所有子系統都挂載到根層級下,隻有葉子節點可以存在tasks,非葉子節點隻進行資源控制。
The unified control group hierarchy in 3.16
參考