什么是 Namespace?

下面是 Namespace 的维基百科定义:

Namespace 是 Linux 内核的一项功能,该功能对内核资源进行分区,以使一组进程看到一组资源,而另一组进程看到另一组资源。Namespace 的工作方式通过为一组资源和进程设置相同的 Namespace 而起作用,但是这些 Namespace 引用了不同的资源。资源可能存在于多个 Namespace 中。这些资源可以是进程 ID、主机名、用户 ID、文件名、与网络访问相关的名称和进程间通信。

简单来说,Namespace 是 Linux 内核的一个特性,该特性可以实现在同一主机系统中,对进程 ID、主机名、用户 ID、文件名、网络和进程间通信等资源的隔离。Docker 利用 Linux 内核的 Namespace 特性,实现了每个容器的资源相互隔离,从而保证容器内部只能访问到自己 Namespace 的资源。
最新的 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) 隔离 System V IPC 和 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。
下面,我们详细了解下 Docker 使用的 6 种 Namespace的作用分别是什么。

各种 Namespace 的作用?

(1)Mount Namespace

Mount Namespace 是 Linux 内核实现的第一个 Namespace,从内核的 2.4.19 版本开始加入。它可以用来隔离不同的进程或进程组看到的挂载点。通俗地说,就是可以实现在不同的进程中看到不同的挂载目录。使用 Mount Namespace 可以实现容器内只能看到自己的挂载信息,在容器内的挂载操作不会影响主机的挂载目录。

下面通过一个实例来演示下 Mount Namespace。在演示之前,先来认识一个命令行工具 unshare。unshare 是 util-linux 工具包中的一个工具,CentOS 7 系统默认已经集成了该工具,使用 unshare 命令可以实现创建并访问不同类型的 Namespace

首先使用以下命令创建一个 bash 进程并且新建一个 Mount Namespace:

  1. $ sudo unshare --mount --fork /bin/bash
  2. [root@centos7 centos]#

执行完上述命令后,这时已经在主机上创建了一个新的 Mount Namespace,并且当前命令行窗口加入了新创建的 Mount Namespace。下面通过一个例子来验证下,在独立的 Mount Namespace 内创建挂载目录是不影响主机的挂载目录的。

首先在 /tmp 目录下创建一个目录。

  1. [root@centos7 centos]# mkdir /tmp/tmpfs

创建好目录后使用 mount 命令挂载一个 tmpfs 类型的目录。命令如下:

  1. [root@centos7 centos]# mount -t tmpfs -o size=20m tmpfs /tmp/tmpfs

然后使用 df 命令查看一下已经挂载的目录信息:

  1. [root@centos7 centos]# df -h
  2. Filesystem Size Used Avail Use% Mounted on
  3. /dev/vda1 500G 1.4G 499G 1% /
  4. devtmpfs 16G 0 16G 0% /dev
  5. tmpfs 16G 0 16G 0% /dev/shm
  6. tmpfs 16G 0 16G 0% /sys/fs/cgroup
  7. tmpfs 16G 57M 16G 1% /run
  8. tmpfs 3.2G 0 3.2G 0% /run/user/1000
  9. tmpfs 20M 0 20M 0% /tmp/tmpfs

可以看到 /tmp/tmpfs 目录已经被正确挂载。为了验证主机上并没有挂载此目录,新打开一个命令行窗口,同样执行 df 命令查看主机的挂载信息:

  1. [centos@centos7 ~]$ df -h
  2. Filesystem Size Used Avail Use% Mounted on
  3. devtmpfs 16G 0 16G 0% /dev
  4. tmpfs 16G 0 16G 0% /dev/shm
  5. tmpfs 16G 57M 16G 1% /run
  6. tmpfs 16G 0 16G 0% /sys/fs/cgroup
  7. /dev/vda1 500G 1.4G 499G 1% /
  8. tmpfs 3.2G 0 3.2G 0% /run/user/1000

通过上面输出可以看到主机上并没有挂载 /tmp/tmpfs,可见我们独立的 Mount Namespace 中执行 mount 操作并不会影响主机。

为了进一步验证我们的想法,我们继续在当前命令行窗口查看一下当前进程的 Namespace 信息,命令如下:

  1. 1. [root@centos7 centos]# ls -l /proc/self/ns/
  2. 2. total 0
  3. 3. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 ipc -> ipc:[4026531839]
  4. 4. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 mnt -> mnt:[4026532239]
  5. 5. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 net -> net:[4026531956]
  6. 6. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 pid -> pid:[4026531836]
  7. 7. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 user -> user:[4026531837]
  8. 8. lrwxrwxrwx. 1 root root 0 Sep 4 08:20 uts -> uts:[4026531838]

然后新打开一个命令行窗口,使用相同的命令查看一下主机上的 Namespace 信息:

  1. 1. [centos@centos7 ~]$ ls -l /proc/self/ns/
  2. 2. total 0
  3. 3. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 ipc -> ipc:[4026531839]
  4. 4. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 mnt -> mnt:[4026531840]
  5. 5. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 net -> net:[4026531956]
  6. 6. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 pid -> pid:[4026531836]
  7. 7. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 user -> user:[4026531837]
  8. 8. lrwxrwxrwx. 1 centos centos 0 Sep 4 08:20 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。

下面通过一个实例来演示下 PID Namespace的作用。首先使用以下命令创建一个 bash 进程,并且新建一个 PID Namespace:

  1. $ sudo unshare --pid --fork --mount-proc /bin/bash
  2. [root@centos7 centos]#

执行完上述命令后,在主机上创建了一个新的 PID Namespace,并且当前命令行窗口加入了新创建的 PID Namespace。在当前的命令行窗口使用 ps aux 命令查看一下进程信息:

  1. [root@kubetest-3 ~]# ps -ef
  2. UID PID PPID C STIME TTY TIME CMD
  3. root 1 0 0 17:43 pts/1 00:00:00 /bin/bash
  4. root 12 1 0 17:43 pts/1 00:00:00 ps -ef

通过上述命令输出结果可以看到当前 Namespace 下 bash 为 1 号进程,而且也看不到主机上的其他进程信息。

(3)UTS Namespace

UTS Namespace 主要是用来隔离主机名的,它允许每个 UTS Namespace 拥有一个独立的主机名。例如主机名称为 docker,使用 UTS Namespace 可以实现在容器内的主机名称为 diyun 或者其他任意自定义主机名。
同样通过一个实例来验证下 UTS Namespace 的作用,首先使用 unshare 命令来创建一个 UTS Namespace:

  1. $ sudo unshare --uts --fork /bin/bash
  2. [root@centos7 centos]#

创建好 UTS Namespace 后,当前命令行窗口已经处于一个独立的 UTS Namespace 中,下面使用 hostname 命令(hostname 可以用来查看主机名称)设置一下主机名:

  1. [root@centos7 centos]# hostname -b diyun

然后再查看一下主机名:

  1. [root@centos7 centos]# hostname
  2. diyun

通过上面命令的输出,我们可以看到当前UTS Namespace 内的主机名已经被修改为 lagoudocker。然后我们新打开一个命令行窗口,使用相同的命令查看一下主机的 hostname:

  1. [centos@centos7 ~]$ hostname
  2. centos7

可以看到主机的名称仍然为 centos7,并没有被修改。由此,可以验证 UTS Namespace 可以用来隔离主机名。

(4)IPC Namespace

IPC Namespace 主要是用来隔离进程间通信的。例如 PID Namespace 和 IPC Namespace 一起使用可以实现同一 IPC Namespace 内的进程彼此可以通信,不同 IPC Namespace 的进程却不能通信。
通过一个实例来验证下IPC Namespace的作用,首先我们使用 unshare 命令来创建一个 IPC Namespace:

  1. 1. $ sudo unshare --ipc --fork /bin/bash
  2. 2. [root@centos7 centos]#

下面我们需要借助两个命令来实现对 IPC Namespace 的验证。

  • ipcs -q 命令:用来查看系统间通信队列列表。
  • ipcmk -Q 命令:用来创建系统间通信队列。

首先使用 ipcs -q 命令查看一下当前 IPC Namespace 下的系统通信队列列表:

  1. 1. [centos@centos7 ~]$ ipcs -q
  2. 2.
  3. 3. ------ Message Queues --------
  4. 4. key msqid owner perms used-bytes messages

由上可以看到当前无任何系统通信队列,然后使用 ipcmk -Q 命令创建一个系统通信队列:

  1. 1. [root@centos7 centos]# ipcmk -Q
  2. 2. Message queue id: 0

再次使用 ipcs -q 命令查看当前 IPC Namespace 下的系统通信队列列表:

  1. 1. [root@centos7 centos]# ipcs -q
  2. 2.
  3. 3. ------ Message Queues --------
  4. 4. key msqid owner perms used-bytes messages
  5. 5. 0x73682a32 0 root 644 0 0

可以看到已经成功创建了一个系统通信队列。然后新打开一个命令行窗口,使用ipcs -q 命令查看一下主机的系统通信队列:

  1. 1. [centos@centos7 ~]$ ipcs -q
  2. 2.
  3. 3. ------ Message Queues --------
  4. 4. key msqid owner perms used-bytes messages

通过上面的实验,可以发现,在单独的 IPC Namespace 内创建的系统通信队列在主机上无法看到。即 IPC Namespace 实现了系统通信队列的隔离。

(5)User Namespace

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

User Namesapce 的创建是可以不使用 root 权限的。下面以普通用户的身份创建一个 User Namespace,命令如下:

  1. [centos@centos7 ~]$ unshare --user -r /bin/bash
  2. [root@centos7 ~]#

CentOS7 默认允许创建的 User Namespace 为 0,如果执行上述命令失败( unshare 命令返回的错误为 unshare: unshare failed: Invalid argument ),需要使用以下命令修改系统允许创建的 User Namespace 数量,命令为:echo 65535 > /proc/sys/user/max_user_namespaces,然后再次尝试创建 User Namespace。

然后执行 id 命令查看一下当前的用户信息:

  1. [root@centos7 ~]# id
  2. uid=0(root) gid=0(root) groups=0(root),65534(nfsnobody) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

通过上面的输出可以看到在新的 User Namespace 内已经是 root 用户了。下面使用只有主机 root 用户才可以执行的 reboot 命令来验证一下,在当前命令行窗口执行 reboot 命令:

  1. 1. [root@centos7 ~]# reboot
  2. 2. Failed to open /dev/initctl: Permission denied
  3. 3. 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. $ ip a
  2. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
  3. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  4. inet 127.0.0.1/8 scope host lo
  5. valid_lft forever preferred_lft forever
  6. inet6 ::1/128 scope host
  7. valid_lft forever preferred_lft forever
  8. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
  9. link/ether 02:11:b0:14:01:0c brd ff:ff:ff:ff:ff:ff
  10. inet 172.20.1.11/24 brd 172.20.1.255 scope global dynamic eth0
  11. valid_lft 86063337sec preferred_lft 86063337sec
  12. inet6 fe80::11:b0ff:fe14:10c/64 scope link
  13. valid_lft forever preferred_lft forever
  14. 3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
  15. link/ether 02:42:82:8d:a0:df brd ff:ff:ff:ff:ff:ff
  16. inet 172.17.0.1/16 scope global docker0
  17. valid_lft forever preferred_lft forever
  18. inet6 fe80::42:82ff:fe8d:a0df/64 scope link
  19. valid_lft forever preferred_lft forever

然后我们使用以下命令创建一个 Net Namespace:

  1. $ sudo unshare --net --fork /bin/bash
  2. [root@centos7 centos]#

同样的我们使用 ip a 命令查看一下网络信息:
复制代码

  1. [root@centos7 centos]# ip a
  2. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
  3. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

可以看到,宿主机上有 lo、eth0、docker0 等网络设备,新建的 Net Namespace 内则与主机上的网络设备不同。

为什么 Docker 需要 Namespace?

Linux 内核从 2002 年 2.4.19 版本开始加入了 Mount Namespace,而直到内核 3.8 版本加入了 User Namespace 才为容器提供了足够的支持功能。
当 Docker 新建一个容器时, 它会创建这六种 Namespace,然后将容器中的进程加入这些 Namespace 之中,使得 Docker 容器中的进程只能看到当前 Namespace 中的系统资源。

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 功能及核心概念

cgroups 主要提供了如下功能。

  • 资源限制: 限制资源的使用量,例如我们可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行。
  • 优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级。
  • 审计:计算控制组的资源使用情况。
  • 控制:控制进程的挂起或恢复。

了解了 cgroups 可以为我们提供什么功能,下面我来看下 cgroups 是如何实现这些功能的。
cgroups功能的实现依赖于三个核心概念:子系统、控制组、层级树。

  • 子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器。例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间。
  • 控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系。例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组。
  • 层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的。这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组。比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制。

cgroups 的三个核心概念中,子系统是最核心的概念,因为子系统是真正实现某类资源的限制的基础。

cgroups 子系统实例

下面我通过一个实例演示一下在 Linux 上默认都启动了哪些子系统。
我们先通过 mount 命令查看一下当前系统已经挂载的cgroups信息:
复制代码

  1. 1. $ sudo mount -t cgroup
  2. 2. 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)
  3. 3. cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
  4. 4. cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
  5. 5. cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
  6. 6. cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
  7. 7. cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
  8. 8. cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
  9. 9. cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
  10. 10. cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
  11. 11. cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
  12. 12. cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)

我的操作系统版本为 CentOS7.8,内核为 3.10.0-1127.el7.x86_64 版本,不同内核版本cgroups子系统和使用方式可能略有差异。如果你对cgroups不是很熟悉,请尽量使用与我相同的内核环境操作。

通过输出,可以看到当前系统已经挂载了我们常用的cgroups子系统,例如 cpu、memory、pids 等我们常用的cgroups子系统。这些子系统中,cpu 和 memory 子系统是容器环境中使用最多的子系统,下面我对这两个子系统做详细介绍。

cpu 子系统

我首先以 cpu 子系统为例,演示一下cgroups如何限制进程的 cpu 使用时间。由于cgroups的操作很多需要用到 root 权限,我们在执行命令前要确保已经切换到了 root 用户,以下命令的执行默认都是使用 root 用户。
第一步:在 cpu 子系统下创建 cgroup
cgroups的创建很简单,只需要在相应的子系统下创建目录即可。下面我们到 cpu 子系统下创建测试文件夹:
复制代码

  1. 1. # mkdir /sys/fs/cgroup/cpu/mydocker

执行完上述命令后,我们查看一下我们新创建的目录下发生了什么?
复制代码

  1. 1. # ls -l /sys/fs/cgroup/cpu/mydocker
  2. 2. total 0
  3. 3. -rw-r--r--. 1 root root 0 Sep 5 09:19 cgroup.clone_children
  4. 4. --w--w--w-. 1 root root 0 Sep 5 09:19 cgroup.event_control
  5. 5. -rw-r--r--. 1 root root 0 Sep 5 09:19 cgroup.procs
  6. 6. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.cfs_period_us
  7. 7. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.cfs_quota_us
  8. 8. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.rt_period_us
  9. 9. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.rt_runtime_us
  10. 10. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpu.shares
  11. 11. -r--r--r--. 1 root root 0 Sep 5 09:19 cpu.stat
  12. 12. -r--r--r--. 1 root root 0 Sep 5 09:19 cpuacct.stat
  13. 13. -rw-r--r--. 1 root root 0 Sep 5 09:19 cpuacct.usage
  14. 14. -r--r--r--. 1 root root 0 Sep 5 09:19 cpuacct.usage_percpu
  15. 15. -rw-r--r--. 1 root root 0 Sep 5 09:19 notify_on_release
  16. 16. -rw-r--r--. 1 root root 0 Sep 5 09:19 tasks

由上可以看到我们新建的目录下被自动创建了很多文件,其中 cpu.cfs_quota_us 文件代表在某一个阶段限制的 CPU 时间总量,单位为微秒。例如,我们想限制某个进程最多使用 1 核 CPU,就在这个文件里写入 100000(100000 代表限制 1 个核) ,tasks 文件中写入进程的 ID 即可(如果要限制多个进程 ID,在 tasks 文件中用换行符分隔即可)。
此时,我们所需要的 cgroup 就创建好了。对,就是这么简单。
第二步:创建进程,加入 cgroup
这里为了方便演示,我先把当前运行的 shell 进程加入 cgroup,然后在当前 shell 运行 cpu 耗时任务(这里利用到了继承,子进程会继承父进程的 cgroup)。
使用以下命令将 shell 进程加入 cgroup 中:
复制代码

  1. 1. # cd /sys/fs/cgroup/cpu/mydocker
  2. 2. # echo $$ > tasks

查看一下 tasks 文件内容:
复制代码

  1. 1. # cat tasks
  2. 2. 3485
  3. 3. 3543

其中第一个进程 ID 为当前 shell 的主进程,也就是说,当前 shell 主进程为 3485。
第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间
下面,我们使用以下命令制造一个死循环,来提升 cpu 使用率:
复制代码

  1. 1. # while true;do echo;done;

执行完上述命令后,我们新打开一个 shell 窗口,使用 top -p 命令查看当前 cpu 使用率,-p 参数后面跟进程 ID,我这里是 3485。
复制代码

  1. 1. $ top -p 3485
  2. 2. top - 09:51:35 up 3 days, 22:00, 4 users, load average: 1.59, 0.58, 0.27
  3. 3. Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
  4. 4. %Cpu(s): 9.7 us, 2.8 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. 5. KiB Mem : 32779616 total, 31009780 free, 495988 used, 1273848 buff/cache
  6. 6. KiB Swap: 0 total, 0 free, 0 used. 31852336 avail Mem
  7. 7.
  8. 8. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  9. 9. 3485 root 20 0 116336 2852 1688 S 99.7 0.0 2:10.71 bash

通过上面输出可以看到 3485 这个进程被限制到了只能使用 100 % 的 cpu,也就是 1 个核。说明我们使用 cgroup 来限制 cpu 使用时间已经生效。此时,执行 while 循环的命令行窗口可以使用 Ctrl+c 退出循环。
为了进一步证实 cgroup 限制 cpu 的准确性,我们修改 cpu 限制时间为 0.5 核,命令如下:
复制代码

  1. 1. # cd /sys/fs/cgroup/cpu/mydocker
  2. 2. # echo 50000 > cpu.cfs_quota_us

同样使用上面的命令来制造死循环:
复制代码

  1. 1. # while true;do echo;done;

保持当前窗口,新打开一个 shell 窗口,使用 top -p 参数查看 cpu 使用率:
复制代码

  1. 1. $ top -p 3485
  2. 2. top - 10:05:25 up 3 days, 22:14, 3 users, load average: 1.02, 0.43, 0.40
  3. 3. Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
  4. 4. %Cpu(s): 5.0 us, 1.3 sy, 0.0 ni, 93.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. 5. KiB Mem : 32779616 total, 31055676 free, 450224 used, 1273716 buff/cache
  6. 6. KiB Swap: 0 total, 0 free, 0 used. 31898216 avail Mem
  7. 7.
  8. 8. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  9. 9. 3485 root 20 0 115544 2116 1664 R 50.0 0.0 0:23.39 bash

通过上面输出可以看到,此时 cpu 使用率已经被限制到了 50%,即 0.5 个核。
验证完 cgroup 限制 cpu,我们使用相似的方法来验证 cgroup 对内存的限制。

memroy 子系统

第一步:在 memory 子系统下创建 cgroup
复制代码

  1. 1. # mkdir /sys/fs/cgroup/memory/mydocker

同样,我们查看一下新创建的目录下发生了什么?
复制代码

  1. 1. total 0
  2. 2. -rw-r--r--. 1 root root 0 Sep 5 10:18 cgroup.clone_children
  3. 3. --w--w--w-. 1 root root 0 Sep 5 10:18 cgroup.event_control
  4. 4. -rw-r--r--. 1 root root 0 Sep 5 10:18 cgroup.procs
  5. 5. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.failcnt
  6. 6. --w-------. 1 root root 0 Sep 5 10:18 memory.force_empty
  7. 7. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.failcnt
  8. 8. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.limit_in_bytes
  9. 9. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.max_usage_in_bytes
  10. 10. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.slabinfo
  11. 11. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.failcnt
  12. 12. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.limit_in_bytes
  13. 13. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.max_usage_in_bytes
  14. 14. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.tcp.usage_in_bytes
  15. 15. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.kmem.usage_in_bytes
  16. 16. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.limit_in_bytes
  17. 17. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.max_usage_in_bytes
  18. 18. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.failcnt
  19. 19. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.limit_in_bytes
  20. 20. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.max_usage_in_bytes
  21. 21. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.memsw.usage_in_bytes
  22. 22. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.move_charge_at_immigrate
  23. 23. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.numa_stat
  24. 24. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.oom_control
  25. 25. ----------. 1 root root 0 Sep 5 10:18 memory.pressure_level
  26. 26. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.soft_limit_in_bytes
  27. 27. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.stat
  28. 28. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.swappiness
  29. 29. -r--r--r--. 1 root root 0 Sep 5 10:18 memory.usage_in_bytes
  30. 30. -rw-r--r--. 1 root root 0 Sep 5 10:18 memory.use_hierarchy
  31. 31. -rw-r--r--. 1 root root 0 Sep 5 10:18 notify_on_release
  32. 32. -rw-r--r--. 1 root root 0 Sep 5 10:18 tasks

其中 memory.limit_in_bytes 文件代表内存使用总量,单位为 byte。
例如,这里我希望对内存使用限制为 1G,则向 memory.limit_in_bytes 文件写入 1073741824,命令如下:
复制代码

  1. 1. # cd /sys/fs/cgroup/memory/mydocker
  2. 2. # echo 1073741824 > memory.limit_in_bytes

第二步:创建进程,加入 cgroup
同样把当前 shell 进程 ID 写入 tasks 文件内:
复制代码

  1. 1. # cd /sys/fs/cgroup/memory/mydocker
  2. 2. # echo $$ > tasks

第三步,执行内存测试工具,申请内存
这里我们需要借助一下工具 memtester,memtester 的安装这里不再详细介绍了。具体安装方式可以参考这里
安装好 memtester 后,我们执行以下命令:
复制代码

  1. 1. # memtester 1500M 1
  2. 2. memtester version 4.2.2 (64-bit)
  3. 3. Copyright (C) 2010 Charles Cazabon.
  4. 4. Licensed under the GNU General Public License version 2 (only).
  5. 5.
  6. 6. pagesize is 4096
  7. 7. pagesizemask is 0xfffffffffffff000
  8. 8. want 1500MB (1572864000 bytes)
  9. 9. got 1500MB (1572864000 bytes), trying mlock ...Killed

该命令会申请 1500 M 内存,并且做内存测试。由于上面我们对当前 shell 进程内存限制为 1 G,当 memtester 使用的内存达到 1G 时,cgroup 便将 memtester 杀死。
上面最后一行的输出结果表示 memtester 想要 1500 M 内存,但是由于 cgroup 限制,达到了内存使用上限,被杀死了,与我们的预期一致。
我们可以使用以下命令,降低一下内存申请,将内存申请调整为 500M:
复制代码

  1. 1. # memtester 500M 1
  2. 2. memtester version 4.2.2 (64-bit)
  3. 3. Copyright (C) 2010 Charles Cazabon.
  4. 4. Licensed under the GNU General Public License version 2 (only).
  5. 5.
  6. 6. pagesize is 4096
  7. 7. pagesizemask is 0xfffffffffffff000
  8. 8. want 500MB (524288000 bytes)
  9. 9. got 500MB (524288000 bytes), trying mlock ...locked.
  10. 10. Loop 1/1:
  11. 11. Stuck Address : ok
  12. 12. Random Value : ok
  13. 13. Compare XOR : ok
  14. 14. Compare SUB : ok
  15. 15. Compare MUL : ok
  16. 16. Compare DIV : ok
  17. 17. Compare OR : ok
  18. 18. Compare AND : ok
  19. 19. Sequential Increment: ok
  20. 20. Solid Bits : ok
  21. 21. Block Sequential : ok
  22. 22. Checkerboard : ok
  23. 23. Bit Spread : ok
  24. 24. Bit Flip : ok
  25. 25. Walking Ones : ok
  26. 26. Walking Zeroes : ok
  27. 27. 8-bit Writes : ok
  28. 28. 16-bit Writes : ok
  29. 29.
  30. 30. Done.

这里可以看到,此时 memtester 已经成功申请到 500M 内存并且正常完成了内存测试。
到此,我们讲解了cgroups的 cpu 和 memroy 子系统,如果你想了解更多的cgroups的知识和使用,可以参考 Red Hat 官网

删除 cgroups

上面创建的cgroups如果不想使用了,直接删除创建的文件夹即可。
例如我想删除内存下的 mydocker 目录,使用以下命令即可:
复制代码

  1. 1. # rmdir /sys/fs/cgroup/memory/mydocker/

学习了cgroups的使用方式,下面我带你了解一下 Docker 是如何使用cgroups的。

Docker 是如何使用cgroups的?

首先,我们使用以下命令创建一个 nginx 容器:
复制代码

  1. 1. docker run -it -m=1g nginx

上述命令创建并启动了一个 nginx 容器,并且限制内存为 1G。然后我们进入cgroups内存子系统的目录,使用 ls 命令查看一下该目录下的内容:
复制代码

  1. 1. # ls -l /sys/fs/cgroup/memory
  2. 2. total 0
  3. 3. -rw-r--r--. 1 root root 0 Sep 1 11:50 cgroup.clone_children
  4. 4. --w--w--w-. 1 root root 0 Sep 1 11:50 cgroup.event_control
  5. 5. -rw-r--r--. 1 root root 0 Sep 1 11:50 cgroup.procs
  6. 6. -r--r--r--. 1 root root 0 Sep 1 11:50 cgroup.sane_behavior
  7. 7. drwxr-xr-x. 3 root root 0 Sep 5 10:50 docker
  8. 8. ... 省略部分输出

通过上面输出可以看到,该目录下有一个 docker 目录,该目录正是 Docker 在内存子系统下创建的。我们进入到 docker 目录下查看一下相关内容:
复制代码

  1. 1. # cd /sys/fs/cgroup/memory/docker
  2. 2. # ls -l
  3. 3. total 0
  4. 4. drwxr-xr-x. 2 root root 0 Sep 5 10:49 cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
  5. 5. -rw-r--r--. 1 root root 0 Sep 4 10:40 cgroup.clone_children
  6. 6. --w--w--w-. 1 root root 0 Sep 4 10:40 cgroup.event_control
  7. 7. -rw-r--r--. 1 root root 0 Sep 4 10:40 cgroup.procs
  8. 8. ... 省略部分输出
  9. 9. -rw-r--r--. 1 root root 0 Sep 4 10:40 tasks

可以看到 docker 的目录下有一个一串随机 ID 的目录,该目录即为我们上面创建的 nginx 容器的 ID。然后我们进入该目录,查看一下该容器的 memory.limit_in_bytes 文件的内容。
复制代码

  1. 1. # cd cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
  2. 2. # cat memory.limit_in_bytes
  3. 3. 1073741824

可以看到内存限制值正好为 1G。
事实上,Docker 创建容器时,Docker 会根据启动容器的参数,在对应的 cgroups 子系统下创建以容器 ID 为名称的目录, 然后根据容器启动时设置的资源限制参数, 修改对应的 cgroups 子系统资源限制文件, 从而达到资源限制的效果。