Docker的实现原理—NameSpace、Cgroup

容器其实是一种沙盒技术。顾名思义,沙盒就是能够像一个集装箱一样,把你的应用“装”起来的技术。这样,应用与应用之间,因为有了边界而不至于相互干扰;而被装进集装箱的应用,也可以被方便地搬来搬去,这也是 PaaS 最理想的状态。

计算机的程序和进程初解

在将Docker的实现原理之前我们先来聊一聊计算机的程序。

计算机的程序归根结底来说就是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。

由于计算机只认识 0 和 1,所以无论用哪种语言编写这段代码,最后都需要通过某种方式翻译成二进制文件,才能在计算机操作系统中运行起来。而为了能够让这些代码正常运行,我们往往还要给它提供数据,比如一个加法程序需要提供一个输入或输入文件。这些数据加上代码本身的二进制文件,放在磁盘上,就是我们平常所说的一个“程序”,也叫代码的可执行镜像(executable image)

执行程序时,首先,操作系统从“程序”中发现输入数据保存在一个文件中,所以这些数据就会被加载到内存中待命。同时,操作系统又读取到了计算加法的指令,这时,它就需要指示 CPU 完成加法操作。而 CPU 与内存协作进行加法计算,又会使用寄存器存放数值、内存堆栈保存执行的命令和变量。同时,计算机里还有被打开的文件,以及各种各样的 I/O 设备在不断地调用中修改自己的状态。

一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。这样一个程序运行起来后的计算机执行环境的总和,我们称之为:进程

Docker的实现

回过头来我们来看,虚拟机和容器,无论是虚拟机还是容器都可以理解是在做虚拟化(也就是一台机器当做多台来使用),虚拟化核心需要解决的问题:资源隔离与资源限制

  • 虚拟机硬件虚拟化技术, 通过一个 hypervisor 层和独立的Guest OS实现对资源的彻底隔离。

  • 容器技术的核心功能,就是通过约束和修改进程的动态表现,从而制造一个边界

    • 边界的实现方式是利用内核的 Cgroup 和 Namespace 特性,此功能完全通过软件实现。

这里虽然容器继续也通过Namesapce和Cgroup实现了隔离,但它也有其弊端:隔离的不彻底

尽管可以在容器里通过 Mount Namespace 单独挂载其他不同版本的操作系统文件,比如 CentOS 或者 Ubuntu,但这并不能改变共享宿主机内核的事实。

隔离机制

首先,我这里使用centos7系统先尝试启动一个容器,去观察它一下:

[root@cluster1 ~]$ docker run -it busybox /bin/sh
/ $ ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps aux

这里可以看到一个比较有趣的事情,我们去查看进程时,我在容器中运行的/bin/sh进程ID变为了“1”,熟悉centos的人都知道进程为“1”的应该是systemd程序,而不是我运行的/bin/sh,这其实就是Docker对我们运行的进程使用了障眼法,对被隔离的应用的进程空间做了手脚,使得这个进程只能看到重新计算过的进程编号,比如 PID=1。这其实就是Linux当中的Namespace机制,这里的PID namespace被隔离后就会呈现这样的效果。

除此以外,Linux还提供了Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

这就是Docker对于隔离机制的实现。下面我来深入的了解一下

namesapce 名称 隔离的资源
Mount Linux 内核实现的第一个 Namespace,Mount points(文件系统挂载点)
IPC System V IPC(信号量、消息队列、共享内存) 和POSIX MESSAGE QUEUES
Network Network devices、stacks、ports(网络设备、网络栈、端口等)
PID Process IDs(进程编号)
User User and Groups IDs(用户和用户组)
UTS Hostname and NIS domain name(主机名与NIS域名)

同样的道理,我们去观察一个运行的docker容器,然后手动的去模拟实现它的隔离,为了方便理解,先确认宿主机存在的内容,并且制造一些可以用来区别的数据

[root@cluster1 ~]$ touch /root/host.txt
# 宿主机上制作一个挂载点
[root@cluster1 ~]$ mkdir /tmp/tmpfs
[root@cluster1 ~]$ mount -t tmpfs -o size=20m tmpfs /tmp/tmpfs
[root@cluster1 ~]$ df -h /tmp/tmpfs/
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            20M     0   20M   0% /tmp/tmpfs
# 查看宿主机主机名
[root@cluster1 ~]$ hostname
cluster1

# 查看宿主机进程
[root@cluster1 ~]$ ps aux  | head
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.2 191056  3960 ?        Ss   21:30   0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root          2  0.0  0.0      0     0 ?        S    21:30   0:00 [kthreadd]
root          4  0.0  0.0      0     0 ?        S<   21:30   0:00 [kworker/0:0H]
root          6  0.0  0.0      0     0 ?        S    21:30   0:00 [ksoftirqd/0]
root          7  0.0  0.0      0     0 ?        S    21:30   0:00 [migration/0]
....

# 为了验证IPC namespace,需要创建一个系统间通信队列
# ipcmk -Q 命令:用来创建系统间通信队列。
# ipcs -q 命令:用来查看系统间通信队列列表。
[root@cluster1 ~]$ ipcmk -Q 
[root@cluster1 ~]$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x48d07ddf 0          root       644        0            0 

# 查看宿主机用户数量个数
[root@cluster1 ~]$ cat /etc/passwd | wc -l
20

# 查看宿主机网络
[root@cluster1 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:0c:29:88:77:6c brd ff:ff:ff:ff:ff:ff
    inet 10.10.1.100/24 brd 10.10.1.255 scope global noprefixroute ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe88:776c/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:b7:30:58:e4 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:b7ff:fe30:58e4/64 scope link 
       valid_lft forever preferred_lft forever
       
# 观察宿主机内核
[root@cluster1 ~]$ uname -a
Linux cluster1 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

下面开始观察docker容器

# 启动一个容器
[root@cluster1 ~]$ docker run -it busybox /bin/sh

# 完全独立的目录(非宿主机目录)
/ $ ls /
bin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var
/ $ ls /root
/ $ df -h /tmp/tmpfs/
Filesystem                Size      Used Available Use% Mounted on
df: /tmp/tmpfs/: can't find mount point

# 独立的主机名
/# hostname 
d87b4acb91fc

# 独立的进程
/$ ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps aux
    
# 独立的系统通信队列
/$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

# 独立的用户
/$ cat /etc/passwd | wc -l
9

# 独立的网络
/$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

# 相同的宿主机内核
/$ uname -a
Linux d87b4acb91fc 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 GNU/Linux

OK,现在验证了我们上面的观点,容器是被沙河隔离的,下面我们再来看看是如何实现的?

我们进行一下,用namespace模拟这个过程,看看我们能否达到docker的效果

首先我们先来了解一个命令—unshare

作用:一个用来取消与父进程共享指定的命名空间

[root@cluster1 ~]# unshare --help

Usage: unshare [options] <program> [<argument>...]

Run a program with some namespaces unshared from the parent.

Options: -m, --mount unshare mounts namespace -u, --uts unshare UTS namespace (hostname etc) -i, --ipc unshare System V IPC namespace -n, --net unshare network namespace -p, --pid unshare pid namespace -U, --user unshare user namespace -f, --fork fork before launching <program> --mount-proc[=<dir>] mount proc filesystem first (implies --mount) -r, --map-root-user map current user to root (implies --user) --propagation <slave|shared|private|unchanged> modify mount propagation in mount namespace

# 我们通过unshare模拟容器的创建
# CentOS7 默认允许创建的 User Namespace 为 0
# 所以需要先echo 65535 > /proc/sys/user/max_user_namespaces 打开限制
[root@cluster1 ~]$ echo 65535 > /proc/sys/user/max_user_namespaces
[root@cluster1 ~]$ unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash

# 没有独立的目录(独立思考一下,后面会讲)
[root@cluster1 ~]$ ls /root/host.txt 
/root/host.txt

# 没有独立的mount挂载(独立思考一下,后面会讲)
[root@cluster1 ~]$ df -h /tmp/tmpfs/
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            20M     0   20M   0% /tmp/tmpfs

# 看起来没有独立的主机名
[root@cluster1 ~]$ hostname
cluster1

# 独立的进程
[root@cluster1 ~]$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.1 116204  2912 pts/1    S    22:58   0:00 /bin/bash
root         25  0.0  0.0 155472  1852 pts/1    R+   23:00   0:00 ps aux

# 独立的系统通信队列
[root@cluster1 ~]$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

# 独立的网络
[root@cluster1 ~]$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

# 看起来没有独立的用户
[root@cluster1 ~]$ cat /etc/passwd | wc -l
20

从上边论证,我们还缺乏几个没有隔离的挂载点、主机名、目录和用户,我们接着分析

# 我们在执行'unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash' 之后的终端继续验证

# 尝试修改主机名
$ hostname -b yijiuweishu
$ hostname
yijiuweishu

# 新增挂载点
$ mkdir /tmp/containerfs
$ mount -t tmpfs -o size=20m tmpfs /tmp/containerfs
$ df -h /tmp/containerfs/
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            20M     0   20M   0% /tmp/containerfs

在新开启一个终端,去查看宿主机的主机名和挂载点

[root@cluster1 ~]$ hostname
cluster1

[root@cluster1 ~]$  df -h /tmp/containerfs/
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2        49G  2.4G   47G   5% /

可以看到实际上,主机名和挂载点也是独立的,只不过刚取消共享那一刻,我们不做操作,主机名和挂载点是和宿主机一致的。

再去验证user namespace

  • User Namespace 主要是用来隔离用户和用户组的。

  • 一个比较典型的应用场景就是在主机上以非 root 用户运行的进程可以在一个单独的 User Namespace 中映射成 root 用户。使用 User Namespace 可以实现进程在容器内拥有 root 权限,而在主机上却只是普通用户。

  • 而不是以root用户映射成root用户,这样映射后还是有root的权限

# 我们体验下用普通用户去取消共享user namespace
[root@cluster1 ~]$ useradd test
[root@cluster1 ~]$ su - test
Last login: Fri Mar 17 23:08:56 CST 2023 on pts/2

[test@cluster1 ~]$ unshare --user -r --fork /bin/bash
# 可以看到,虽然我们变成了root用户,但是并不能调用一些root能执行的命令,由此可以推断我们是进行了用户隔离
[root@cluster1 ~]$ reboot 
Failed to open /dev/initctl: Permission denied
Failed to talk to init daemon.

再来说一下我们看见的还是宿主机目录,这里会引申出一个新的概念

根文件系统rootfs

根文件系统首先是内核启动时所mount(挂载)的第一个文件系统,内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。

根文件系统一般叫做rootfs,这里所谓的文件系统并不是指FAT、FAT32、NTFS、XFS这类的文件系统,熟悉Linux的人都知道,Linux一切皆文件,且目录结构都在 /下开始,这里的根文件系统指的就是/

namespace虽然解决了共享问题,但没有改变容器的根目录,所以容器中是通过rootfs来实现的。

切换新的根目录,我们需要一个根文件系统(有自己的/bin/dev/sys/proc 等)

# 获取一个根文件系统
# 方式一
$ wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz

# 方式二(docker每个容器也有自己的根文件系统,我们拷贝一份)
$ docker run -d -i  busybox /bin/sh
$ docker ps
CONTAINER ID   IMAGE     COMMAND     CREATED         STATUS         PORTS     NAMES
6bd3747bc339   busybox   "/bin/sh"   3 minutes ago   Up 3 minutes             heuristic_napier
$ docker export 6bd3747bc339 -o busybox.tar
$ tar xf busybox.tar 
$ ls
bin  busybox.tar  dev  etc  home  lib  lib64  proc  root  sys  tmp  usr  var

chroot 隔离

chroot是在 Unix 和 Linux 系统的一个操作,针对正在运作的软件行程和它的子进程,改变的根目录。使它不能对改变后的目录之外的访问(读写、查看)

  • chroot 是通过指定 新的根目录 和运行的命令组成,所以这意味着新的根目录中也要有可执行的命令和层次结构
# chroot NEWROOT [COMMAND [ARG]...]
# 注意 此处的COMMAND需要是新的根目录中存在的
[root@cluster1 ~]$ pwd
/root
[root@cluster1 ~]$ mkdir rootfs
[root@cluster1 ~]$ docker ps
CONTAINER ID   IMAGE     COMMAND     CREATED         STATUS         PORTS     NAMES
6bd3747bc339   busybox   "/bin/sh"   3 minutes ago   Up 3 minutes             heuristic_napier
[root@cluster1 ~]$ docker export 6bd3747bc339 -o busybox.tar

# 将busybox的根文件系统 放到/root/rootfs/下
[root@cluster1 ~]$ tar xf busybox.tar -C rootfs/



# 切换根目录
[root@cluster1 ~]$ chroot rootfs/ /bin/bash
# 这表示busybox中,不存在bash终端
chroot: failed to run command ‘/bin/bash’: No such file or director

[root@cluster1 ~]$ chroot rootfs/ /bin/sh
/ # /bin/ls
bin    dev    etc    home   lib    lib64  proc   root   sys    tmp    usr    var
/ # /bin/ls /root
/ #

pivot_root隔离

pivot_root把当前进程的root文件系统放在put_old目录,而使new_root成为新的root文件系统

  • new_root 与 put_old 必须是文件夹

  • new_root文件夹必须是一个挂载点 ,并且new_root文件夹里面有完整rootfs的各种文件

  • new_root 文件夹挂载应该是一个与主机不同的namespace

  • put_old文件夹必须在new_root文件夹内

  • 这从某方面也解释了,为什么有时候docker run一个容器后,宿主机会多一个挂载点(/var/lib/docker/overlay2/xxx/merged)

  • pviot_root主要是把整个系统切换到一个新的root目录,然后去掉对之前root文件系统的依赖,以便于可以umount 之前的文件系统(pivot_root需要root权限)
  • chroot是只改变即将运行的某进程的根目录,而系统的其他部分依旧依赖于老的root文件系统

在Docker中,会优先调用pivot_root,如果系统不支持才会使用 chroot

# 宿主机/root 目录
[root@cluster1 ~]$ ls
anaconda-ks.cfg  busybox.tar  host.txt rootfs

[root@cluster1 ~]$ unshare --mount --fork /bin/bash         #需要有独立的命名空间
[root@cluster1 ~]$ mkdir /new_root
[root@cluster1 ~]$ mount -t tmpfs mytmpfs  /new_root        #new_root是一个独立的挂载点
[root@cluster1 ~]$ mkdir /new_root/old_root                 # 可以是任意名字,
[root@cluster1 ~]$ tar xf busybox.tar -C /new_root

# busybox文件系统的/root
[root@cluster1 ~]$ ls /new_root/root
[root@cluster1 ~]$ touch /new_root/root/container.txt
[root@cluster1 ~]$ ls /new_root/root/
container.txt

# pivot_root 切换根文件系统
[root@cluster1 ~]$ pivot_root /new_root /new_root/old_root
[root@cluster1 ~]$ ls   
bash: /usr/bin/ls: No such file or directory                # 没有环境变量了
[root@cluster1 ~]$ /bin/ls /root/
container.txt

从另一个角度验证

Linux中每个进程都会记录其所使用的namespace(一串id),下面我们来找一找

我们要知道一个环境变量, $$ 用于表示当前的终端所在的进程ID

# 首先查看当前系统所用的namespace
[root@cluster1 ~]$ echo $$
5210
[root@cluster1 ~]$ ls -l /proc/$$/ns 
total 0
lrwxrwxrwx 1 root root 0 Mar 18 00:22 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 uts -> uts:[4026531838]

# 查看一个容器所用的namesapce
[root@cluster1 ~]$ docker run -it busybox /bin/sh
/ # echo $$
1
/ # ls -l /proc/1/ns/
total 0
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 ipc -> ipc:[4026532700]
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 mnt -> mnt:[4026532698]
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 net -> net:[4026532703]
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 pid -> pid:[4026532701]
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 user -> user:[4026531837]
lrwxrwxrwx    1 root     root             0 Mar 17 16:23 uts -> uts:[4026532699]


# 再来试试我们手动实现的namesapce 隔离,是否生效
[root@cluster1 ~]$ unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash
[root@cluster1 ~]$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 18 00:25 ipc -> ipc:[4026532699]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 mnt -> mnt:[4026532697]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 net -> net:[4026532702]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 pid -> pid:[4026532700]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 user -> user:[4026532633]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 uts -> uts:[4026532698]

从这个角度看,我们手动unshare的也都是独立的namespace,也论证了我们的观点。

当然Docker可不仅仅只是创建了namesapce,实际上还做了很多细节上的处理

Cgroup资源限制

通过namespace可以保证容器之间的隔离,但是无法控制每个容器可以占用多少资源, 如果其中的某一个容器正在执行 CPU 密集型的任务,那么就会影响其他容器中任务的性能与执行效率,导致多个容器相互影响并且抢占资源。如何对多个容器的资源使用进行限制就成了解决进程虚拟资源隔离之后的主要问题。

什么是Cgroup?

Control Groups(简称 CGroups)就是能够隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。每一个 CGroup 都是一组被相同的标准和参数限制的进程。而我们需要做的,其实就是把容器这个进程加入到指定的Cgroup中。

类似如上的图片表示,创建了 2 个 cgroup(每个 cgroup 有 4 个进程),并且限制它们各自最多只能使用 2GB 的内存。如果使用超过 2GB 的内存,那么将会触发 OOM(Out Of Memory) 。

cgroup原理和用法并不复杂,但其内核数据结构特别复杂,错综复杂的数据结构感觉才是cgroup真正的难点,了解即可。

感兴趣可以看看:https://github.com/dongzhiyan-stack/kernel-code-comment/blob/master/linux-3.10.96/kernel/cgroup.c

Cgroup的特点

  • cgroups的API以一个伪文件系统的方式实现,用户态的程序可以通过文件操作实现cgroups 的组织

  • cgroups的组织管理操作单元可以细粒到线程级别,用户可以创建销毁cgroups,从而实现资源再分配管理

  • 所有资源管理的功能都以子系统方式实现,接口统一

  • 子任务创建之初与其父进程处于同一个cgroups的控制组

Cgroup的作用

作用 说明
资源限制 cgroups可以对任务使用的资源总额进行限制,如设定应用运行时使用的内存上限,一但超过这个配额就发出OOM提示
优先级分配 通过分配的CPU时间片数量及磁盘IO带宽大小,实际就相当于控制了任务运行的优先级
资源统计 cgroups 可以统计系统的资源使用量,如CPU时长,内存使用量,这个功能非常适用于计费
任务控制 cgroups可以对任务执行挂起

Cgroup的术语

术语 说明
task(任务) 在cgroups的术语中,任务表示系统的一个进程或者线程
cgroups(控制组) cgroups中的资源控制都以cgroups以单位实现,cgroups表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统,一个任务加入某个cgroups,也可以从某个cgroups签到另一cgroups
subsystem(子系统) cgroups中的子系统就是一个资源调度控制器。CPU子系统可以控制从CPU时间分配,内存子系统可以设置cgroups的内存使用量
hierarchy(层级) 层级由一系列cgroups以一个树状结构排列而成,每个层级通过绑定对应的子系统进行资源控制。层级中cgroups的节点可以包含零个节点或多个子节点,子节点继承父节点挂载的子系统。整个操作系统可以有多个层级

Cgroup子系统

一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。子系统是真正实现某类资源的限制的基础。

Subsystem(子系统) cgroups 中的子系统就是一个资源调度控制器(又叫 controllers)

最终在Linux表现其实是一个文件系统

下面我们在Centos7上查看一下Cgroup的子系统(Centos7 默认systemd也会使用cgroup)

# 控制cgroup可以使用命令工具行,lssubsys
[root@cluster1 ~]$ yum install -y libcgroup-tools

[root@cluster1 ~]$ lssubsys -m 
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids

# 通过查看文件也是一样
[root@cluster1 ~]$ ll /sys/fs/cgroup/
total 0
drwxr-xr-x 2 root root  0 Mar 18 11:57 blkio
lrwxrwxrwx 1 root root 11 Mar 18 11:57 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Mar 18 11:57 cpuacct -> cpu,cpuacct
drwxr-xr-x 2 root root  0 Mar 18 11:57 cpu,cpuacct
drwxr-xr-x 2 root root  0 Mar 18 11:57 cpuset
drwxr-xr-x 3 root root  0 Mar 18 12:01 devices
drwxr-xr-x 2 root root  0 Mar 18 11:57 freezer
drwxr-xr-x 2 root root  0 Mar 18 11:57 hugetlb
drwxr-xr-x 2 root root  0 Mar 18 11:57 memory
lrwxrwxrwx 1 root root 16 Mar 18 11:57 net_cls -> net_cls,net_prio
drwxr-xr-x 2 root root  0 Mar 18 11:57 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Mar 18 11:57 net_prio -> net_cls,net_prio
drwxr-xr-x 2 root root  0 Mar 18 11:57 perf_event
drwxr-xr-x 2 root root  0 Mar 18 11:57 pids
drwxr-xr-x 4 root root  0 Mar 18 11:57 systemd

# 通过挂载点查看关联的cgroup子系统
[root@cluster1 ~]$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
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/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
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/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
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)
  • cpu:使用调度程序控制任务对cpu的使用

  • cpuacct:自动生成cgroup中任务对cpu资源使用情况的报告

  • cpuset:主要用于设置CPU和内存的亲和性

  • blkio:块设备 I/O 限制

  • devices:可以开启或关闭cgroup中任务对设备的访问

  • freezer: 可以挂起或恢复cgroup中的任务

  • pids:限制任务数量

  • memory:可以设定cgroup中任务对内存使用量的限定,并且自动生成这些任务对内存资源使用情况的报告

  • perf_event:增加了对每 group 的监测跟踪的能力,可以监测属于某个特定的 group 的所有线程以及运行在特定CPU上的线程

  • net_cls:docker没有直接使用它,它通过使用等级识别符标记网络数据包,从而允许linux流量控制程序识别从具体cgroup中生成的数据包

Cgroup控制组

控制组 说白了就是一组进程(进程组),cgroup 就是用来限制 控制组 的资源使用。为了能够方便地向一个 控制组 添加或者移除进程(在命令行也能操作),内核使用了 虚拟文件系统 来进行管理 控制组。

控制组可以类比成Linux的目录树结构,由于目录有层级关系,所以 控制组 也有层级关系

每个控制组目录中,都有一个名为 tasks 的文件,用于保存当前 控制组 包含的进程列表。如果我们想向某个 控制组 添加一个进程时,可以把进程的 PID 写入到 tasks 文件中即可。

在 Linux 内核中,可以存在多个 层级(控制组树),每个层级可以关联一个或多个 资源控制子系统,但同一个 资源控制子系统 不能关联到多个层级中。如下图所示:

直白来说,即/cgrp4关联在/cgrp1下,所以不能再直接关联/sys/fs/cgroup/的某个子系统了

# 如果用户想把资源控制子系统关联到其他层级,那么可以使用 mount 命令来进行挂载

# 将内存子系统重新关联到 /sys/fs/cgroup/memory 这个层级
mount -t cgroup -o memory memory /sys/fs/cgroup/memory

动手操作

为了不被冗余的篇幅影响阅读体验,这里以cpuset设置独占cpu举例

其他的子系统请参考我的另一篇文章:

cpuset子系统

先来看下cpuset子系统下的文件作用

文件 说明
cpuset.cpus 允许cgroup中的进程使用的CPU列表。如0-2,16代表 0,1,2,16这4个CPU
cpuset.cpu_exclusive cgroup是否独占cpuset.cpus 中分配的cpu 。(默认值0,共享;1,独占),如果设置为1,其他cgroup内的cpuset.cpus值不能包含有该cpuset.cpus内的值
cpuset.mems 允许cgroup中的进程使用的内存节点列表。如0-2,16代表 0,1,2,16这4个可用节点
cpuset.mem_exclusive 是否独占memory,(默认值0,共享;1,独占)
cpuset.mem_hardwall cgroup中任务的内存是否隔离,(默认值0,不隔离;1,隔离,每个用户的任务将拥有独立的空间)
cpuset.memory_pressure 衡量cpuset中内存分页压力的大小
cpuset.memory_spread_page 是否在允许的节点上均匀分布页面缓存
cpuset.memory_spread_slab 是否将 slab 缓存均匀分布在允许的节点上
cpuset.sched_load_balance 是否在该 cpuset 上的 CPU 内进行负载平衡
cpuset.sched_relax_domain_level 迁移任务时的搜索范围
cpuset.memory_pressure_enabled 仅在root cgroup中存在,表示是否计算内存压力

简单了解一下stress命令

stress --cpu 2 --timeout 600

表示使用stress命令模拟将2个CPU使用到百分之百,一会我们只绑定一个CPU,用stress压一下,看看会有几个CPU被占满

# 由于centos7已经使用了cgroup,所以不需要我们额外挂载
# mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset

cd /sys/fs/cgroup/cpuset
mkdir wzp
cd wzp
echo 1 > cpuset.cpus
echo 0 > cpuset.mems
/bin/echo $$ > tasks
bash
# 这里执行之后,我们新执行的bash终端是在wzp这个cgroup的限制中的,也就是说,他仅可以使用1号CPU
cat /proc/self/cpuset


# 执行命令,模拟让两个CPU 100%
stress --cpu 2 --timeout 600

# 另开一个终端,top看看实际情况

PS:要删除/sys/fs/cgroup/xxx/下 自己创建的目录,不能用 rm -rf 只能用rmdir

观察Docker容器

docker的cgroup存放在/sys/fs/cgroups/某子系统/docker/<container-ID>

下面我们启动一个docker,并设置上cpuset、cpu和memory限制来验证我们的观点

# 设置nginx容器使用CPU0-1,共享CPU512,内存大小500m
[root@cluster1 ~]$ docker run -d --cpuset-cpus="0-1" --cpu-shares=512 --memory=500m nginx:alpine
4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6

# 查看cgroup配置
[root@cluster1 ~]$ cat /sys/fs/cgroup/cpuset/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/cpuset.cpus 
0-1
[root@cluster1 ~]$ cat /sys/fs/cgroup/cpu/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/cpu.shares 
512
[root@cluster1 ~]$ cat /sys/fs/cgroup/memory/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/memory.limit_in_bytes 
524288000

下一篇,Docker的实现原理——网络

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容