Docker 容器资源隔离与资源限制原理

一、Namespace

  • Namespace 是 Linux 内核的一个特性,该特性可以实现在同一主机系统中,对进程 ID、主机名、用户 ID、文件名、网络和进程间通信等资源的隔离,以使一组进程看到一组资源,而另一组进程看到另一组资源
  • Linux 5.6 内核中提供了 8 种类型的 Namespace 如下:
Namespace 名称 作用 内核版本
Mount 隔离挂载点 2.4.19
Process ID 隔离进程 ID 2.6.24
Network 隔离网络设备,端口号等 2.6.29
Interprocess Communication 隔离 System V IPC 和 POSIX message queues 2.6.19
UTS 隔离主机名和域名 2.6.19
User 隔离用户和用户组 3.8
Control group 隔离 Cgroups 根目录 4.6
Time 隔离系统时间 5.6
  • Docker 新建一个容器时,会创建以下六种 Namespace,然后将容器中的进程加入到这些 Namespace 中,使得 Docker 容器中的进程只能看到当前 Namespace 中的系统资源

1. Mount Namespace

  • 隔离不同进程或进程组看到的挂载点,不同 Namespace 的挂载操作互不影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建一个bash进程并新建一个Mount Namespace
$ sudo unshare --mount --fork /bin/bash

# 当前命令行窗口加入了新创建的Mount Namespace
root$ mkdir /tmp/mytmpfs
root$ mount -t tmpfs -o size=20m tmpfs /tmp/mytmpfs # 挂载一个tmpfs类型的目录
root$ df -h | grep mytmpfs
tmpfs 20M 0 20M 0% /tmp/mytmpfs
root$ ls -l /proc/self/ns/
lrwxrwxrwx 1 root root 0 Apr 30 15:54 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Apr 30 15:54 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Apr 30 15:54 mnt -> 'mnt:[4026532763]'
......

# 新窗口
$ sudo df -h | grep mytmpfs # 没有该挂载点
$ sudo ls -l /proc/self/ns/ # Namespace信息除了mnt的ID不一样,其他都一致
lrwxrwxrwx 1 root root 0 Apr 30 15:55 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Apr 30 15:55 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Apr 30 15:55 mnt -> 'mnt:[4026531841]'
......

2. PID Namespace

  • 隔离进程,不同 Namespace 中进程可以拥有相同的 PID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建一个bash进程并新建一个PID Namespace
$ sudo unshare --pid --fork --mount-proc /bin/bash

# 当前命令行窗口加入了新创建的PID Namespace
root$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 7196 3856 pts/1 S 16:10 0:00 /bin/bash
root 2 0.0 0.2 11040 4340 pts/1 R+ 16:10 0:00 ps aux

# 新窗口
$ sudo ps aux | grep /bin/bash
root 2811 0.0 0.2 10004 4632 pts/0 S+ 16:10 0:00 sudo unshare --pid --fork --mount-proc /bin/bash
root 2812 0.0 0.0 10004 480 pts/1 Ss 16:10 0:00 sudo unshare --pid --fork --mount-proc /bin/bash
root 2813 0.0 0.0 5504 976 pts/1 S 16:10 0:00 unshare --pid --fork --mount-proc /bin/bash
root 2814 0.0 0.1 7196 3892 pts/1 S+ 16:10 0:00 /bin/bash

3. UTS Namespace

  • 隔离主机名,不同 Namespace 可以有独立的主机名
1
2
3
4
5
6
7
8
9
10
11
# 创建一个bash进程并新建一个UTS Namespace
$ sudo unshare --uts --fork /bin/bash

# 当前命令行窗口加入了新创建的UTS Namespace
root$ hostname -b test
root$ hostname
test

# 新窗口
$ sudo hostname
server

4. IPC Namespace

  • 隔离进程间通信,通常和 PID Namespace 一起使用,实现同一 Namespace 内的进程可以彼此通信,不同 Namespace 的进程不能通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建一个bash进程并新建一个IPC Namespace
$ sudo unshare --ipc --fork /bin/bash

# 当前命令行窗口加入了新创建的IPC Namespace
root$ ipcs -q # 查看系统通信队列列表
------ Message Queues --------
key msqid owner perms used-bytes messages
root$ ipcmk -Q # 创建一个系统通信队列
Message queue id: 0
root$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x78109125 0 root 644 0 0

# 新窗口
$ sudo ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages

5. User Namespace

  • 隔离用户和用户组,可以让普通用户进程在一个单独的 User Namespace 中映射成 root 用户
1
2
3
4
5
6
7
8
# 创建一个bash进程并新建一个User Namespace(可以不使用root权限创建)
$ unshare --user -r /bin/bash

# 当前命令行窗口加入了新创建的User Namespace
root$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
root$ touch /etc/123 # 不是主机的root权限
touch: cannot touch '/etc/123': Permission denied
  • CentOS7 默认允许创建的 User Namespace 为 0,可以使用 echo 65535 > /proc/sys/user/max_user_namespaces 命令修改系统允许创建的 User Namespace 数量

6. Net Namespace

  • 隔离网络设备、IP 地址和端口等信息,可以让每个进程拥有自己独立的 IP 地址、端口和网卡信息
1
2
3
4
5
6
7
# 创建一个bash进程并新建一个Net Namespace
$ sudo unshare --net --fork /bin/bash

# 当前命令行窗口加入了新创建的Net Namespace
root$ 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

二、Cgroups

  • cgroups(control groups)是 Linux 内核的一个功能,它可以实现限制进程或者进程组的资源(如 CPU、内存、磁盘 I/O 等),但是不能保证资源的使用。例如限制某个容器最多使用 1 核 CPU,但不保证总是能使用到 1 核 CPU
  • 在 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 功能:
    • 资源限制:限制资源的使用量,例如限制某进程的内存上限
    • 优先级控制:不同的组可以有不同的资源使用优先级
    • 审计:统计控制组的资源使用情况
    • 控制:控制进程的挂起或恢复
  • cgroups 概念:
    • 子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量
    • 控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系
    • 层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的,使得控制组拥有父子关系,子控制组默认拥有父控制组的属性
  • 系统默认挂载了常用的 cgroups 子系统,例如 cpu、memory、pids
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ uname -a
Linux linux1 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

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

1. CPU 子系统

  • 限制进程的 cpu 使用时间
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
34
35
36
37
38
39
40
41
42
43
44
# 创建cgroup
$ mkdir /sys/fs/cgroup/cpu/mydocker
$ ls /sys/fs/cgroup/cpu/mydocker
-rw-r--r--. 1 root root 0 Apr 30 14:14 cgroup.clone_children
--w--w--w-. 1 root root 0 Apr 30 14:14 cgroup.event_control
-rw-r--r--. 1 root root 0 Apr 30 14:14 cgroup.procs
-r--r--r--. 1 root root 0 Apr 30 14:14 cpuacct.stat
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpuacct.usage
-r--r--r--. 1 root root 0 Apr 30 14:14 cpuacct.usage_percpu
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpu.cfs_quota_us # 限制CPU使用总量,单位微秒(100000代表1核)
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Apr 30 14:14 cpu.shares
-r--r--r--. 1 root root 0 Apr 30 14:14 cpu.stat
-rw-r--r--. 1 root root 0 Apr 30 14:14 notify_on_release
-rw-r--r--. 1 root root 0 Apr 30 14:14 tasks # 限制的进程ID,换行符分隔
$ echo 100000 > /sys/fs/cgroup/cpu/mydocker/cpu.cfs_quota_us

# 将当前shell进程加入cgroup中
$ cd /sys/fs/cgroup/cpu/mydocker
$ echo $$ > tasks
$ cat tasks
2838 # 当前shell主进程
2916

# 执行CPU耗时任务(子进程会继承父进程的cgroup)
$ while true;do echo;done;

# 新窗口查看
$ top -p 2838
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2838 root 20 0 116204 2868 1792 S 99.7 0.1 0:20.66 bash # 限制只能使用1核

# 修改cpu限制时间为0.5核
$ echo 50000 > cpu.cfs_quota_us

# 再次查看
$ top -p 2838
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2838 root 20 0 116204 2876 1800 S 50.3 0.1 0:55.65 bash # 限制只能使用0.5核

# 删除cgroups
$ rmdir /sys/fs/cgroup/cpu/mydocker/

2. Memroy 子系统

  • 限制进程申请的内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 创建cgroup
$ mkdir /sys/fs/cgroup/memory/mydocker
$ ls -l /sys/fs/cgroup/memory/mydocker
-rw-r--r--. 1 root root 0 Apr 30 14:37 memory.limit_in_bytes # 限制内存使用总量,单位byte(1073741824代表1G)
-rw-r--r--. 1 root root 0 Apr 30 14:37 tasks # 限制的进程ID,换行符分隔
......
$ echo 1073741824 > /sys/fs/cgroup/memory/mydocker/memory.limit_in_bytes

# 将当前shell进程加入cgroup中
$ cd /sys/fs/cgroup/memory/mydocker
$ echo $$ > tasks
$ cat tasks
2929 # 当前shell主进程
3355

# 申请内存(子进程会继承父进程的cgroup)
$ memtester 1500M 1
pagesize is 4096
pagesizemask is 0xfffffffffffff000
want 1500MB (1572864000 bytes)
got 1500MB (1572864000 bytes), trying mlock ...Killed # cgroup将进程杀死

# 删除cgroups
$ rmdir /sys/fs/cgroup/memory/mydocker/

3. Docker 使用 cgroups

  • Docker 创建容器时,会根据启动容器的参数,在对应的 cgroups 子系统下创建以容器 ID 为名称的目录,然后根据容器启动时设置的资源限制参数,修改对应的 cgroups 子系统资源限制文件,从而达到资源限制的效果
1
2
3
4
5
6
# 创建容器,限制内存为1G,
$ docker run -d -m=1g nginx
a705463041a974d449f3477c932fb3e7dc21b88ec44f85356ab0beffe9c30d7e

$ cat /sys/fs/cgroup/memory/docker/a705463041a974d449f3477c932fb3e7dc21b88ec44f85356ab0beffe9c30d7e/memory.limit_in_bytes
1073741824