容器技术三剑客:Namespace、Cgroups、UnionFS 的通俗解析
一、Namespace:给进程 “划地盘” 的隔离管家
核心作用:让不同进程以为自己 “独占” 了整个系统资源(如文件目录、网络、进程号等),但实际是在各自的 “虚拟空间” 里活动。
工作原理类比:
就像在一间大教室里用隔板(Namespace)隔出多个小房间,每个房间里的学生(进程)以为自己独占了教室的黑板(文件系统)、讲台(网络接口)、座位号(PID),但其实这些资源是共享的,只是通过隔板让他们看不到彼此的存在。
- 具体隔离场景:
- Mount Namespace:比如你在容器里用
cd /
看到的根目录,其实是容器独有的 “虚拟目录”,和宿主机的/
完全不同(通过chroot
切换目录)。 - PID Namespace:容器里的进程号(如 PID=1)和宿主机的进程号互不干扰,就像两个班各自排学号,1 号学生不是同一个人。
- Network Namespace:每个容器有独立的网卡、IP 地址、端口,就像每个房间有独立的电话线,互相打不通,只能通过教室的总交换机(宿主机网络)中转。
- Mount Namespace:比如你在容器里用
Linux Namespace 是容器技术的核心隔离机制,它通过内核提供的系统调用和数据结构,让进程只能访问特定的系统资源。以下是其具体实现逻辑的解析:
Linux 通过以下几个关键系统调用实现 Namespace 隔离:
- clone()创建新进程时指定隔离类型,例如:
-
clone(child_func, stack, CLONE_NEWNS | // Mount Namespace CLONE_NEWPID | // PID Namespace CLONE_NEWNET | // Network Namespace SIGCHLD, arg);
-
unshare()
让当前进程脱离某个 Namespace,加入新的 Namespace:unshare(CLONE_NEWNS); // 脱离当前 Mount Namespace
-
setns()
让进程加入已存在的 Namespace:int fd = open("/proc/1234/ns/mnt", O_RDONLY); setns(fd, 0); // 加入 PID 为 1234 的进程的 Mount Namespace
-
Linux 内核目前支持 6 种 Namespace,每种对应不同的系统资源隔离:
Namespace 类型 | 隔离的资源 | 系统调用标志 | 内核版本 |
---|---|---|---|
Mount (mnt) | 文件系统挂载点 | CLONE_NEWNS | 2.4.19 |
PID | 进程 ID 空间 | CLONE_NEWPID | 2.6.24 |
Network (net) | 网络栈(网卡、IP、端口) | CLONE_NEWNET | 2.6.29 |
UTS | 主机名和 NIS 域名 | CLONE_NEWUTS | 2.6.19 |
IPC | 进程间通信资源 | CLONE_NEWIPC | 2.6.19 |
User (user) | 用户和组 ID 空间 | CLONE_NEWUSER | 3.8 |
Namespace 的数据结构:
struct task_struct {
// ...
struct nsproxy *nsproxy; // 指向进程所属的 Namespace
// ...
};
struct nsproxy {
atomic_t count; // 引用计数
struct mnt_namespace *mnt_ns; // Mount Namespace
struct uts_namespace *uts_ns; // UTS Namespace
struct ipc_namespace *ipc_ns; // IPC Namespace
struct pid_namespace *pid_ns; // PID Namespace
struct net *net_ns; // Network Namespace
struct user_namespace *user_ns; // User Namespace
};
Namespace 的文件系统接口
Linux 通过 /proc
文件系统暴露 Namespace 信息:
/proc/[pid]/ns/
目录下包含当前进程所属的所有 Namespace 文件:
$ ls -l /proc/self/ns/
lrwxrwxrwx 1 root root 0 Jul 1 10:00 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul 1 10:00 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jul 1 10:00 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Jul 1 10:00 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul 1 10:00 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 1 10:00 uts -> uts:[4026531838]
- 这些文件是特殊的 inode,相同 Namespace 的进程指向同一个 inode。
- 可以通过
setns()
系统调用和这些文件让进程加入其他 Namespace。
二、Cgroups:给进程 “定规矩” 的资源裁判
核心作用:限制进程能使用的资源上限,避免某个进程 “吃太多” 导致系统崩溃。
工作原理类比:
好比学校给每个班级分配固定的零食(CPU)、饮用水(内存)份额,每个学生(进程)不能超过班级的配额。如果某个班的学生吃太多零食,裁判(Cgroups)会强制限制,防止他们撑坏肚子(系统资源耗尽)。
- 具体限制方式:
- CPU 限制:可以规定某个容器最多只能用 2 个 CPU 核心,即使宿主机有 8 个核心,它也只能用 2 个。
- 内存限制:比如给容器分配 1GB 内存,当它用到 1GB 时就会被强制杀死,避免占用宿主机其他进程的内存。
- 磁盘 IO 限制:限制容器写磁盘的速度,防止它把硬盘 “写爆” 影响其他程序。
Cgroups 通过以下组件实现资源控制:
-
核心系统调用
clone()
:创建进程时加入特定 Cgroup(通过CLONE_NEWCGROUP
标志,需内核支持)。cgroup_set()
/cgroup_get()
:直接操作 Cgroup 文件系统接口。cgroup_procs
文件:将进程 PID 写入对应 Cgroup 目录,实现进程分组。
-
文件系统接口
Cgroups 通过虚拟文件系统cgroupfs
或systemd-cgroup
暴露控制接口,典型路径如:/sys/fs/cgroup/cpu/my_container/cpu.cfs_quota_us # CPU 配额配置 /sys/fs/cgroup/memory/my_container/memory.limit_in_bytes # 内存限制
Cgroups 的核心实现逻辑
以 CPU 资源限制和内存资源限制为例,解析具体工作机制:
1. CPU 子系统(cpu,cpuacct)的实现
-
CFS(完全公平调度)配额机制
-
周期与配额配置:
cfs_period_us
:定义资源分配周期(默认 100ms)。cfs_quota_us
:定义进程组在周期内最多可使用的 CPU 时间(-1 表示无限制)。
例如:cfs_period_us=100000
(100ms)+cfs_quota_us=50000
(50ms)表示限制为 50% CPU 使用率。
-
调度执行逻辑:
- 内核通过
cpu_cgroup_schedule()
钩子介入进程调度,检查当前进程所属 Cgroup 的配额。 - 当进程使用的 CPU 时间超过
cfs_quota_us
时,将其加入等待队列,直到下一个周期。
- 内核通过
-
-
CPU 权重(shares)机制
- 通过
cpu.shares
配置相对权重(默认 1024),用于多核或多进程组竞争资源时的比例分配。 - 例如:组 A(shares=2048)和组 B(shares=1024)竞争 1 个 CPU 核心时,组 A 获得 2/3 资源。
- 通过
2. 内存子系统(memory)的实现
-
内存限制与 OOM(Out of Memory)机制
-
限制配置:
- 通过
memory.limit_in_bytes
设定进程组可使用的最大内存(包括物理内存和 Swap)。 memory.swappiness
控制内存换页策略(0 表示优先淘汰页面,100 表示优先使用 Swap)。
- 通过
-
内存监控与回收
- 内核通过
memory_cgroup_commit_charge()
钩子监控内存分配,超过限制时触发:- 优先回收进程组内的页面缓存(page cache)。
- 若仍不足,触发 OOM Killer,杀死组内占用内存最多的进程。
- 内核通过
-
-
内存统计与压力反馈
memory.usage_in_bytes
实时统计当前内存使用量。- 内存压力通过
memory.pressure
文件暴露,供上层调度系统(如 OOM Killer)决策。
三、UnionFS:给容器 “搭积木” 的文件系统魔术师
UnionFS 是一种「联合文件系统」,核心能力是将多个不同位置的目录「合并」成一个虚拟文件系统,让用户看起来像访问单个目录一样。它的特点是 分层存储、写时复制(Copy-on-Write),这也是容器镜像实现轻量化的关键。
核心作用:将多个文件系统层 “叠加” 在一起,形成一个统一的目录结构,用于保存操作系统和应用文件,同时支持 “只读 + 可写” 的分层机制。
工作原理类比:
像搭积木一样,最底层是操作系统的基础镜像(只读,如 Ubuntu 系统),中间层是应用依赖(如 Java 环境,只读),最上层是容器运行时的临时修改(可写)。就像在一本固定的教科书(底层镜像)上贴便签(应用层),再用铅笔写笔记(运行时修改),最后看到的是 “书 + 便签 + 笔记” 的整体内容,但修改不会影响原书。
- 具体工作流程:
- 镜像分层:比如 Docker 镜像由多个只读层组成(如 Ubuntu 层、Nginx 层),每层只保存和上一层的差异。
- 容器运行时:启动容器时,会在镜像顶层添加一个可写层(容器层),所有对文件的修改(如新建文件、修改配置)都保存在这个层里。
- 挂载到容器:通过 Mount Namespace 将这个 “叠加后的文件系统” 挂载到容器的根目录,容器进程看到的就是完整的文件系统,但实际修改不会影响底层镜像。
UnionFS 的核心工作逻辑:分层与挂载
以 Docker 中常用的 UnionFS 类型(如 Overlay2)为例,其工作原理可以拆解为以下步骤:
1. 镜像的分层结构
- 基础层(只读层):容器镜像的每一层(如操作系统、应用依赖)都是一个只读的目录,类似「模板」。例如,一个 Ubuntu 镜像可能包含
base
层(系统基础文件)、python
层(Python 环境)、app
层(应用代码)。 - 容器层(可写层):当容器运行时,UnionFS 会在所有只读层之上创建一个 可写层(Writable Layer),用于存储容器运行时修改的文件(如新增的文件、修改的配置)。
2. 合并挂载:虚拟文件系统的形成
- UnionFS 的挂载过程:
- 所有只读层按顺序堆叠(如
base
→python
→app
),每一层只能读取上一层的文件,但不能修改。 - 最上层是可写层,容器进程对文件的所有修改(增、删、改)都只会作用于这一层。
- UnionFS 通过内核机制将这些分层「合并」成一个虚拟目录,容器进程看到的就是这个合并后的文件系统。
- 所有只读层按顺序堆叠(如
3. 写时复制(Copy-on-Write,CoW)
- 修改文件的逻辑:当容器需要修改只读层中的某个文件时(例如修改
/etc/hosts
),UnionFS 不会直接修改原文件,而是执行以下操作:- 将只读层中的目标文件「复制」到可写层。
- 在可写层中修改这个复制后的文件,原只读层的文件保持不变。
- 好处:多个容器共享同一个镜像时,只读层的文件只需存储一份,节省磁盘空间;修改时才复制,避免重复占用资源。
4. 删除与重命名的实现
- 删除文件:如果删除的是只读层中的文件,UnionFS 不会真正删除原文件,而是在可写层中创建一个「白名单文件」(.wh 文件),标记该文件为「隐藏」,这样在虚拟文件系统中就看不到这个文件了。
- 重命名文件:类似修改操作,会先将文件复制到可写层,再执行重命名操作。
UnionFS 在容器中的实际应用
以 Docker 启动容器为例:
- 镜像加载:Docker 拉取镜像时,将各分层解压到宿主机的
/var/lib/docker/overlay2
目录下的不同分层目录(如lowerdir
存储只读层,upperdir
存储可写层)。 - 容器启动:通过
Mount Namespace
将 UnionFS 合并后的虚拟目录挂载为容器的根目录(/
),并通过chroot
切换容器进程的根目录,使其只能访问该虚拟文件系统。 - 运行时修改:容器运行中产生的所有文件修改都保存在可写层,容器删除后,可写层也会被清理,不影响原镜像。
常见 UnionFS 类型对比
类型 | 系统支持 | 特点 |
---|---|---|
Overlay2 | Linux 4.0+ | Docker 默认 UnionFS,性能较好,支持分层数量多,适合生产环境。 |
AUFS | 旧版 Docker | 早期 Docker 使用,分层性能略低,目前逐渐被 Overlay2 取代。 |
Btrfs | 部分 Linux 系统 | 支持快照、写时复制,功能全面,但对系统兼容性要求较高。 |
ZFS | 特定系统 | 强于数据完整性和压缩,常用于存储密集型场景,但资源消耗较大。 |
UnionFS 就像一个「文件拼接大师」,把多个分层目录合并成一个虚拟空间,让容器既能共享基础镜像的文件(只读层),又能独立保存自己的修改(可写层)。通过写时复制和分层机制,它实现了容器镜像的轻量化和隔离性,是容器技术中「一次构建,多次运行」的核心基础。
四、三者如何配合打造容器?
- UnionFS 先搭好 “舞台”:把操作系统和应用文件按分层叠好,形成容器的文件系统。
- Namespace 划分 “独立房间”:让容器进程以为自己在独立的系统里运行,看不到宿主机和其他容器的资源。
- Cgroups 定 “资源规矩”:限制容器进程能用多少 CPU、内存,防止它 “抢资源”。
举个生活例子:
- UnionFS 像搭建公寓楼(底层是毛坯房,中间层是装修,顶层是住户家具)。
- Namespace 像给每个住户分配独立的房间,关上门后看不到其他住户。
- Cgroups 像物业规定每户每月最多用 100 度电,不能超量。
通过这三个技术的配合,容器就能在隔离的环境中高效运行,同时不浪费宿主机资源,这也是 Docker 等容器技术的核心底层原理。
扩展参考文章:
Docker 上 Kubernetes 的入门到进阶指南:构建和管理你的容器化应用_kubernetes docker 先装谁?-CSDN博客