目录

容器的本质是一种特殊的进程

  • Linux Namespace - 空间隔离
  • Linux Cgroups - 资源限制
  • chroot - 切换进程的根目录

Linux Namespace

Linux Cgroups - Linux Control Group

作用:限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

查看Cgroups限制的资源各类

在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。可以用 mount 指令把它们展示出来。

$ 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/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
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/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件。

$ ls /sys/fs/cgroup/cpu
aegis                  cpuacct.usage_all          cpu.cfs_quota_us   notify_on_release
assist                 cpuacct.usage_percpu       cpu.rt_period_us   release_agent
cgroup.clone_children  cpuacct.usage_percpu_sys   cpu.rt_runtime_us  system.slice
cgroup.procs           cpuacct.usage_percpu_user  cpu.shares         tasks
cgroup.sane_behavior   cpuacct.usage_sys          cpu.stat           user.slice
cpuacct.stat           cpuacct.usage_user         docker
cpuacct.usage          cpu.cfs_period_us          kubepods

可以看到 cfs_period 和 cfs_quota 这样的关键词。这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。

演示CPU子系统的控制

现在进入 /sys/fs/cgroup/cpu 目录下,创建一个子目录 container,这个目录就称为一个“控制组”。操作系统会在新创建的 container 目录下,自动生成该子系统对应的资源限制文件。

$ cd /sys/fs/cgroup/cpu
$ mkdir container
$ ls container/
cgroup.clone_children  cpuacct.usage_percpu       cpu.cfs_period_us  cpu.stat
cgroup.procs           cpuacct.usage_percpu_sys   cpu.cfs_quota_us   notify_on_release
cpuacct.stat           cpuacct.usage_percpu_user  cpu.rt_period_us   tasks
cpuacct.usage          cpuacct.usage_sys          cpu.rt_runtime_us
cpuacct.usage_all      cpuacct.usage_user         cpu.shares

我在后台执行下面一条脚本,这是一个死循环,可以把计算机的 CPU 吃到 100%,根据它的输出,可以看到这个脚本在后台运行的进程号(PID)是 2383。

$ while : ; do : ; done &
[1] 2383

可以用 top 指令来确认一下 CPU 有没有被打满。

$ top - 16:25:19 up 392 days,  5:37,  1 user,  load average: 2.12, 1.41, 0.86
Tasks: 153 total,   3 running, 150 sleeping,   0 stopped,   0 zombie
%Cpu(s): 55.6 us,  1.0 sy,  0.0 ni, 42.7 id,  0.2 wa,  0.5 hi,  0.0 si,  0.0 st
MiB Mem :   3780.8 total,    182.6 free,   1709.1 used,   1889.1 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   1947.3 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 2383 root      20   0   29804   1948      0 R 100.0   0.1   3:13.97 bash

通过查看 container 目录下的文件,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us)

$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us 
100000

可以通过修改这些文件的内容来设置限制。比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us)。这意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。

$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

接下来,把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了。

$ echo 2383 > /sys/fs/cgroup/cpu/container/tasks

可以用 top 指令查看一下

top - 16:43:04 up 392 days,  5:54,  1 user,  load average: 0.71, 1.37, 1.27
Tasks: 155 total,   2 running, 153 sleeping,   0 stopped,   0 zombie
%Cpu(s): 16.6 us,  1.8 sy,  0.0 ni, 80.7 id,  0.2 wa,  0.5 hi,  0.2 si,  0.0 st
MiB Mem :   3780.8 total,    173.5 free,   1711.3 used,   1896.0 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   1945.1 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 2383 root      20   0   29804   1948      0 R  20.3   0.1  20:17.55 bash

可以看到,计算机的 CPU 使用率立刻降到了 20%(%Cpu0 : 20.3 us)。

结束进程

$ kill 2383

删除创建的控制组 container

$ rmdir container/

使用Docker来完成上面的CPU资源限制

$ docker run -it --cpu-period=100000 --cpu-quota=20000 centos /bin/bash
15163f5037e2c76073c52e1d8d25fd96af31b5b2f13d46f2f58b5fbdaba547ed

查看Docker对CPU子系统限制的设置

$ cat /sys/fs/cgroup/cpu/docker/15163f5037e2c76073c52e1d8d25fd96af31b5b2f13d46f2f58b5fbdaba547ed/cpu.cfs_period_us 
100000
$ cat /sys/fs/cgroup/cpu/docker/15163f5037e2c76073c52e1d8d25fd96af31b5b2f13d46f2f58b5fbdaba547ed/cpu.cfs_quota_us 
20000

chroot - change root file system

创建 mini 文件系统

  • 创建目录
    $ mkdir -p $HOME/test
    $ mkdir -p $HOME/test/{bin,lib64,lib}
    
  • 拷贝基本命令
    $ cp -v /bin/{bash,ls} $HOME/test/bin
    
    '/bin/bash' -> '/root/test/bin/bash'
    '/bin/ls' -> '/root/test/bin/ls'
    
  • 拷贝基本命令依赖的动态库
    $ list="$(ldd /bin/{bash,ls} | egrep -o '/lib.*\.[0-9]' | sort | uniq)"
    $ for i in $list; do cp -v "$i" "${HOME}/test${i}"; done
    
    '/lib64/ld-linux-x86-64.so.2' -> '/root/test/lib64/ld-linux-x86-64.so.2'
    '/lib64/libcap.so.2' -> '/root/test/lib64/libcap.so.2'
    '/lib64/libc.so.6' -> '/root/test/lib64/libc.so.6'
    '/lib64/libdl.so.2' -> '/root/test/lib64/libdl.so.2'
    '/lib64/libpcre2-8.so.0' -> '/root/test/lib64/libpcre2-8.so.0'
    '/lib64/libpthread.so.0' -> '/root/test/lib64/libpthread.so.0'
    '/lib64/libselinux.so.1' -> '/root/test/lib64/libselinux.so.1'
    '/lib64/libtinfo.so.6' -> '/root/test/lib64/libtinfo.so.6'
    

创建容器进程

执行 chroot 命令,告诉操作系统,使用 $HOME/test 目录作为 /bin/bash 进程的根目录。

chroot 后面的命令 /bin/bash 是指宿主机的 $HOME/test/bin/bash,执行 chroot 命令后,看到的根文件系统就是宿主机的 $HOME/test 目录下的文件和目录。

$ chroot $HOME/test /bin/bash
# export PATH=/bin
# ls /
bin  lib  lib64

这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

Docker 会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别。