本文是容器技术系列文章的一部分:
- Namespace 和 Cgroup 简介及其工作原理(本文)
- 构建小型容器镜像
我最近一直在研究开源多语言应用服务器 NGINX Unit。在研究中,我注意到 Unit 同时支持 namespace 和 cgroup 这两项 进程隔离 的特性本文将介绍这两大构成 容器 基础的 Linux 技术。
容器及相关工具(例如 Docker 和 Kubernetes) 出现有一段时间了。它们改变了现代应用环境中软件的开发和交付方式。容器可支持软件在各自的隔离环境中快速部署和运行,而无需用户构建单独的虚拟机 (VM)。
大多数人可能很少考虑容器的工作基础,但我认为,了解底层技术很重要,因为这有助于我们制定决策。另外,就我个人而言,能够彻底弄清事物的工作原理令我心情舒畅!
什么是 Namespaces?
Namespace 作为 Linux 内核的组成部分大约出现于 2002 年,随着时间的推移,Linux 内核添加了更多的工具和 namespace 类型。然而,直到 2013 年,Linux 内核才添加了真正的容器支持。至此,namespace 开始大显身手,并得到了广泛应用。
那么,namespace 到底是什么呢?百度百科 是这样定义的:
“namespace 即’命名空间’,也称’名称空间’。是许多编程语言使用的一种代码组织的形式,通过命名空间来分类,区别不同的代码功能,避免不同的代码片段(通常由不同的人协同工作或调用已有的代码片段)同时使用时由于不同代码间变量名相同而造成冲突。”
换句话说,namespace 的关键特性是进程隔离。在运行许多不同服务的服务器上,将各个服务及其相关进程相互隔离能够减少变更带来的影响以及安全性方面的问题。大多数情况下,隔离服务符合 Martin Fowler 所描述的微服务架构风格。
在开发过程中使用容器为开发人员提供了一个独立的环境,看起来就像一个完整的虚拟机。但它不是虚拟机,而是在服务器某处上运行的进程。如果开发人员启动了两个容器,那么某个服务器上的某两个地方将有两个进程运行,但它们是相互隔离的。
Namespace 的类型
Linux 内核包含了不同类型的 namespace。每个 namespace 都有自己的独特属性。
- User namespace 拥有自己的一组用户 ID 和组 ID,用于分配给进程。这意味着进程可以在其 user namespace 中拥有
root
权限,而不需要在其他 user namespace 中获得。 - Process ID (PID) namespace 将一组 PID 分配给独立于其他 namespace 中的一组 PID 的进程。在新的 namespace 中创建的第一个进程分得 PID 1,子进程被分配给后续的 PID。如果子进程使用自己的 PID namespace 创建,则它在该 namespace 中使用 PID 1,在父进程的 namespace 中使用自己的 PID。请参见下面的示例。
- Network namespace 拥有独立的网络栈:自己的专用路由表、IP 地址集、套接字列表、连接跟踪表、防火墙及其他网络相关资源。
- Mount namespace 拥有一个独立的挂载点列表,并对该 namespace 中的进程可见。这意味着您可以在 mount namespace 中挂载和卸载文件系统,而不会影响主机文件系统。
- Interprocess communication (IPC) namespace 拥有自己的 IPC 资源,例如 POSIX 消息队列。
- UNIX Time‑Sharing (UTS) namespace 允许单个系统对不同的进程显示不同的主机名和域名。
父子 PID Namespace 示例
下图中共有三个 PID namespace:一个父 namespace 和两个子 namespace。父 namespace 中共有四个进程,PID1 至 PID4。这些都是正常的进程,它们可以看到彼此并共享资源。
父 namespace 中使用 PID2 和 PID3 的子进程也属于它们各自的 PID namespace(PID 为 1)。在子 namespace 中,PID1 进程看不到任何外部资源。例如,两个子 namespace 中的 PID1 看不到父 namespace 中的 PID4。
在这种情况下,这使得不同 namespace 中的进程之间得以隔离。
创建 Namespace
有了这些理论,现在我们实际创建一个新的 namespace 以加深理解。Linux unshare
命令是一个很好的着手点。手册页显示它就是我们要找的:
NAME
unshare - run program in new name namespaces
当前我以普通用户 svk
的身份登录,该用户拥有自己的用户 ID、组等,但没有 root
权限:
svk $ id
uid=1000(svk) gid=1000(svk) groups=1000(svk) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c.1023
现在,我运行以下 unshare
命令,使用自己的 user 和 PID namespace 创建一个新的 namespace。我将 root
用户映射到新的 namespace(换句话说,我在新的 namespace 中拥有 root 权限),挂载一个新的 proc 文件系统,并在新创建的 namespace 中进行进程(本例中为 bash
)分支。
svk $ unshare --user --pid --map-root-user --mount-proc --fork bash
(对于熟悉容器的人来说,这相当于在运行的容器中执行 <runtime> exec -it <image> /bin/bash
命令。)
ps
-ef
命令显示有两个进程正在运行(bash
和 ps
命令本身),并且 id
命令确认我在新的 namespace 中是 root
用户(这一点也可以从更改的命令提示符看出):
root # ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 14:46 pts/0 00:00:00 bash
root 15 1 0 14:46 pts/0 00:00:00 ps -ef
root # id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c.1023
需要注意的是,我只能看到我的 namespace 中的两个进程,而看不到系统上运行的任何其他进程。我完全隔离在自己的 namespace 中。
从外部角度看 Namespace
虽然我从自己的 namespace 中看不到其他进程,但我可以使用 lsns
(list namespace) 命令,从父 namespace(在新的 namespace 之外)的角度列出所有可用的 namespace 并显示其相关信息。
输出结果显示了三个 namespace(类型为 user
、mnt
和 pid
),对应我上面运行的 unshare
命令中的参数。从这个外部角度来看,每个 namespace 都以用户 svk
身份运行,而不是以 root
身份。而在 namespace 内部,进程以 root
身份运行,并可以访问所有预期的资源。(为了便于阅读,输出结果分成了两行。)
root # lsns --output-all | head -1; lsns --output-all | grep svk
NS TYPE PATH NPROCS PID PPID ...
4026532690 user /proc/97964/ns/user 2 97964 97944 ...
4026532691 mnt /proc/97964/ns/mnt 2 97964 97944 ...
4026532692 pid /proc/97965/ns/pid 1 97965 97964 ...
... COMMAND UID USER
... unshare --user --map-root-user --fork –pid --mount-proc bash 1000 svk
... unshare --user --map-root-user --fork –pid --mount-proc bash 1000 svk
... bash 1000 svk
Namespace 和容器
Namespace 是容器的基础技术之一,用于隔离资源。我们已经展示了如何手动创建 namespace,Docker、rkt 和 podman 等容器运行时可为您创建 namespace,帮助您减轻工作负担。同样地,NGINX Unit 中的 isolation
应用对象 也可以创建 namespace 和 cgroup。
什么是 cgroup?
控制组 (cgroup) 是 Linux 内核的一个特性,用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)。
Cgroup 具有以下特性:
- 资源限制 —— 您可以配置 cgroup,从而限制进程可以对特定资源(例如内存或 CPU)的使用量。
- 优先级 —— 当资源发生冲突时,您可以控制一个进程相比另一个 cgroup 中的进程可以使用的资源量(CPU、磁盘或网络)。
- 记录 —— 在 cgroup 级别监控和报告资源限制。
- 控制 —— 您可以使用单个命令更改 cgroup 中所有进程的状态(冻结、停止或重新启动)。
Cgroup 的作用基本上就是控制一个进程或一组进程可以访问或使用给定关键资源(CPU、内存、网络和磁盘 I/O)的量。一个容器中通常运行了多个进程,并且您需要对这些进程实施统一控制,因此 cgroup 是容器的关键组件。Kubernetes 环境使用 cgroup 在 pod 级别上部署 资源请求和限制 以及对应的 QoS 类。
T下图说明了当您将特定比例的可用系统资源分配给一个 cgroup(在本例中,为 cgroup‑1)后,剩余资源是如何在系统上其他 cgroup(以及各个进程)之间进行分配的。
Cgroup 版本
根据维基百科的说法,cgroup 第一个版本于 2007 年末或 2008 年初并入 Linux 内核主线,“cgroup 第二个版本的文档于 2016 年首次出现在 Linux 内核中”。第二个版本进行了许多改变,其中较大的变化是树形结构更加简化、cgroup 层次结构增加了新的特性和接口,以及能够更好地适应“rootless”容器(使用非零 UID)。
V2版本的接口也有所更新,其中我最喜欢的是 Pressure Stall Information (PSI)。与之前相比,它能够更精细地提供对每个进程内存使用和分配情况的洞察(不在本博客的讨论范围内,但这是一个非常有意思的话题)。
创建 cgroup
以下命令将创建一个名为 foo
的v1 cgroup(通过 pathname 格式可以看出来),并将其内存限制设为 50,000,000 字节 (50 MB)。
root # mkdir -p /sys/fs/cgroup/memory/foo
root # echo 50000000 > /sys/fs/cgroup/memory/foo/memory.limit_in_bytes
现在,我可以为该 cgroup 分配一个进程,从而对其施加 cgroup 的内存限制。我编写了一个名为 test.sh
的 shell 脚本,它将 cgroup
testing
tool
打印到屏幕上,然后我只需静静等待。因为在我停止它之前,它会持续运行。
我在后台启动 test.sh
,脚本生成其输出结果, PID 为 2428,然后我将其 PID 写入 cgroup 文件 /sys/fs/cgroup/memory/foo/cgroup.procs,从而将该进程分配给 cgroup。
root # ./test.sh &
[1] 2428
root # cgroup testing tool
root # echo 2428 > /sys/fs/cgroup/memory/foo/cgroup.procs
为了验证我的进程是否受限于我为 cgroup foo
定义的内存限制,我运行了以下 ps
命令。-o
cgroup
标志显示了指定进程 (2428) 所属的 cgroup。输出结果确认其内存 cgroup 为 foo
。
root # ps -o cgroup 2428
CGROUP
12:pids:/user.slice/user-0.slice/\
session-13.scope,10:devices:/user.slice,6:memory:/foo,...
默认情况下,当进程超过其 cgroup 定义的资源限制时,操作系统将终止该进程。
结语
Namespace 和 cgroup 是容器和现代应用的构建模块。当我们将应用重构为更现代的架构后,深入了解它们的工作方式非常重要。
Namespace 支持系统资源隔离,而 cgroup 则支持对这些资源进行精细的控制和限制。
容器并非 namespace 和 cgroup 的唯一用例。namespace 和 cgroup 接口内置于 Linux 内核中,这意味着其他应用也可以使用它们来提供隔离和资源限制。
了解更多关于 NGINX Unit 的信息并 下载源码,亲自试用吧。