底层技术
一、Namespace
Namespace是Linux内核的一项功能,该功能对内核资源进行分区,以使一组进程看到一组资源,而另一组进程看到另一组资源。Namespace的工作方式通过为一组资源和进程设置相同的Namespace而起作用,但是这些Namespace引用了不同的资源。资源可能存在于多个Namespace中。这些资源可以是进程ID、主机名、用户ID、文件名、与网络访问相关的名称和进程间通信。
Linux 5.6 内核中提供了 8 种类型的 Namespace:
Namespace |
作用 |
内核版本 |
Mount(mnt) |
隔离挂载点 |
2.4.19 |
Process ID(pid) |
隔离进程ID |
2.6.24 |
Network(net) |
隔离网络设备,端口号等 |
2.6.29 |
Interprocess Communication (ipc) |
隔离SystemVIPC和POSIX message queues |
2.6.19 |
UTS Namespace(uts) |
隔离主机名和域名 |
2.6.19 |
User Namespace(user) |
隔离用户和用户组 |
3.8 |
Control group (cgroup) Namespace |
隔离Cgroups根目录 |
4.6 |
Time Namespace |
隔离系统时间 |
5.6 |
虽然 Linux 内核提供了8种 Namespace,但是最新版本的 Docker 只使用了其中的前6 种,分别为Mount Namespace、PID Namespace、Net Namespace、IPC Namespace、UTS Namespace、User Namespace。
从3.8版本的内核开始,用户可以在/proc/[pid]/ns目录下看到指向不同namespace的文件,效果如下图,形如[4026531839]者即为namespace号。
| [root@localhost ~]# ls -l /proc/1061/ns
total 0
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Oct 29 23:12 uts -> uts:[4026531838]
|
如果两个进程指向的namespace编号相同,就说明它们在同一个namespace下,否则便在不同namespace里面。
1. Mount Namespace
Mount Namespace 是 Linux 内核实现的第一个 Namespace,从内核的 2.4.19 版本开始加入。它可以用来隔离不同的进程或进程组看到的挂载点。通俗地说,就是可以实现在不同的进程中看到不同的挂载目录。使用 Mount Namespace 可以实现容器内只能看到自己的挂载信息,在容器内的挂载操作不会影响主机的挂载目录。
Mount Namespace演示
unshare 是 util-linux 工具包中的一个工具,CentOS 7 系统默认已经集成了该工具,使用 unshare 命令可以实现创建并访问不同类型的 Namespace。
首先我们使用以下命令创建一个 bash 进程并且新建一个 Mount Namespace:
| unshare --mount --fork /bin/bash
|
执行完上述命令后,这时我们已经在主机上创建了一个新的 Mount Namespace,并且当前命令行窗口加入了新创建的 Mount Namespace。
在独立的 Mount Namespace 内创建挂载目录是不影响主机的挂载目录的。
1
2
3
4
5
6
7
8
9
10
11
12
13 | [root@localhost ~]# mkdir /tmp/tmpfs
[root@localhost ~]# # 使用 mount 命令挂载一个 tmpfs 类型的目录
[root@localhost ~]# mount -t tmpfs -o size=20m tmpfs /tmp/tmpfs
[root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 37G 2.5G 35G 7% /
devtmpfs 898M 0 898M 0% /dev
tmpfs 910M 0 910M 0% /dev/shm
tmpfs 910M 0 910M 0% /sys/fs/cgroup
tmpfs 910M 9.6M 901M 2% /run
tmpfs 182M 0 182M 0% /run/user/0
/dev/sda1 1014M 151M 864M 15% /boot
tmpfs 20M 0 20M 0% /tmp/tmpfs
|
可以看到 /tmp/tmpfs 目录已经被正确挂载。
新打开一个命令行窗口,同样执行 df -h命令查看主机的挂载信息:
| [root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 898M 0 898M 0% /dev
tmpfs 910M 0 910M 0% /dev/shm
tmpfs 910M 9.6M 901M 2% /run
tmpfs 910M 0 910M 0% /sys/fs/cgroup
/dev/mapper/centos-root 37G 2.5G 35G 7% /
/dev/sda1 1014M 151M 864M 15% /boot
tmpfs 182M 0 182M 0% /run/user/0
|
通过上面输出可以看到主机上并没有挂载 /tmp/tmpfs,可见我们独立的 Mount Namespace 中执行 mount 操作并不会影响主机。
我们继续在当前命令行窗口查看一下当前进程的 Namespace 信息,命令如下:
| [root@localhost ~]# ls -l /proc/$$/ns/
total 0
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 mnt -> mnt:[4026532494]
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Oct 29 23:21 uts -> uts:[4026531838]
|
然后新打开一个命令行窗口,使用相同的命令查看一下主机上的 Namespace 信息:
| [root@localhost ~]# ls -l /proc/$$/ns/
total 0
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Oct 29 23:22 uts -> uts:[4026531838]
|
通过对比两次命令的输出结果,我们可以看到,除了 Mount Namespace 的 ID 值不一样外,其他Namespace 的 ID 值均一致。
通过以上结果我们可以得出结论,使用 unshare 命令可以新建 Mount Namespace,并且在新建的 Mount Namespace 内 mount 是和外部完全隔离的。
2. PID Namespace
PID Namespace 的作用是用来隔离进程。在不同的 PID Namespace 中,进程可以拥有相同的 PID 号,利用 PID Namespace 可以实现每个容器的主进程为 1 号进程,而容器内的进程在主机上却拥有不同的PID。例如一个进程在主机上 PID 为 122,使用 PID Namespace 可以实现该进程在容器内看到的 PID 为 1。
创建一个 bash 进程,并且新建一个 PID Namespace:
| unshare --pid --fork --mount-proc /bin/bash
|
执行完上述命令后,我们在主机上创建了一个新的 PID Namespace,并且当前命令行窗口加入了新创建的 PID Namespace。在当前的命令行窗口使用 ps aux 命令查看一下进程信息:
| root@localhost ~]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 115548 2096 pts/0 S 23:24 0:00 /bin/bash
root 12 0.0 0.0 155452 1856 pts/0 R+ 23:24 0:00 ps aux
|
通过上述命令输出结果可以看到当前 Namespace 下 bash 为 1 号进程,而且我们也看不到主机上的其他进程信息。
3. UTS Namespace
UTS Namespace 主要是用来隔离主机名的,它允许每个 UTS Namespace 拥有一个独立的主机名。例如我们的主机名称为 docker,使用UTS Namespace可以实现在容器内的主机名称为 mydocker或者其他任意自定义主机名。
使用 unshare 命令来创建一个 UTS Namespace:
| unshare --uts --fork /bin/bash
|
创建好 UTS Namespace 后,当前命令行窗口已经处于一个独立的 UTS Namespace 中,使用 hostname 命令设置一下主机名:
| [root@localhost ~]# hostname -b docker
[root@localhost ~]# hostname
docker
|
通过上面命令的输出,我们可以看到当前UTS Namespace 内的主机名已经被修改为docker。然后我们新打开一个命令行窗口,使用相同的命令查看一下主机的 hostname:
| [root@localhost ~]# hostname
localhost
|
可以看到主机的名称仍然为 localhost,并没有被修改。可见UTS Namespace可以用来隔离主机名。
4. IPC Namespace
IPC Namespace 主要是用来隔离进程间通信的。例如 PID Namespace 和 IPC Namespace 一起使用可以实现同一 IPC Namespace 内的进程彼此可以通信,不同 IPC Namespace 的进程却不能通信。
使用 unshare 命令来创建一个 IPC Namespace:
| unshare --ipc --fork /bin/bash
|
查看系统间通信队列列表
| [root@localhost ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
|
创建系统间通信队列
| [root@localhost ~]# ipcmk -Q
Message queue id: 0
|
再次查看当前IPC Namespace下的系统通信队列列表:
| [root@localhost ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x255268e0 0 root 644 0 0
|
当前已经成功创建了一个系统通信队列。新打开一个命令行窗口,使用ipcs -q 命令查看一下主机的系统通信队列:
| [root@192 ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
|
在单独的 IPC Namespace内创建的系统通信队列在主机上无法看到。即 IPC Namespace 实现了系统通信队列的隔离。
5. User Namespace
User Namespace主要是用来隔离用户和用户组的。一个比较典型的应用场景就是在主机上以非root用户运行的进程可以在一个单独的User Namespace中映射成root用户。使用User Namespace可以实现进程在容器内拥有 root 权限,而在主机上却只是普通用户。
创建一个 User Namespace:
| [centos@localhost ~]$ unshare --user -r /bin/bash
[root@localhost ~]#
|
CentOS7 默认允许创建的 User Namespace 为 0,如果执行上述命令失败( unshare 命令返回的错误为 unshare: unshare failed: Invalid argument ),需要使用以下命令修改系统允许创建的 User Namespace 数量,命令为:echo 65535 > /proc/sys/user/max_user_namespaces,然后再次尝试创建 User Namespace。
然后执行 id 命令查看一下当前的用户信息:
| [root@localhost ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
|
通过上面的输出可以看到在新的User Namespace内已经是root用户了。使用只有主机root用户才可以执行的reboot命令来验证一下,在当前命令行窗口执行 reboot 命令:
| [root@localhost ~]# reboot
Failed to open /dev/initctl: Permission denied
Failed to talk to init daemon.
|
可以看到,在新创建的User Namespace内虽然是 root 用户,但是并没有权限执行reboot命令。这说明在隔离的User Namespace中,并不能获取到主机的root权限,也就是说User Namespace实现了用户和用户组的隔离。
6. Net Namespace
Net Namespace 是用来隔离网络设备、IP 地址和端口等信息的。Net Namespace 可以让每个进程拥有自己独立的 IP 地址,端口和网卡信息。例如主机 IP 地址为 172.16.4.1 ,容器内可以设置独立的 IP 地址为 192.168.1.1。
同样用实例验证,我们首先使用 ip a 命令查看一下主机上的网络信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | [root@localhost ~]# 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:a8:63:dc brd ff:ff:ff:ff:ff:ff
inet 192.168.1.136/24 brd 192.168.1.255 scope global noprefixroute dynamic ens33
valid_lft 1092sec preferred_lft 1092sec
inet6 fe80::f4d6:a364:587f:861b/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:42:44:05:d1 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
|
创建一个 Net Namespace:
| unshare --net --fork /bin/bash
|
使用 ip a 命令查看一下网络信息:
| [root@localhost ~]# 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
|
可以看到,宿主机上有 lo、eth0、docker0 等网络设备,而新建的 Net Namespace 内则与主机上的网络设备不同。
使用不同的 Namespace,可以实现容器中的进程看不到别的容器的资源,但是有一个问题你是否注意到?容器内的进程仍然可以任意地使用主机的 CPU 、内存等资源,如果某一个容器使用的主机资源过多,可能导致主机的资源竞争,进而影响业务。那如果我们想限制一个容器资源的使用(如 CPU、内存等)应该如何做呢?
这里就需要用到 Linux 内核的另一个核心技术cgroups。
二、cgroups
cgroups(全称:control groups)是 Linux 内核的一个功能,它可以实现限制进程或者进程组的资源(如 CPU、内存、磁盘 IO 等)。
在 2006 年,Google 的工程师( Rohit Seth 和 Paul Menage 为主要发起人) 发起了这个项目,起初项目名称并不是cgroups,而被称为进程容器(process containers)。在 2007 年cgroups代码计划合入Linux 内核,但是当时在 Linux 内核中,容器(container)这个词被广泛使用,并且拥有不同的含义。为了避免命名混乱和歧义,进程容器被重名为cgroups,并在 2008 年成功合入 Linux 2.6.24 版本中。cgroups目前已经成为 systemd、Docker、Linux Containers(LXC) 等技术的基础。
cgroups 主要提供了如下功能:
- 资源限制: 限制资源的使用量,例如我们可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行。
- 优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级。
- 审计:计算控制组的资源使用情况。
- 控制:控制进程的挂起或恢复。
cgroups功能的实现依赖于三个核心概念:子系统、控制组、层级树。
-
子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
-
控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
-
层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组c1,限制了CPU可以使用1核,然后另外一个控制组c2想实现既限制CPU 使用1核,同时限制内存使用2G,那么c2就可以直接继承c1,无须重复定义CPU限制。
cgroups 的三个核心概念中,子系统是最核心的概念,因为子系统是真正实现某类资源的限制的基础。
1. cgroups 子系统实例
mount 命令查看一下当前系统已经挂载的cgroups信息:
1
2
3
4
5
6
7
8
9
10
11
12 | [root@localhost ~]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
|
通过输出,可以看到当前系统已经挂载了我们常用的cgroups子系统,例如 cpu、memory、pids 等我们常用的cgroups子系统。
这些子系统中,cpu 和 memory 子系统是容器环境中使用最多的子系统。
1.1 cpu 子系统
第一步:在 cpu 子系统下创建 cgroup
在相应的子系统下创建目录即可
| mkdir /sys/fs/cgroup/cpu/mydocker
|
查看一下新创建的目录变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | [root@localhost ~]# ls -l /sys/fs/cgroup/cpu/mydocker
total 0
-rw-r--r--. 1 root root 0 Oct 30 22:41 cgroup.clone_children
--w--w--w-. 1 root root 0 Oct 30 22:41 cgroup.event_control
-rw-r--r--. 1 root root 0 Oct 30 22:41 cgroup.procs
-r--r--r--. 1 root root 0 Oct 30 22:41 cpuacct.stat
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpuacct.usage
-r--r--r--. 1 root root 0 Oct 30 22:41 cpuacct.usage_percpu
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Oct 30 22:41 cpu.shares
-r--r--r--. 1 root root 0 Oct 30 22:41 cpu.stat
-rw-r--r--. 1 root root 0 Oct 30 22:41 notify_on_release
-rw-r--r--. 1 root root 0 Oct 30 22:41 tasks
|
由上可以看到我们新建的目录下被自动创建了很多文件,其中 cpu.cfs_quota_us 文件代表在某一个阶段限制的 CPU 时间总量,单位为微秒。例如,我们想限制某个进程最多使用 1 核 CPU,就在这个文件里写入 100000(100000 代表限制 1 个核) ,tasks 文件中写入进程的 ID 即可(如果要限制多个进程 ID,在 tasks 文件中用换行符分隔即可)。
此时,我们所需要的 cgroup 就创建好了。
第二步:创建进程,加入 cgroup
把当前运行的 shell 进程加入 cgroup,然后在当前 shell 运行 cpu 耗时任务(这里利用到了继承,子进程会继承父进程的 cgroup)。
使用以下命令将 shell 进程加入 cgroup 中:
| [root@localhost ~]# cd /sys/fs/cgroup/cpu/mydocker
[root@localhost mydocker]# echo $$ > tasks
[root@localhost mydocker]# cat tasks
1727
1777
|
其中第一个进程 ID 为当前 shell 的主进程,也就是说,当前 shell 主进程为 1727。
第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间
下面,我们使用以下命令制造一个死循环,来提升 cpu 使用率:
执行完上述命令后,我们新打开一个 shell 窗口,使用 top -p 命令查看当前 cpu 使用率,-p 参数后面跟进程 ID,我这里是 3485。
| [root@localhost ~]# top -p 1727
top - 22:46:43 up 29 min, 2 users, load average: 0.89, 0.22, 0.11
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 34.6 us, 7.7 sy, 0.0 ni, 57.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1863032 total, 1339788 free, 275472 used, 247772 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 1434404 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1727 root 20 0 115672 2140 1660 R 100.0 0.1 0:24.58 bash
|
通过上面输出可以看到1727这个进程被限制到了只能使用100 % 的 cpu,也就是1个核。说明我们使用 cgroup来限制cpu使用时间已经生效。此时,执行while循环的命令行窗口可以使用Ctrl+C退出循环。
为了进一步证实cgroup限制cpu的准确性,我们修改cpu限制时间为 0.5 核,命令如下:
| [root@localhost ~]# cd /sys/fs/cgroup/cpu/mydocker
[root@localhost mydocker]# echo 50000 > cpu.cfs_quota_us
|
同样使用上面的命令来制造死循环:
| # while true;do echo;done;
|
保持当前窗口,新打开一个 shell 窗口,使用 top -p 参数查看 cpu 使用率:
| [root@localhost ~]# top -p 1727
top - 22:49:42 up 31 min, 2 users, load average: 0.31, 0.42, 0.22
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 15.3 us, 5.1 sy, 0.0 ni, 79.3 id, 0.0 wa, 0.0 hi, 0.4 si, 0.0 st
KiB Mem : 1863032 total, 1337872 free, 277124 used, 248036 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 1432736 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1727 root 20 0 115672 2152 1672 R 50.0 0.1 1:27.29 bash
|
通过上面输出可以看到,此时 cpu 使用率已经被限制到了 50%,即 0.5 个核。
验证完 cgroup 限制 cpu,我们使用相似的方法来验证 cgroup 对内存的限制。
1.2 memroy 子系统
第一步:在 memory 子系统下创建 cgroup
| mkdir /sys/fs/cgroup/memory/mydocker
|
查看一下新创建的目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | [root@localhost ~]# ls -l /sys/fs/cgroup/memory/mydocker
total 0
-rw-r--r--. 1 root root 0 Oct 30 22:51 cgroup.clone_children
--w--w--w-. 1 root root 0 Oct 30 22:51 cgroup.event_control
-rw-r--r--. 1 root root 0 Oct 30 22:51 cgroup.procs
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.failcnt
--w-------. 1 root root 0 Oct 30 22:51 memory.force_empty
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.failcnt
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.limit_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.max_usage_in_bytes
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.slabinfo
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.tcp.failcnt
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.tcp.limit_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.tcp.max_usage_in_bytes
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.tcp.usage_in_bytes
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.kmem.usage_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.limit_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.max_usage_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.memsw.failcnt
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.memsw.limit_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.memsw.max_usage_in_bytes
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.memsw.usage_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.move_charge_at_immigrate
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.numa_stat
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.oom_control
----------. 1 root root 0 Oct 30 22:51 memory.pressure_level
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.soft_limit_in_bytes
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.stat
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.swappiness
-r--r--r--. 1 root root 0 Oct 30 22:51 memory.usage_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 22:51 memory.use_hierarchy
-rw-r--r--. 1 root root 0 Oct 30 22:51 notify_on_release
-rw-r--r--. 1 root root 0 Oct 30 22:51 tasks
|
其中memory.limit_in_bytes
文件代表内存使用总量,单位为 byte。
例如,这里我希望对内存使用限制为 1G,则向 memory.limit_in_bytes 文件写入 1073741824,命令如下:
| [root@localhost ~]# cd /sys/fs/cgroup/memory/mydocker
[root@localhost mydocker]# cat memory.limit_in_bytes
9223372036854771712
[root@localhost mydocker]# echo 1073741824 > memory.limit_in_bytes
[root@localhost mydocker]# cat memory.limit_in_bytes
1073741824
|
第二步:创建进程,加入 cgroup
同样把当前 shell 进程 ID 写入 tasks 文件内:
| [root@localhost mydocker]# cd /sys/fs/cgroup/memory/mydocker
[root@localhost mydocker]# echo $$ > tasks
[root@localhost mydocker]# cat tasks
2033
2049
|
第三步,执行内存测试工具,申请内存
需要安装工具 memtester
| [root@localhost mydocker]# memtester 1500M 1
memtester version 4.3.0 (64-bit)
Copyright (C) 2001-2012 Charles Cazabon.
Licensed under the GNU General Public License version 2 (only).
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 1500MB (1572864000 bytes)
got 1500MB (1572864000 bytes), trying mlock ...Killed
|
该命令会申请 1500 M 内存,并且做内存测试。由于上面我们对当前 shell 进程内存限制为 1 G,当 memtester 使用的内存达到 1G 时,cgroup 便将 memtester 杀死。
上面最后一行的输出结果表示 memtester 想要 1500 M 内存,但是由于 cgroup 限制,达到了内存使用上限,被杀死了,与我们的预期一致。
我们可以使用以下命令,降低一下内存申请,将内存申请调整为 500M:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | [root@localhost mydocker]# memtester 500M 1
memtester version 4.3.0 (64-bit)
Copyright (C) 2001-2012 Charles Cazabon.
Licensed under the GNU General Public License version 2 (only).
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 500MB (524288000 bytes)
got 500MB (524288000 bytes), trying mlock ...locked.
Loop 1/1:
Stuck Address : ok
Random Value : ok
Compare XOR : ok
Compare SUB : ok
Compare MUL : ok
Compare DIV : ok
Compare OR : ok
Compare AND : ok
Sequential Increment: ok
Solid Bits : ok
Block Sequential : ok
Checkerboard : ok
Bit Spread : ok
Bit Flip : ok
Walking Ones : ok
Walking Zeroes : ok
8-bit Writes : ok
16-bit Writes : ok
Done.
|
这里可以看到,此时 memtester 已经成功申请到 500M 内存并且正常完成了内存测试。
上面创建的cgroups如果不想使用了,直接删除创建的文件夹即可。
2. Docker是如何使用cgroups
首先,我们使用以下命令创建一个 nginx 容器:
| [root@localhost ~]# docker run -itd -m=1g busybox
3444837506a982a90ff805a1974242a1e2357ceac52307c7dbac8898f985c3c1
|
上述命令创建并启动了一个 nginx 容器,并且限制内存为 1G。然后我们进入cgroups内存子系统的目录,使用 ls 命令查看一下该目录下的内容:
| [root@localhost ~]# ls -l /sys/fs/cgroup/memory/docker/ | head
total 0
drwxr-xr-x. 2 root root 0 Oct 30 23:21 3444837506a982a90ff805a1974242a1e2357ceac52307c7dbac8898f985c3c1
-rw-r--r--. 1 root root 0 Oct 30 23:18 cgroup.clone_children
--w--w--w-. 1 root root 0 Oct 30 23:18 cgroup.event_control
-rw-r--r--. 1 root root 0 Oct 30 23:18 cgroup.procs
-rw-r--r--. 1 root root 0 Oct 30 23:18 memory.failcnt
--w-------. 1 root root 0 Oct 30 23:18 memory.force_empty
-rw-r--r--. 1 root root 0 Oct 30 23:18 memory.kmem.failcnt
-rw-r--r--. 1 root root 0 Oct 30 23:18 memory.kmem.limit_in_bytes
-rw-r--r--. 1 root root 0 Oct 30 23:18 memory.kmem.max_usage_in_bytes
|
可以看到 docker 的目录下有一个一串随机 ID 的目录,该目录即为我们上面创建的 busybox容器的 ID。然后我们进入该目录,查看一下该容器的 memory.limit_in_bytes 文件的内容。
| [root@localhost ~]# cd /sys/fs/cgroup/memory/docker/3444837506a982a90ff805a1974242a1e2357ceac52307c7dbac8898f985c3c1
[root@localhost 3444837506a982a90ff805a1974242a1e2357ceac52307c7dbac8898f985c3c1]# cat memory.limit_in_bytes
1073741824
|
可以看到内存限制值正好为 1G。
事实上,Docker 创建容器时,Docker 会根据启动容器的参数,在对应的 cgroups 子系统下创建以容器 ID 为名称的目录, 然后根据容器启动时设置的资源限制参数, 修改对应的 cgroups 子系统资源限制文件, 从而达到资源限制的效果。
cgroups 虽然可以实现资源的限制,但是不能保证资源的使用。例如,cgroups 限制某个容器最多使用 1 核 CPU,但不保证总是能使用到 1 核 CPU,当 CPU 资源发生竞争时,可能会导致实际使用的 CPU 资源产生竞争。