深入解析:Docker 容器如何实现文件系统与资源的多维隔离?
目录
- 一、RootFs
- 1. Docker 镜像与文件系统层
- 2. RootFs 与容器隔离的意义
- 二、Linux Namespace
- 1. 进程命名空间
- 1.1 `lsns` 命令说明
- 1.2 查看“祖先进程”命名空间
- 1.3 查看当前用户进程命名空间
- 2. 容器进程命名空间
- 2.1 查看容器进程命名空间列表
- 2.2 容器进程命名空间的具体体现
- 三、cgroups
- 1. cpu 子系统
- 1.1 CFS (Completely Fair Scheduler)
- 1.2 RT (Real-time Scheduler)
- 1.3 示例
- 2. cpuset 子系统
- 3. cpuacct 子系统
- 4. memory 子系统
- 示例
- 5. blkio 子系统
- 6. devices 子系统
- 7. freezer 子系统
- 8. net_cls 子系统
- 9. net_prio 子系统
- 10. perf_event
- 11. hugetlb
- 总结
一、RootFs
在传统的 Linux 系统中,rootfs(根文件系统)就是系统的“根”目录,一般位于 /
,其下包含 /bin
、/dev
、/etc
等常见目录结构。
在容器中,则通过镜像(Image)+**容器层(Container Layer)**的组合来提供容器自身的根文件系统,这个容器的根文件系统就是rootfs
,与宿主机的 rootfs 相对隔离。
1. Docker 镜像与文件系统层
- **镜像(Image)**由多层只读层(read-only layers)构成,利用了 Union FS 或 OverlayFS 等联合文件系统技术。
- **容器层(Container Layer)**是该镜像之上的可写层(read-write layer)。容器运行时,对文件的修改都会写入到这个容器层中,不会影响只读层的内容。
2. RootFs 与容器隔离的意义
- 当启动一个容器时,容器看到的“根文件系统”并非与宿主机相同,而是来自镜像 + 容器层组合形成的 rootfs。
- 对于容器内部的进程来说,
/
就是容器自身的根目录,与宿主机的/
相互独立(尽管底层还是同一个内核)。 - 这种文件系统级别的隔离为容器提供了和宿主机隔离的外观,不同容器之间也不会直接污染彼此的文件系统。
二、Linux Namespace
Linux Namespace 是容器隔离的核心机制之一。Namespace 的主要作用是将系统资源进行“分名字空间”隔离,如进程(PID)、网络(NET)、文件系统挂载(MOUNT)、用户(USER)、IPC 等,从而让容器“以为”它拥有自己独立的环境。
1. 进程命名空间
1.1 lsns
命令说明
lsns
是 Linux 提供的一个查看系统中命名空间信息的命令。可以用lsns
查看当前系统里所有命名空间,如lsns -t pid
可以查看 PID 命名空间,lsns -t net
查看网络命名空间等。- 常见列示信息包含:
- NS TYPE: 命名空间类型(如
pid
、net
、mnt
、uts
、ipc
、user
等)。 - NSID: 命名空间 ID,一般可以在
/proc/pid/ns
路径中看到相应符号链接。 - PID: 对应的进程号。
- Command: 该命名空间对应进程的启动命令。
- NS TYPE: 命名空间类型(如
1.2 查看“祖先进程”命名空间
在 Linux 中,每个进程都有对应的命名空间引用(指针)。如果想要查看某个进程(比如 PID=1 或者宿主机上的某个 PID)的命名空间,可以通过:
列出系统所有命名空间
sudo lsns --output0all
ls -l /proc/<PID>/ns
这会展示对应的符号链接,比如:
lrwxrwxrwx 1 root root 0 Jan 1 00:00 /proc/1/ns/pid -> pid:[4026531836]
这样就能看到 pid:[4026531836]
对应的命名空间 ID。通常 PID=1(系统的 init 或 systemd)会在宿主机的最初始 namespace 中。
1.3 查看当前用户进程命名空间
直接对你当前 shell 的 PID 进行查看:
echo $$
# 假设输出为 12345
ls -l /proc/12345/ns
就能看到你当前 shell 进程所使用的 namespace。
2. 容器进程命名空间
容器之所以能够让其内部进程彼此隔离,主要原因之一是 Docker(或其他容器运行时)在启动容器进程时,会为该进程创建或加入单独的命名空间(PID/NET/IPC/UTS 等)。
2.1 查看容器进程命名空间列表
假设我们有一个正在运行的容器,可以先找到容器对应的“容器进程”:
docker ps
docker inspect <container_id> | grep "Pid"
然后拿到对应的 PID,比如是 23456。接着:
ls -l /proc/23456/ns
就能看到容器进程使用的所有 namespace 绑定信息。
2.2 容器进程命名空间的具体体现
- PID Namespace
容器内部查看到的进程号(PID)从 1 开始,而在宿主机上,这个容器的进程是一个完全不同的 PID 值。容器内部的“PID=1”通常是容器内的 init 进程。 - Network Namespace
容器有自己单独的网卡配置(如eth0
),与宿主机是隔离的。通过容器的 network namespace,可以将容器网络与宿主机网络解耦(或进行端口映射)。 - Mount Namespace
容器有自己挂载的文件系统视图,比如/
是容器自己的 rootfs。与宿主机的挂载点不同。 - IPC Namespace
容器之间的共享内存、消息队列等 IPC 机制互不影响。 - UTS Namespace
容器可以有自己独立的 hostname。容器内hostname
与宿主机可以不同。
借助这些命名空间,容器可以呈现一个与宿主机几乎隔离的操作系统视图。
三、cgroups
cgroups (control groups) 是 Linux 提供的另一项关键特性,用于对系统资源进行“配额、限制、监控、隔离”。Docker 容器通过将容器进程加入到相应的 cgroup,来限制其对 CPU、内存、IO 等资源的使用或进行统计。
cgroups 是一个可插拔的框架,常见的子系统包括:
- cpu
- cpuset
- cpuacct
- memory
- blkio
- devices
- freezer
- net_cls
- net_prio
- perf_event
- hugetlb
等。
下面分别简要介绍这些子系统在容器隔离中的作用或使用示例。
1. cpu 子系统
cpu
子系统主要用于限制或分配 CPU 时间片给某个 cgroup 内进程。让我们来看看常见的调度器和示例。
1.1 CFS (Completely Fair Scheduler)
- Linux 默认的 CPU 调度器。可通过
cpu.shares
、cpu.cfs_period_us
、cpu.cfs_quota_us
等文件对 CPU 使用进行相对或绝对限额设置。 - 例如要限制某个 cgroup 的进程只能使用“相当于一个 CPU 核心”的计算量,可以在
cpu.cfs_period_us
= 100000(默认100ms)和cpu.cfs_quota_us
= 100000 之间做设置,这样就大致等价于 1 core。
1.2 RT (Real-time Scheduler)
- RT 调度针对实时任务,可以用来做实时优先级的资源控制。不过容器中常见应用较少直接动用 RT 调度。
1.3 示例
在手动配置 cgroup 时,可能会:
- 创建目录:
mkdir /sys/fs/cgroup/cpu/test_cgroup
- 写入一些限制:
表示此 cgroup 一次调度周期内(100ms),只能用 200ms CPU 时间,相当于可以使用 2 核的 CPU 时间。echo 200000 > /sys/fs/cgroup/cpu/test_cgroup/cpu.cfs_quota_us echo 100000 > /sys/fs/cgroup/cpu/test_cgroup/cpu.cfs_period_us
- 把某个进程写入 tasks:
该进程的 CPU 使用就受限于此组的规则。echo <pid> > /sys/fs/cgroup/cpu/test_cgroup/tasks
Docker 在启动容器时会自动做这些事情,如 --cpus
、--cpuset-cpus
等参数。
2. cpuset 子系统
cpuset
子系统允许指定某些 CPU 核、某些内存节点给特定的 cgroup。- 比如可以指定容器只能在 CPU 0 和 1 上运行,或者只能从 NUMA 节点0分配内存。
- Docker 对应的参数是
--cpuset-cpus="0-1"
之类。
3. cpuacct 子系统
cpuacct
用于统计某个 cgroup 内进程的 CPU 使用情况(用户态、内核态占用总时长),只做统计不做限制。- Docker 可以通过这个子系统查看容器的 CPU 使用状态。
4. memory 子系统
memory
子系统用于限制和统计进程的内存使用,包括物理内存和 swap。
- 常见的控制文件:
memory.limit_in_bytes
:该 cgroup 最大物理内存限制。memory.memsw.limit_in_bytes
:物理内存 + swap 限制(如果启用 swap 记账)。
- 通过设置这些值,可防止某些进程用光系统所有内存。
示例
- 创建 cgroup:
mkdir /sys/fs/cgroup/memory/test_mem
- 设置限制:
echo 524288000 > /sys/fs/cgroup/memory/test_mem/memory.limit_in_bytes # 500MB
- 将进程加入:
这样该进程占用内存在超过 500MB 时可能会触发 OOM(Out Of Memory)动作。echo <pid> > /sys/fs/cgroup/memory/test_mem/tasks
5. blkio 子系统
blkio
(Block IO) 用于限制进程的块设备 IO 速率,比如磁盘读写速度。- 可以设置读取速率、写入速率的限制。对需要在容器层面做 IO QoS 的场景很有帮助。
6. devices 子系统
devices
子系统可以控制某个 cgroup 中的进程可以访问哪些设备、只能读或写、或完全禁止访问等。- 容器通常为了安全,会只允许访问少数必要设备(比如
/dev/null
、/dev/random
等)。
7. freezer 子系统
freezer
子系统提供了把 cgroup 内进程“冻结/解冻”的功能。- 可以把某个 cgroup 的状态设置为
FROZEN
,则该组内所有进程都挂起,等到切回THAWED
才继续运行。
8. net_cls 子系统
net_cls
可以为网络数据包打上一个分类标识(classid),配合 tc(traffic control)做网络流量整形或带宽控制。
9. net_prio 子系统
net_prio
子系统可以为 cgroup 中的进程设置网络优先级(priority),从而在同一台宿主机上的容器间做网络流量优先级区分。
10. perf_event
perf_event
子系统方便对一组进程进行性能计数器(performance counter)的监控,比如 CPU cycle、cache miss 等。
11. hugetlb
hugetlb
用于管理大页内存(Huge Pages)。可以限制某个 cgroup 使用多少大页内存。
总结
通过 RootFs、Linux Namespace 和 cgroups 的巧妙组合,Docker 容器能够在同一个 Linux 内核上运行,却拥有与宿主机和其他容器相对独立的文件系统、进程空间、网络环境、IPC、以及严格的资源配额/限制。这为容器提供了接近虚拟机的隔离性,同时也保留了“共享同一个内核”的优势(启动速度快、资源开销小等)。
- RootFs:让容器拥有独立的文件系统视图,与宿主机的根目录区分开来。
- Linux Namespace:
- PID Namespace 让容器内部进程有各自的 PID 视图。
- Network Namespace 让容器拥有独立的虚拟网卡、网络栈。
- Mount Namespace 让容器控制自己的挂载点。
- UTS、IPC、User Namespace 等也实现其他层面的隔离。
- cgroups:
- 对 CPU、内存、IO 等进行资源限制和监控。
- 通过 Docker 参数可很方便地指定容器的资源上限、优先级。
这些机制共同构成了 Docker 容器环境下的核心隔离和限制手段,使得容器能够安全、稳定地在生产环境中运行各种应用。
参考:
0voice · GitHub