关于 Linux 容器,你需要知道的一切,第一部分:Linux 控制组和进程隔离

每个人都听说过这个术语,但容器到底是什么?

支持这项技术的软件有很多形式,其中 Docker 是最受欢迎的。容器技术在数据中心内近期的兴起,直接归因于其可移植性和隔离工作环境的能力,从而限制了其对底层计算系统的影响和总体占用空间。要完全理解这项技术,您首先需要了解使其成为可能的所有组成部分。

旁注:人们经常询问容器和虚拟机之间的区别。两者都有特定的用途和位置,重叠很少,而且一个不会淘汰另一个。容器旨在成为轻量级环境,您可以启动它来托管一个或几个以裸机性能隔离的应用程序。当您想要托管整个操作系统或生态系统,或者可能运行与底层环境不兼容的应用程序时,您应该选择虚拟机。

Linux 控制组

说实话,某些实际应用中的软件可能需要被控制或限制——至少为了稳定性和一定程度的安全性。通常,一个错误或仅仅是糟糕的代码就可能扰乱整台机器,并可能破坏整个生态系统。幸运的是,有一种方法可以控制这些应用程序。控制组 (cgroups) 是一种内核功能,它可以限制、核算和隔离一个或多个进程的 CPU、内存、磁盘 I/O 和网络使用情况。

cgroups 技术最初由 Google 开发,最终在 2.6.24 版本(2008 年 1 月)中进入 Linux 内核主线。这项技术的重新设计——即添加 kernfs(以分离部分 sysfs 逻辑)——将被合并到 3.15 和 3.16 内核中。

cgroups 的主要设计目标是提供一个统一的接口来管理进程或整个操作系统级别的虚拟化,包括 Linux 容器或 LXC(我计划在后续文章中更详细地重新讨论这个主题)。cgroups 框架提供以下功能:

  • 资源限制: 可以配置一个组不超过指定的内存限制,或使用不超过所需数量的处理器,或限制为特定的外围设备。
  • 优先级排序: 可以配置一个或多个组以使用更少或更多的 CPU 或磁盘 I/O 吞吐量。
  • 核算: 监控和测量组的资源使用情况。
  • 控制: 可以冻结或停止并重新启动进程组。

一个 cgroup 可以包含一个或多个进程,这些进程都绑定到同一组限制。这些组也可以是分层的,这意味着子组继承其父组的管理限制。

Linux 内核为 cgroup 技术提供了一系列控制器或子系统。控制器负责将特定类型的系统资源分配给一组或多个进程。例如,memory 控制器用于限制内存使用,而 cpuacct 控制器用于监控 CPU 使用情况。

您可以直接和间接地(使用 LXC、libvirt 或 Docker)访问和管理 cgroups,我在此处通过 sysfs 和 libcgroups 库介绍第一种方法。要跟随这里的示例,您首先需要安装必要的软件包。在 Red Hat Enterprise Linux 或 CentOS 上,在命令行中键入以下命令:


$ sudo yum install libcgroup libcgroup-tools

在 Ubuntu 或 Debian 上,键入:


$ sudo apt-get install libcgroup1 cgroup-tools

对于示例应用程序,我正在使用一个名为 test.sh 的简单 shell 脚本文件,它将在一个无限 while 循环中运行以下两个命令:


$ cat test.sh
#!/bin/sh

while [ 1 ]; do
        echo "hello world"
        sleep 60
done

手动方法

安装了正确的软件包后,您可以直接通过 sysfs 层次结构配置您的 cgroups。例如,要在 memory 子系统下创建一个名为 foo 的 cgroup,请在 /sys/fs/cgroup/memory 中创建一个名为 foo 的目录:


$ sudo mkdir /sys/fs/cgroup/memory/foo

默认情况下,每个新创建的 cgroup 都将继承对系统整个内存池的访问权限。对于某些应用程序,主要是那些继续分配更多内存但拒绝释放已分配内存的应用程序,这可能不是一个好主意。要将应用程序限制在合理的限制内,您需要更新 memory.limit_in_bytes 文件。

将 cgroup foo 下运行的任何内容的内存限制为 50MB:


$ echo 50000000 | sudo tee
 ↪/sys/fs/cgroup/memory/foo/memory.limit_in_bytes

验证设置:


$ sudo cat memory.limit_in_bytes
50003968

请注意,读取回的值始终是内核页面大小(即 4096 字节或 4KB)的倍数。此值是最小可分配的内存大小。

启动应用程序:


$ sh ~/test.sh &

使用其进程 ID (PID),将应用程序移动到 memory 控制器下的 cgroup foo


$ echo 2845 > /sys/fs/cgroup/memory/foo/cgroup.procs

使用相同的 PID 号,列出正在运行的进程并验证它是否在所需的 cgroup 中运行:


$ ps -o cgroup 2845
CGROUP
8:memory:/foo,1:name=systemd:/user.slice/user-0.slice/
↪session-4.scope

您还可以通过读取所需的文件来监控该 cgroup 当前正在使用的资源。在这种情况下,您需要查看您的进程(和衍生的子进程)分配的内存量:


$ cat /sys/fs/cgroup/memory/foo/memory.usage_in_bytes
253952

当进程出错时

现在让我们重新创建相同的场景,但不是将 cgroup foo 的内存限制为 50MB,而是将其限制为 500 字节:


$ echo 500 | sudo tee /sys/fs/cgroup/memory/foo/
↪memory.limit_in_bytes

注意:如果任务超出其定义的限制,内核将进行干预,在某些情况下,会杀死该任务。

同样,当您读取回值时,它始终是内核页面大小的倍数。因此,虽然您将其设置为 500 字节,但实际上设置为 4 KB:


$ cat /sys/fs/cgroup/memory/foo/memory.limit_in_bytes
4096

启动应用程序,将其移动到 cgroup 中,并监控系统日志:


$ sudo tail -f /var/log/messages
Oct 14 10:22:40 localhost kernel: sh invoked oom-killer:
 ↪gfp_mask=0xd0, order=0, oom_score_adj=0
Oct 14 10:22:40 localhost kernel: sh cpuset=/ mems_allowed=0
Oct 14 10:22:40 localhost kernel: CPU: 0 PID: 2687 Comm:
 ↪sh Tainted: G
OE  ------------   3.10.0-327.36.3.el7.x86_64 #1
Oct 14 10:22:40 localhost kernel: Hardware name: innotek GmbH
VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
Oct 14 10:22:40 localhost kernel: ffff880036ea5c00
 ↪0000000093314010 ffff88000002bcd0 ffffffff81636431
Oct 14 10:22:40 localhost kernel: ffff88000002bd60
 ↪ffffffff816313cc 01018800000000d0 ffff88000002bd68
Oct 14 10:22:40 localhost kernel: ffffffffbc35e040
 ↪fffeefff00000000 0000000000000001 ffff880036ea6103
Oct 14 10:22:40 localhost kernel: Call Trace:
Oct 14 10:22:40 localhost kernel: [<ffffffff81636431>]
 ↪dump_stack+0x19/0x1b
Oct 14 10:22:40 localhost kernel: [<ffffffff816313cc>]
 ↪dump_header+0x8e/0x214
Oct 14 10:22:40 localhost kernel: [<ffffffff8116d21e>]
 ↪oom_kill_process+0x24e/0x3b0
Oct 14 10:22:40 localhost kernel: [<ffffffff81088e4e>] ?
 ↪has_capability_noaudit+0x1e/0x30
Oct 14 10:22:40 localhost kernel: [<ffffffff811d4285>]
 ↪mem_cgroup_oom_synchronize+0x575/0x5a0
Oct 14 10:22:40 localhost kernel: [<ffffffff811d3650>] ?
 ↪mem_cgroup_charge_common+0xc0/0xc0
Oct 14 10:22:40 localhost kernel: [<ffffffff8116da94>]
 ↪pagefault_out_of_memory+0x14/0x90
Oct 14 10:22:40 localhost kernel: [<ffffffff8162f815>]
 ↪mm_fault_error+0x68/0x12b
Oct 14 10:22:40 localhost kernel: [<ffffffff816422d2>]
 ↪__do_page_fault+0x3e2/0x450
Oct 14 10:22:40 localhost kernel: [<ffffffff81642363>]
 ↪do_page_fault+0x23/0x80
Oct 14 10:22:40 localhost kernel: [<ffffffff8163e648>]
 ↪page_fault+0x28/0x30
Oct 14 10:22:40 localhost kernel: Task in /foo killed as
 ↪a result of limit of /foo
Oct 14 10:22:40 localhost kernel: memory: usage 4kB, limit
 ↪4kB, failcnt 8
Oct 14 10:22:40 localhost kernel: memory+swap: usage 4kB,
 ↪limit 9007199254740991kB, failcnt 0
Oct 14 10:22:40 localhost kernel: kmem: usage 0kB, limit
 ↪9007199254740991kB, failcnt 0
Oct 14 10:22:40 localhost kernel: Memory cgroup stats for /foo:
 ↪cache:0KB rss:4KB rss_huge:0KB mapped_file:0KB swap:0KB
 ↪inactive_anon:0KB active_anon:0KB inactive_file:0KB
 ↪active_file:0KB unevictable:0KB
Oct 14 10:22:40 localhost kernel: [ pid ]   uid  tgid total_vm
 ↪rss nr_ptes swapents oom_score_adj name
Oct 14 10:22:40 localhost kernel: [ 2687]     0  2687    28281
 ↪347     12        0             0 sh
Oct 14 10:22:40 localhost kernel: [ 2702]     0  2702    28281
 ↪50    7        0             0 sh
Oct 14 10:22:40 localhost kernel: Memory cgroup out of memory:
 ↪Kill process 2687 (sh) score 0 or sacrifice child
Oct 14 10:22:40 localhost kernel: Killed process 2702 (sh)
 ↪total-vm:113124kB, anon-rss:200kB, file-rss:0kB
Oct 14 10:22:41 localhost kernel: sh invoked oom-killer:
 ↪gfp_mask=0xd0, order=0, oom_score_adj=0
[ ... ]

请注意,内核的内存不足杀手 (oom-killer) 在应用程序达到 4KB 限制后立即介入。它杀死了应用程序,并且不再运行。您可以通过键入以下命令来验证这一点:


$ ps -o cgroup 2687
CGROUP

使用 libcgroup

libcgroup 软件包中提供的管理实用程序简化了此处描述的许多早期步骤。例如,使用 cgcreate 二进制文件的单个命令调用即可处理创建 sysfs 条目和文件的过程。

要在 memory 子系统下创建名为 foo 的组,请键入以下命令:


$ sudo cgcreate -g memory:foo

注意:libcgroup 提供了一种管理控制组中任务的机制。

使用与以前相同的方法,您可以开始设置阈值:


$ echo 50000000 | sudo tee
 ↪/sys/fs/cgroup/memory/foo/memory.limit_in_bytes

验证新配置的设置:


$ sudo cat memory.limit_in_bytes
50003968

使用 cgexec 二进制文件在 cgroup foo 中运行应用程序:


$ sudo cgexec -g memory:foo ~/test.sh

使用其 PID 号,验证应用程序是否在 cgroup 和定义的子系统 (memory) 下运行:


$  ps -o cgroup 2945
CGROUP
6:memory:/foo,1:name=systemd:/user.slice/user-0.slice/
↪session-1.scope

如果您的应用程序不再运行,并且您想要清理并删除 cgroup,您可以使用 cgdelete 二进制文件来执行此操作。要从 memory 控制器下删除组 foo,请键入:


$ sudo cgdelete memory:foo

持久组

您还可以通过一个简单的配置文件和启动服务来完成上述所有操作。您可以在 /etc/cgconfig.conf 文件中定义所有 cgroup 名称和属性。以下内容为组 foo 附加了一些属性:


$ cat /etc/cgconfig.conf
#
#  Copyright IBM Corporation. 2007
#
#  Authors:     Balbir Singh <balbir@linux.vnet.ibm.com>
#  This program is free software; you can redistribute it
#  and/or modify it under the terms of version 2.1 of the GNU
#  Lesser General Public License as published by the Free
#  Software Foundation.
#
#  This program is distributed in the hope that it would be
#  useful, but WITHOUT ANY WARRANTY; without even the implied
#  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#  PURPOSE.
#
#
# By default, we expect systemd mounts everything on boot,
# so there is not much to do.
# See man cgconfig.conf for further details, how to create
# groups on system boot using this file.

group foo {
  cpu {
    cpu.shares = 100;
  }
  memory {
    memory.limit_in_bytes = 5000000;
  }
}

cpu.shares 选项定义了组的 CPU 优先级。默认情况下,所有组都继承 1,024 份共享或 100% 的 CPU 时间。通过将此值降低到更保守的值,例如 100,该组将被限制为大约 10% 的 CPU 时间。

如前所述,在 cgroup 中运行的进程也可以限制其可以访问的 CPU(核心)数量。将以下部分添加到相同的 cgconfig.conf 文件中的所需组名下:


cpuset {
  cpuset.cpus="0-5";
}

通过此限制,此 cgroup 将应用程序绑定到核心 0 到 5——也就是说,它将仅看到系统上的前六个 CPU 核心。

接下来,您需要使用 cgconfig 服务加载此配置。首先,启用 cgconfig 以在系统启动时加载上述配置:


$ sudo systemctl enable cgconfig
Create symlink from /etc/systemd/system/sysinit.target.wants/
↪cgconfig.service
to /usr/lib/systemd/system/cgconfig.service.

现在,启动 cgconfig 服务并手动加载相同的配置文件(或者您可以跳过此步骤并重新启动系统):


$ sudo systemctl start cgconfig

将应用程序启动到 cgroup foo 中,并将其绑定到您的 memorycpu 限制:


$ sudo cgexec -g memory,cpu,cpuset:foo ~/test.sh &

除了将应用程序启动到预定义的 cgroup 中之外,所有其余操作将在系统重启后持续存在。但是,您可以通过定义一个依赖于 cgconfig 服务的启动 init 脚本来自动化该过程,以启动相同的应用程序。

总结

通常,有必要限制机器上的一个或多个任务。控制组提供了该功能,通过利用它,您可以对一些最重要或无法控制的应用程序强制执行严格的硬件和软件限制。如果一个应用程序没有设置上限阈值或限制它可以在系统上消耗的内存量,cgroups 可以解决这个问题。如果另一个应用程序往往是 CPU 占用大户,那么 cgroups 再次为您提供保障。您可以使用 cgroups 完成很多工作,并且只需投入少量时间,您就可以将稳定性、安全性和理智恢复到您的操作环境中。

在本系列的第二部分中,我将超越 Linux 控制组,并将重点转移到 Linux 容器等技术如何利用它们。

资源

有关容器的其他 LJ 文章,请参阅以下内容:

Petros Koutoupis,LJ 特约编辑,目前是 Cray 公司 Lustre 高性能文件系统部门的高级性能软件工程师。他还是 RapidDisk 项目的创建者和维护者。Petros 在数据存储行业工作了十多年,并帮助开创了当今广泛应用的许多技术。

加载 Disqus 评论