Docker资源隔离与限制 Namespace Cgroups【Docker系列 (7)】

资源隔离 Namespace 什么是 Namespace? 下面是 Namespace 的维基百科定义: Namespace 是 Linux 内核的一项功能,该功能对内核资源进行分区,以使一组进程看到一组资源,而另一组进程看到另一组资源。Namespace 的工作方式通过为一组资源和进程设置相同的

资源隔离 Namespace

什么是 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 需要 Namespace?

Linux 内核从 2002 年 2.4.19 版本开始加入了 Mount Namespace,而直到内核 3.8 版本加入了 User Namespace 才为容器提供了足够的支持功能。

当 Docker 新建一个容器时, 它会创建这六种 Namespace,然后将容器中的进程加入这些 Namespace 之中,使得 Docker 容器中的进程只能看到当前 Namespace 中的系统资源。

正是由于 Docker 使用了 Linux 的这些 Namespace 技术,才实现了 Docker 容器的隔离,可以说没有 Namespace,就没有 Docker 容器。

资源限制 Cgoups

使用不同的 Namespace,可以实现容器中的进程看不到别的容器的资源,但是有一个问题你是否注意到?容器内的进程仍然可以任意地使用主机的 CPU 、内存等资源,如果某一个容器使用的主机资源过多,可能导致主机的资源竞争,进而影响业务。那如果我们想限制一个容器资源的使用(如 CPU、内存等)应该如何做呢?

这里就需要用到 Linux 内核的另一个核心技术cgroups。那么究竟什么是cgroups?我们应该如何使用cgroups?Docker 又是如何使用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 功能及核心概念

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信息:

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

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

cpu 子系统

我首先以 cpu 子系统为例,演示一下cgroups如何限制进程的 cpu 使用时间。由于cgroups的操作很多需要用到 root 权限,我们在执行命令前要确保已经切换到了 root 用户,以下命令的执行默认都是使用 root 用户。

第一步:在 cpu 子系统下创建 cgroup

cgroups的创建很简单,只需要在相应的子系统下创建目录即可。下面我们到 cpu 子系统下创建测试文件夹:

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

然后再查看 ls -l /sys/fs/cgroup/cpu/mydocker新建的目录下被自动创建了很多文件,其中 cpu.cfs_quota_us 文件代表在某一个阶段限制的 CPU 时间总量,单位为微秒。例如,我们想限制某个进程最多使用 1 核 CPU,就在这个文件里写入 100000(100000 代表限制 1 个核) ,tasks 文件中写入进程的 ID 即可(如果要限制多个进程 ID,在 tasks 文件中用换行符分隔即可)。

第二步:创建进程,加入 cgroup

这里为了方便演示,我先把当前运行的 shell 进程加入 cgroup,然后在当前 shell 运行 cpu 耗时任务(这里利用到了继承,子进程会继承父进程的 cgroup)。

使用以下命令将 shell 进程加入 cgroup 中:

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

查看一下 tasks 文件内容:

# cat tasks
3485
3543

其中第一个进程 ID 为当前 shell 的主进程,也就是说,当前 shell 主进程为 3485。

第三步:执行 CPU 耗时任务,验证 cgroup 是否可以限制 cpu 使用时间

下面,我们使用以下命令制造一个死循环,来提升 cpu 使用率:

# while true;do echo;done;

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

$ top -p 3485

top - 09:51:35 up 3 days, 22:00,  4 users,  load average: 1.59, 0.58, 0.27
Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
%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
KiB Mem : 32779616 total, 31009780 free,   495988 used,  1273848 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 31852336 avail Mem
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
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 退出循环。

memroy 子系统

同理,在 memory 子系统下创建 cgroup

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

后续和cpu流程类似,但是内存的测试需要用一些工具,去逐步申请内存,才能看到内存的限制

Docker 是如何使用cgroups的?

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

docker run -it -m=1g nginx

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

# ls -l /sys/fs/cgroup/memory
total 0
-rw-r--r--.  1 root root 0 Sep  1 11:50 cgroup.clone_children
--w--w--w-.  1 root root 0 Sep  1 11:50 cgroup.event_control
-rw-r--r--.  1 root root 0 Sep  1 11:50 cgroup.procs
-r--r--r--.  1 root root 0 Sep  1 11:50 cgroup.sane_behavior
drwxr-xr-x.  3 root root 0 Sep  5 10:50 docker
...

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

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

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

# cd cb5c5391177b44ad87636bf3840ecdda83529e51b76a6406d6742f56a2535d5e
# cat memory.limit_in_bytes
1073741824

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

LICENSED UNDER CC BY-NC-SA 4.0
Comment