容器 = Namespace + Cgroups + rootfs

163 阅读11分钟

嗨,你好啊,我是猿java

Linux 内核提供了多种机制来实现系统资源的隔离和管理,这篇文章,我们来详细分析两种关键的技术:Namespace 和 Cgroups。

Namespace 详解

Namespace(命名空间,也称名称空间)是 Linux 内核用于隔离系统资源,使得不同的进程组可以拥有各自独立的资源视图。Namespace 的核心思想是通过将系统资源划分为不同的命名空间,进而实现资源的隔离。

Namespace通常包含以下几种类型:

  1. PID Namespace
  2. Mount Namespace
  3. UTS Namespace
  4. IPC Namespace
  5. Network Namespace
  6. User Namespace

PID Namespace

定义

PID Namespace 用于隔离进程 ID 空间,每个 PID Namespace 都有自己独立的进程 ID 号,父 Namespace 可以查看和管理子 Namespace 中的进程,但反之则不行。这种机制使得在容器中运行的进程可以拥有从 1 开始的 PID。

如下指令,可以创建一个新的 PID 命名空间:

# 创建新的 PID Namespace 并运行一个 Bash 进程
unshare -p -f --mount-proc bash

命令解释

  • unshare:用于创建并进入一个或多个新的命名空间。
  • -p:创建一个新的 PID 命名空间。在新的 PID 命名空间中,进程可以拥有独立的进程 ID 号。
  • -f:让 unshare 命令 fork 一个新的进程,并在这个新的进程中执行指定的命令。这样可以确保 unshare 本身不会受到新的命名空间的影响。
  • --mount-proc:在新的 PID 命名空间中挂载一个新的 /proc 文件系统。这个选项通常与 -p 选项一起使用,以确保新的 PID 命名空间有一个一致的 /proc 视图。
  • bash:在新的命名空间中启动一个新的 Bash shell。

示例

为了更好地说明 Mount Namespace,下面以一个示例进行展示:

1.在终端A中执行以下指令:

# 运行 unshare -p -f --mount-proc bash
unshare -p -f --mount-proc bash

# 在新的 Bash shell 中查看进程 ID,只能看到PID=1的 bash进程和 ps aux进程
ps aux

2.在另一个终端B中执行以下指令:

# 可以看到很多 PID
ps aux

两段指令的终端对比图如下:

image.png

通过上面的对比图可以看出,在终端 A中实现了 PID的隔离,ps aux看不到宿主机中的所有进程,这样可以实现进程级别的安全隔离,即使在新的命名空间中运行的进程出现问题,也不会影响到系统中的其他进程。

使用场景

  1. 容器技术:PID 命名空间和挂载命名空间在容器技术中非常重要。通过 PID 命名空间,容器可以拥有独立的进程 ID 号,这使得容器中的进程可以独立管理和调试。通过挂载新的 /proc 文件系统,容器可以拥有独立的进程视图,便于监控和管理。
  2. 安全隔离:通过创建新的 PID 命名空间和挂载新的 /proc 文件系统,可以实现进程级别的安全隔离。这样一来,即使在新的命名空间中运行的进程出现问题,也不会影响到系统中的其他进程。

Mount Namespace

定义

Mount Namespace 用于隔离挂载点。不同的 Mount Namespace 可以拥有各自独立的文件系统视图。这样一来,在一个 Namespace 中对文件系统的修改不会影响到其他 Namespace。

如下指令:系统会创建一个新的挂载命名空间,使得在该命名空间中的挂载操作不会影响到其他的命名空间

# 创建新的 Mount Namespace 并运行一个 Bash 进程
unshare -m bash

当你运行unshare -m bash时,会发生以下事情:

  1. 创建新的挂载命名空间:使用 -m 选项,系统创建一个新的挂载命名空间。这个新的命名空间是当前进程的子命名空间,拥有独立的挂载点视图。
  2. 启动新的 Bash Shell:在这个新的挂载命名空间中启动一个新的 Bash shell。这个 Bash shell 及其子进程将只在这个新的挂载命名空间中运行。
  3. 独立的挂载操作:在这个新的 Bash shell 中进行的挂载和卸载操作(如 mount 和 umount 命令)将不会影响到其他命名空间中的挂载点。

示例

为了更好地说明 Mount Namespace,下面以一个示例进行展示:

1.在终端A中执行以下指令(子 namespace):

# 运行 unshare -m bash
unshare -m bash

# 在新的挂载命名空间中挂载一个临时文件系统
mount -t tmpfs none /mnt

# 查看挂载点
mount | grep /mnt

2.在另一个终端B中执行以下指令(父 namespace):

# 在父命名空间中,这个挂载点是不可见的
mount | grep /mnt

两段指令的终端对比图如下:

image.png

使用场景

  1. 容器技术:通过挂载命名空间,容器可以拥有独立的文件系统视图,使得容器中的文件系统操作不会影响到宿主系统和其他容器。
  2. 安全隔离:通过挂载命名空间,可以实现文件系统级别的安全隔离。例如,你可以在一个挂载命名空间中挂载一个只读的文件系统,从而防止进程对文件系统进行修改。

UTS Namespace

定义

UTS (UNIX Time-Sharing) Namespace 用于隔离主机名和域名。不同的 UTS Namespace 可以拥有不同的主机名和域名,这对于容器化应用非常有用。

如下指令:系统会创建一个新的命名空间,用于隔离主机名和域名

# 创建新的 UTS Namespace 并运行一个 Bash 进程
unshare -u bash
hostname new_hostname

指令解释

  • unshare:用于创建并进入一个或多个新的命名空间。
  • -u:创建一个新的 UTS 命名空间。在新的 UTS 命名空间中,主机名和域名是独立的。
  • bash:在新的命名空间中启动一个新的 Bash shell。
  • hostname:用于查看或设置系统的主机名。
  • new_hostname:要设置的新主机名。

示例

为了更好地说明 UTS Namespace,下面以一个示例进行展示:

1.在终端A 中执行以下指令(子 namespace):

# 创建新的 UTS 命名空间并启动 Bash
unshare -u bash

# 查看当前主机名
hostname

# 修改主机名
hostname new_hostname

# 再次查看主机名,确认修改
hostname

2.在终端B 中执行以下指令(父 namespace):

# 在父命名空间中查看主机名,主机名没有变化
hostname

两段指令的终端对比图如下:

image.png

使用场景

  1. 容器技术:UTS 命名空间在容器技术中非常重要。通过 UTS 命名空间,每个容器可以拥有独立的主机名和域名,这对于多租户环境和隔离应用非常有用。
  2. 多租户环境:在多租户环境中,不同的租户需要独立的主机名和域名。通过 UTS 命名空间,可以为每个租户创建独立的命名空间,从而实现资源的隔离和独立管理。

IPC Namespace

定义

IPC (Inter-Process Communication) Namespace 用于隔离进程间通信资源,如消息队列、信号量和共享内存。不同的 IPC Namespace 之间的通信资源是隔离的。

如下指令:是一个用于创建新的 IPC(Inter-Process Communication)命名空间并在其中启动一个新的 Bash shell 的命令

# 创建新的 IPC Namespace 并运行一个 Bash 进程
unshare -i bash

# 创建一个新的消息队列
ipcmk -Q

示例

为了更好地说明 UTS Namespace,下面以一个示例进行展示:

1.在终端A 中执行以下指令(子 namespace):

# 创建一个新的 IPC 命名空间并启动一个新的 Bash shell
unshare -i bash

# 创建一个新的消息队列
ipcmk -Q

# 查看当前消息队列
ipcs -q

2.在终端B 中执行以下指令(父 namespace):

# 在父命名空间中查看消息队列,没有新的消息队列,这验证了 IPC 命名空间的隔离效果
ipcs -q

两段指令的终端对比图如下:

image.png

使用场景

  1. 容器技术:通过 IPC 命名空间,每个容器可以拥有独立的进程间通信资源,这对于隔离应用和提高系统安全性非常有用。
  2. 多租户环境:在多租户环境中,不同的租户需要独立的进程间通信资源。通过 IPC 命名空间,可以为每个租户创建独立的命名空间,从而实现资源的隔离和独立管理。

Network Namespace

定义

Network Namespace 用于隔离网络资源,如网络接口、路由表、防火墙规则等。每个 Network Namespace 可以拥有独立的网络设备和配置。

# 创建新的 Network Namespace 并运行一个 Bash 进程
ip netns add mynamespace
ip netns exec mynamespace bash

示例

为了更好地说明 Network Namespace,下面以一个示例进行展示如何创建和配置 Network Namespace:

1.在终端A 中执行以下指令(子 namespace):

# 创建新的 Network Namespace
ip netns add mynamespace

# 创建一对 veth 设备
ip link add veth0 type veth peer name veth1

# 将 veth1 移动到新的命名空间中
ip link set veth1 netns mynamespace

# 在默认命名空间中设置 veth0
ip addr add 192.168.1.1/24 dev veth0
ip link set veth0 up

# 在新的命名空间中设置 veth1
ip netns exec mynamespace ip addr add 192.168.1.2/24 dev veth1
ip netns exec mynamespace ip link set veth1 up

# 在新的命名空间中设置默认路由
ip netns exec mynamespace ip route add default via 192.168.1.1

2.在终端B 中执行以下指令(父 namespace):

# 在父命名空间可以通过 ping 命令测试与 mynamespace 中的网络连接
ping 192.168.1.2

3.在终端A 中执行以下指令(子 namespace):

# 在 mynamespace 中,可以通过 ping 命令测试与默认命名空间中的网络连接
ip netns exec mynamespace ping 192.168.1.1

三段指令的终端对比图如下:

image.png

使用场景

  1. 容器技术:通过 Network Namespace,每个容器可以拥有独立的网络栈,从而实现网络资源的隔离和独立管理。
  2. 多租户环境:在多租户环境中,不同的租户需要独立的网络配置和安全策略。通过 Network Namespace,可以为每个租户创建独立的网络栈,从而实现网络资源的隔离和独立管理。

User Namespace

定义

User Namespace 用于隔离用户和用户组 ID。它允许在不同的 Namespace 中使用相同的用户 ID,但这些 ID 在不同的 Namespace 中是独立的。这样一来,即使在容器中运行的进程以 root 身份运行,也不会拥有对宿主系统的 root 权限。

# 创建新的 User Namespace 并运行一个 Bash 进程
unshare -U bash

示例

为了更好地说明 User Namespace,下面以一个示例进行展示如何创建和配置 Network Namespace:

1.在终端A 中执行以下指令(子 namespace):

# 创建新的 User Namespace 并启动 Bash
unshare -U bash

# 在新的命名空间中查看当前用户ID
id

# 在外部命名空间中设置用户ID映射
# 假设当前用户ID为1000
# 需要在另一个终端中运行以下命令
newuidmap $(pgrep -n bash) 0 1000 1

2.在终端B 中执行以下指令(父 namespace):

# 在父空间查看文件的所有者,找不到文件
ls -l /tmp/testfile

3.在终端A 中执行以下指令(子 namespace):

# 在新的命名空间中创建一个文件
touch /tmp/testfile

# 查看文件的所有者
ls -l /tmp/testfile

三段指令的终端对比图如下:

image.png 文件的所有者将是新的用户ID映射中的用户,而不是外部命名空间中的用户。

使用场景

  1. 容器技术:通过 User Namespace,每个容器可以拥有独立的用户和用户组ID映射,从而实现特权分离和安全隔离。这使得容器中的进程可以以root身份运行,同时在宿主系统中仍然是非特权用户。
  2. 多租户环境:在多租户环境中,不同的租户需要独立的用户和用户组ID映射。通过 User Namespace,可以为每个租户创建独立的用户和用户组ID映射,从而实现资源的隔离和独立管理。

无法 Namespace的资源

尽管 Linux的 Namespace机制提供了对多种系统资源的隔离,但并不是所有的系统资源都能被 Namespace隔离,以下是一些不能被 Namespace隔离的资源及其原因:

  • 内核模块: 内核模块(Kernel Modules)在整个系统中是全局共享的。加载或卸载一个内核模块会影响到所有Namespace中的进程。
  • 内核参数: 通过sysctl命令设置的内核参数(如/proc/sys下的参数)是全局的,无法在不同的Namespace中进行独立设置。
  • 硬件资源:硬件资源是物理存在的,无法通过软件机制进行隔离。
  • CPU:尽管Cgroups可以对CPU资源进行分配和限制,但CPU本身是一个物理资源,无法在不同的Namespace中进行隔离。
  • 内存:Cgroups可以对内存资源进行分配和限制,但物理内存本身无法在不同的Namespace中进行隔离。
  • 磁盘:磁盘设备是物理存在的,无法在不同的Namespace中进行隔离。尽管可以通过Cgroups对磁盘I/O进行限制,但磁盘设备本身是共享的。
  • 时间:系统时间(如系统时钟和硬件时钟)在整个系统中是共享的,无法在不同的Namespace中进行独立设置。
  • 安全机制:一些系统级的安全机制无法在不同的Namespace中进行隔离。
  • SELinux:SELinux(Security-Enhanced Linux)是一种安全模块,它的策略在整个系统中是全局的,无法在不同的Namespace中进行独立设置。
  • AppArmor:类似于SELinux,AppArmor也是一种安全机制,它的配置和策略在整个系统中是全局的。
  • 系统日志:系统日志(如/var/log下的日志文件)在整个系统中是共享的,无法在不同的Namespace中进行独立管理。
  • 特殊设备文件:一些特殊的设备文件(如/dev下的某些设备文件)在不同的Namespace中是共享的,无法进行隔离。例如,/dev/null/dev/zero等设备文件在整个系统中是全局的。

Cgroups 详解

Cgroups 的基本概念

Cgroups (Control Groups,控制组)是 Linux 内核的一个特性,用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)。

Cgroups 通过将进程分组,然后对这些组应用资源限制来工作,核心组件包括:

Cgroup

Cgroup是一个控制组,表示一组进程。Cgroup通过层级结构组织,类似于文件系统的目录结构。每个节点代表一个Cgroup,节点之间的层级关系表示Cgroup的继承关系。

Hierarchy

Hierarchy是Cgroup的组织结构。一个Hierarchy可以包含多个Cgroup,并且每个Hierarchy可以挂载到一个目录中。在同一个Hierarchy中,Cgroup之间存在父子关系,子Cgroup继承父Cgroup的限制和策略。

Subsystem

Subsystem是具体的资源控制器,如CPU、内存、块I/O等。每个Subsystem负责对特定类型的资源进行管理和限制。例如,cpu Subsystem用于控制CPU资源,memory Subsystem用于控制内存资源。

Cgroups 的子系统

Cgroups 支持多种子系统,每种子系统负责不同的资源控制:

  • cpu: 控制 CPU 资源的分配。
  • cpuacct: 提供 CPU 资源使用的统计信息。
  • memory: 控制内存资源的分配和使用。
  • blkio: 控制块设备的 I/O 操作。
  • net_cls: 控制网络资源的分类。
  • devices: 控制设备的访问权限。
  • freezer: 暂停和恢复进程。

创建和管理 Cgroups

通过命令行工具 cgcreatecgsetcgexec 可以方便地创建和管理 Cgroups。

# 创建一个新的 Cgroup
cgcreate -g cpu,memory:/mygroup

# 设置 CPU 使用限制
cgset -r cpu.shares=512 mygroup

# 设置内存使用限制
cgset -r memory.limit_in_bytes=256M mygroup

# 将一个进程加入到 Cgroup
cgexec -g cpu,memory:mygroup /bin/bash

Cgroups v1 & v2

Cgroups有两个版本:v1和v2。Cgroups v1提供了多种独立的子系统,每个子系统可以有独立的层级结构。Cgroups v2则提供了统一的层级结构,所有子系统共享同一个层级。

Cgroups v1

在Cgroups v1中,每个子系统可以有独立的层级结构。例如,可以有一个层级用于CPU资源控制,另一个层级用于内存资源控制。

Cgroups v2

Cgroups v2 是 Cgroups 的第二个版本,提供了更为统一和简化的接口。Cgroups v2 的主要特点包括:

  • 统一的层级结构,所有子系统共享同一个层级。
  • 更为简化的配置接口,减少了配置的复杂性。
  • 提高了资源控制的精度和灵活性。

示例

为了更好地说明 Cgroups,这里以两个示例进行说明

示例1:限制CPU使用

下面是一个示例,展示如何使用Cgroups限制CPU使用:

# 创建一个新的Cgroup
cgcreate -g cpu:/cpulimited

# 设置CPU使用限制
cgset -r cpu.shares=512 cpulimited

# 将一个进程加入到Cgroup
cgexec -g cpu:cpulimited /bin/bash

在这个示例中,我们创建了一个名为cpulimited的 Cgroup,并设置了CPU使用限制。然后,我们将一个新的Bash进程加入到这个Cgroup中。

示例2:限制内存使用

下面是一个示例,展示如何使用 Cgroups 限制内存使用:

# 创建一个新的Cgroup
cgcreate -g memory:/memlimited

# 设置内存使用限制
cgset -r memory.limit_in_bytes=256M memlimited

# 将一个进程加入到Cgroup
cgexec -g memory:memlimited /bin/bash

在这个示例中,我们创建了一个名为memlimited的 Cgroup,并设置了内存使用限制。然后,我们将一个新的Bash进程加入到这个Cgroup中。

使用场景

  1. 容器技术:通过Cgroups,可以为每个容器设置独立的资源限制,从而实现资源的隔离和独立管理。这使得容器中的进程可以在有限的资源范围内运行,而不会影响到宿主系统和其他容器。
  2. 多租户环境:在多租户环境中,不同的租户需要独立的资源限制和管理策略。通过Cgroups,可以为每个租户创建独立的Cgroup,从而实现资源的隔离和独立管理。

为什么学习?

上面的内容我们详细地分析了 Namespace 和 Cgroups两个技术点,那么,为什么要学习它们,它们有什么实际性的应用?

Namespace 和 Cgroups 的结合使用是容器技术(比如 Docker)的基础,Namespace 提供了进程级别的隔离,而 Cgroups 则用于资源的分配和限制,通过这两种机制,可以创建高效且安全的容器化环境。

容器的创建

以下是一个简单的示例,展示如何使用 Namespace 和 Cgroups 创建一个容器:

# 创建新的 Namespace
unshare -p -f -m -u -i -n --mount-proc bash

# 设置主机名
hostname container

# 挂载新的文件系统
mount -t tmpfs none /tmp

# 创建新的 Cgroup
cgcreate -g cpu,memory:/container

# 设置 CPU 和内存限制
cgset -r cpu.shares=512 container
cgset -r memory.limit_in_bytes=256M container

# 将当前进程加入到 Cgroup
cgclassify -g cpu,memory:container $$

在上述示例中:

  • 首先,创建了一个新的 Namespace
  • 然后,设置了主机名并挂载了新的文件系统
  • 接着,创建了一个新的 Cgroup,并设置了 CPU 和内存限制
  • 最后,将当前进程加入到 Cgroup,这样一个容器就创建完成

总结

本文,我们分析了 Linux 内核提供的两种关键机制:Namespace 和 Cgroups,它们用于实现系统资源的隔离和管理。

Namespace 提供了进程级别的隔离,使得不同的进程组可以拥有各自独立的资源视图,而 Cgroups 则用于资源的分配和限制,通过将进程分组,然后对这些组应用资源限制来工作。结合使用这两种机制,可以创建高效且安全的容器化环境。

因此,掌握 Namespace 和 Cgroups 两个技术点,可以帮助我们更好地理解容器(比如 Docker)的实现原理。

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。