跳到主要内容
预计阅读 39 分钟

09|容器与虚拟化

虚拟化让一台物理机变成多台”虚拟机”,容器让一个操作系统里跑出多个”隔离环境”。本章将带你理解虚拟化的原理,深入 Namespace、Cgroups 这两大容器基石,以及 Docker 的底层实现机制。

📋 开篇自测:你已经知道多少?

  1. 虚拟机和容器的本质区别是什么?各自的优劣势是什么?
  2. Linux Namespace 提供了哪些隔离维度?
  3. Cgroups 解决了什么问题?它能限制哪些资源?

一、虚拟化:从一变多

1.1 为什么需要虚拟化

一台高配服务器可能有 128 核 CPU、512GB 内存,但大多数应用只需要 4 核 8GB。如果一台服务器只跑一个应用,资源利用率可能只有 5-10%。虚拟化的目标就是让一台物理机同时运行多个操作系统实例,把利用率提升到 60-80%。

1.2 虚拟化的三种层次

+------------------------------------------------------------------+
|                        虚拟化类型对比                               |
+------------------------------------------------------------------+
|                                                                  |
| 类型1: 全虚拟化 (Full Virtualization)                             |
|                                                                  |
|  VM1        VM2        VM3                                       |
|  +------+  +------+  +------+                                    |
|  |Guest |  |Guest |  |Guest |  <-- 完整的客户操作系统              |
|  | OS   |  | OS   |  | OS   |                                    |
|  +------+  +------+  +------+                                    |
|  +---------------------------+                                   |
|  |     Hypervisor (VMM)      |  <-- 虚拟机监控器                  |
|  +---------------------------+                                   |
|  +---------------------------+                                   |
|  |     Host OS (可选)         |                                   |
|  +---------------------------+                                   |
|  |     硬件                   |                                   |
|  +---------------------------+                                   |
|                                                                  |
| 类型2: 半虚拟化 (Paravirtualization)                              |
|  客户OS知道自己在虚拟环境中, 主动调用Hypervisor接口                  |
|  代表: Xen                                                       |
|                                                                  |
| 类型3: 硬件辅助虚拟化 (Hardware-Assisted)                          |
|  CPU 提供虚拟化指令集 (Intel VT-x, AMD-V)                        |
|  代表: KVM + QEMU                                                |
|                                                                  |
+------------------------------------------------------------------+

1.3 Type-1 vs Type-2 Hypervisor

Type-1 (裸金属):              Type-2 (托管型):
直接运行在硬件上               运行在宿主操作系统上

  VM1     VM2     VM3          VM1     VM2     VM3
  +---+   +---+   +---+       +---+   +---+   +---+
  |   |   |   |   |   |       |   |   |   |   |   |
  +---+   +---+   +---+       +---+   +---+   +---+
  +-----------------------+    +-----------------------+
  |    Hypervisor         |    |    Hypervisor         |
  +-----------------------+    +-----------------------+
  |    硬件               |    |    宿主操作系统        |
  +-----------------------+    +-----------------------+
                               |    硬件               |
代表: VMware ESXi              +-----------------------+
     Microsoft Hyper-V
     KVM (Linux内核模块)        代表: VirtualBox
                                     VMware Workstation

1.4 KVM:Linux 的虚拟化方案

KVM(Kernel-based Virtual Machine)是 Linux 内核的一个模块,把 Linux 变成了一个 Type-1 Hypervisor:

KVM + QEMU 架构:

  用户空间:
  +--------+  +--------+  +--------+
  | QEMU   |  | QEMU   |  | QEMU   |  每个VM一个QEMU进程
  | 进程   |  | 进程   |  | 进程    |  负责设备模拟
  +--------+  +--------+  +--------+
       |           |           |
  -----+-----系统调用-----------+------
       |           |           |
  内核空间:
  +----------------------------------------+
  |  KVM 模块 (/dev/kvm)                    |
  |  - 提供 CPU 虚拟化 (利用 VT-x/AMD-V)   |
  |  - 提供内存虚拟化 (EPT/NPT)            |
  |  - 提供中断虚拟化                       |
  +----------------------------------------+
  |  Linux 内核                             |
  +----------------------------------------+
  |  硬件 (CPU支持 VT-x/AMD-V)             |
  +----------------------------------------+

工作流程:
  1. QEMU 通过 ioctl(/dev/kvm) 创建虚拟机
  2. 设置虚拟CPU、内存、设备
  3. 进入 Guest 模式执行客户OS代码
  4. 遇到特权指令 -> VM Exit -> KVM处理 -> VM Entry -> 继续

二、容器:轻量级的隔离

2.1 容器 vs 虚拟机

容器不需要运行一个完整的操作系统内核,它们共享宿主机的内核,只隔离用户空间的资源。

虚拟机:                          容器:
+-----+ +-----+ +-----+         +-----+ +-----+ +-----+
|App A| |App B| |App C|         |App A| |App B| |App C|
+-----+ +-----+ +-----+         +-----+ +-----+ +-----+
|Bins | |Bins | |Bins |         |Bins | |Bins | |Bins |
|Libs | |Libs | |Libs |         |Libs | |Libs | |Libs |
+-----+ +-----+ +-----+         +-----+-+-----+-+-----+
|Guest| |Guest| |Guest|               容器引擎
| OS  | | OS  | | OS  |         (Docker/containerd)
+-----+ +-----+ +-----+         +---------------------+
+-----------------------+         |   宿主操作系统       |
|     Hypervisor        |         +---------------------+
+-----------------------+         |   硬件               |
|   宿主操作系统/硬件    |         +---------------------+
+-----------------------+

虚拟机: 每个VM一个完整OS           容器: 共享宿主OS内核
       (GB级, 分钟级启动)                 (MB级, 秒级启动)
维度虚拟机容器
隔离级别硬件级(最强)进程级(较弱)
启动速度分钟级秒级甚至毫秒级
资源开销大(完整OS)小(只有应用和依赖)
镜像大小GB 级MB 级
安全性较低(共享内核)
密度一台宿主机十几个VM一台宿主机数百个容器

2.2 容器的两大基石

容器并不是一种新的虚拟化技术,它只是 Linux 内核已有功能的组合应用。容器的核心依赖两个内核特性:

容器 = Namespace (隔离) + Cgroups (限制)

Namespace: "让你看到的世界不同"
  - 每个容器有自己的 PID 空间
  - 每个容器有自己的网络栈
  - 每个容器有自己的文件系统视图
  - ...

Cgroups: "限制你能用多少资源"
  - 限制 CPU 使用量
  - 限制内存使用量
  - 限制磁盘 I/O 带宽
  - 限制网络带宽
  - ...

三、Namespace:资源隔离

3.1 八种 Namespace

Linux 提供了以下 Namespace 来隔离不同维度的资源:

+------------------+------+------------------------------------------+
| Namespace        | 标志 | 隔离的资源                                |
+------------------+------+------------------------------------------+
| Mount (mnt)      | CLONE_NEWNS   | 文件系统挂载点                   |
| PID              | CLONE_NEWPID  | 进程 ID                         |
| Network (net)    | CLONE_NEWNET  | 网络设备、IP、端口、路由表         |
| UTS              | CLONE_NEWUTS  | 主机名和域名                     |
| IPC              | CLONE_NEWIPC  | 信号量、消息队列、共享内存         |
| User             | CLONE_NEWUSER | 用户 ID 和组 ID                 |
| Cgroup           | CLONE_NEWCGROUP| Cgroup 根目录                  |
| Time             | CLONE_NEWTIME | 系统时钟 (Linux 5.6+)           |
+------------------+------+------------------------------------------+

3.2 PID Namespace 详解

宿主机视角:                    容器内视角:

PID=1 (systemd)               PID=1 (容器init进程)
  |                              |
  +-- PID=1000 (dockerd)         +-- PID=2 (nginx master)
  |     |                        |     |
  |     +-- PID=2000 (容器进程)  |     +-- PID=3 (nginx worker)
  |     |     |                  |     +-- PID=4 (nginx worker)
  |     |     +-- PID=2001       |
  |     |     +-- PID=2002       容器里只能看到自己的进程
  |     |     +-- PID=2003       PID 从 1 开始编号
  |     |
  |     +-- PID=3000 (另一个容器)

在容器内部看到的 PID=1 进程,在宿主机上可能是 PID=2000。这种映射由 PID Namespace 维护。

3.3 Network Namespace 详解

容器网络隔离:

宿主机                         容器1              容器2
+--------------------------+
| eth0 (192.168.1.100)     |
| docker0 (172.17.0.1)     |
|     |                    |
|     +-- veth-pair -------+-- eth0              |
|     |   (虚拟网线)       | (172.17.0.2)        |
|     |                    +---------------------+
|     |
|     +-- veth-pair -------+---------------------+
|         (虚拟网线)       | eth0                 |
|                          | (172.17.0.3)         |
+--------------------------+---------------------+

每个容器有独立的:
  - 网络接口 (eth0)
  - IP 地址
  - 路由表
  - iptables 规则
  - 端口空间 (每个容器都能监听 80 端口)

3.4 Mount Namespace 与 Union FS

容器看到的文件系统是通过 Mount Namespace + Union 文件系统(OverlayFS)实现的:

OverlayFS 分层结构:

  容器可写层 (upperdir)        <-- 容器运行时修改的文件
  +-----------------------------+
  | modified_config.yaml        |
  | new_log_file.log            |
  +-----------------------------+
              |
              v  合并视图 (merged)
  +-----------------------------+
  | /bin /sbin /lib /etc ...    |  <-- 容器内看到的完整文件系统
  | + modified_config.yaml      |
  | + new_log_file.log          |
  +-----------------------------+
              ^
              |
  镜像只读层 (lowerdir)         <-- Docker 镜像的各层
  +-----------------------------+
  |  Layer 3: app code          |  FROM node:18
  +-----------------------------+  COPY . /app
  |  Layer 2: npm install       |  RUN npm install
  +-----------------------------+
  |  Layer 1: node:18 base      |  基础镜像
  +-----------------------------+

  关键特性:
  - 镜像层只读,多个容器共享
  - 修改文件时自动复制到可写层 (COW)
  - 删除文件在可写层创建 "whiteout" 标记

🤔 想一想 如果容器内的 PID=1 进程退出了,整个容器会怎样?这和宿主机上 PID=1 的 init 进程退出有什么相似之处?


四、Cgroups:资源限制

4.1 Cgroups 是什么

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

Cgroups 有两个版本:v1 为每种资源类型(cpu、memory、blkio 等)维护独立的层级树,配置分散;v2 采用统一的单层级结构,所有资源控制器挂载在同一棵树上,接口更简洁。当前主流发行版(Fedora 31+、Ubuntu 22.04+、RHEL 9+)已默认使用 Cgroups v2。

Cgroups v1 层级结构(每种资源一棵独立的树):

/sys/fs/cgroup/
├── cpu/
│   ├── docker/
│   │   ├── container-abc123/
│   │   │   ├── cpu.cfs_quota_us    # CPU 配额
│   │   │   ├── cpu.cfs_period_us   # CPU 周期
│   │   │   └── tasks               # 该组的进程列表
│   │   └── container-def456/
│   │       └── ...
│   └── ...
├── memory/
│   ├── docker/
│   │   ├── container-abc123/
│   │   │   ├── memory.limit_in_bytes  # 内存上限
│   │   │   ├── memory.usage_in_bytes  # 当前使用
│   │   │   └── memory.oom_control     # OOM 控制
│   │   └── ...
│   └── ...
├── blkio/                              # 块设备I/O限制
├── pids/                               # 进程数限制
└── ...

Cgroups v2 统一层级(所有资源在同一棵树上):

/sys/fs/cgroup/
├── cgroup.controllers       # 可用的控制器列表
├── cgroup.subtree_control   # 已启用的控制器
├── docker/
│   ├── container-abc123/
│   │   ├── cpu.max          # CPU 限制 (quota period)
│   │   ├── memory.max       # 内存上限
│   │   ├── io.max           # I/O 限制
│   │   └── cgroup.procs     # 进程列表
│   └── ...
└── ...

4.2 CPU 限制

# 限制容器使用最多 2 个 CPU 核心
# 在 Cgroups v1 中:
echo 200000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us
# 200000/100000 = 2 核

# Docker 方式:
docker run --cpus=2 nginx          # 限制使用 2 核
docker run --cpu-shares=512 nginx  # 相对权重 (默认1024)

# Cgroups v2 (更统一的接口):
echo "200000 100000" > /sys/fs/cgroup/mygroup/cpu.max

4.3 内存限制

# 限制容器使用最多 512MB 内存
echo 536870912 > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes

# Docker 方式:
docker run --memory=512m nginx

# 内存超限时:
# 1. 内核先尝试回收缓存
# 2. 回收不够则触发 OOM Killer 杀掉容器中的进程
# 3. 可以通过 memory.oom_control 禁用 OOM (进程会被挂起)

# 查看容器内存使用情况:
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.stat

4.4 I/O 限制

# 限制容器的磁盘读写速率
# Docker 方式:
docker run --device-read-bps /dev/sda:10mb nginx    # 读限速10MB/s
docker run --device-write-bps /dev/sda:10mb nginx   # 写限速10MB/s
docker run --device-read-iops /dev/sda:1000 nginx   # 读限制1000 IOPS

五、Docker 底层机制

5.1 Docker 架构

Docker 架构:

  docker CLI                   Docker Desktop (GUI)
       |                            |
       v                            v
  Docker Daemon (dockerd)
       |
       v
  containerd (容器运行时管理)
       |
       v
  runc (OCI标准容器运行时)
       |
       v
  +---Namespace---+---Cgroups---+---OverlayFS---+
  | PID隔离       | CPU限制     | 分层文件系统    |
  | Network隔离   | 内存限制    | 写时复制       |
  | Mount隔离     | I/O限制     |               |
  +--------------+-------------+---------------+
                    |
                    v
              Linux Kernel

5.2 容器的生命周期

docker run 的幕后:

1. docker CLI 发送请求到 dockerd
2. dockerd 请求 containerd 创建容器
3. containerd 调用 runc 设置 namespace + cgroups
4. runc 执行以下步骤:
   a. clone() 创建新进程 (带 CLONE_NEWPID | CLONE_NEWNS | ...)
   b. 在新 namespace 中:
      - 挂载 OverlayFS (Union Mount)
      - 配置网络 (veth pair + bridge)
      - 设置 hostname
      - 挂载 /proc, /sys
   c. 将进程加入 cgroup
      - 写入 CPU 限制
      - 写入内存限制
   d. execve() 执行容器入口命令 (如 nginx)
5. runc 退出,containerd 接管容器生命周期管理

5.3 容器网络模式

Docker 四种网络模式:

1. bridge (默认):
   容器通过 docker0 网桥互联,NAT 访问外网
   docker run nginx

2. host:
   容器直接使用宿主机网络栈,无隔离
   docker run --network=host nginx

3. none:
   无网络,完全隔离
   docker run --network=none nginx

4. container:
   共享另一个容器的网络栈
   docker run --network=container:web1 sidecar

bridge 模式详细:
  宿主机:
  +-----------+
  | eth0      | (对外)
  | 10.0.0.5  |
  +-----+-----+
        |
  +-----+-----+
  | docker0   | (网桥)
  | 172.17.0.1|
  +--+-----+--+
     |     |
  +--+--+ +--+--+
  |veth0| |veth1|
  +--+--+ +--+--+
     |     |
  容器1   容器2
  172.    172.
  17.0.2  17.0.3

六、容器安全

6.1 容器的安全边界

容器共享宿主机内核,因此容器的安全边界比虚拟机弱得多:

安全层次:

   最强  +----------------------------------+
    ^    | 虚拟机 (硬件级隔离)              |
    |    +----------------------------------+
    |    | 容器 + 安全增强                  |
    |    | (seccomp + AppArmor + rootless)  |
    |    +----------------------------------+
    |    | 普通容器 (Namespace + Cgroups)   |
    |    +----------------------------------+
   最弱  | 进程 (无隔离)                    |
         +----------------------------------+

6.2 增强容器安全的手段

1. seccomp: 限制容器可用的系统调用
   Docker 默认禁止了约 44 个危险系统调用
   (如 reboot, mount, ptrace 等)

2. AppArmor / SELinux: 强制访问控制
   限制容器可访问的文件和操作

3. Rootless Container: 以非root用户运行容器引擎
   即使容器内提权成功也只是普通用户

4. Read-only Root FS: 只读根文件系统
   docker run --read-only nginx

5. Capabilities: 细粒度的权限控制
   docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

🤔 想一想 为什么说”容器逃逸”是容器安全最严重的威胁?一旦成功逃逸,攻击者能做什么?


七、本章总结

+----------------------------------------------------+
|              容器与虚拟化核心知识                      |
+----------------------------------------------------+
|                                                    |
|  虚拟化:                                            |
|    全虚拟化 / 半虚拟化 / 硬件辅助                     |
|    KVM = Linux 内核模块 + QEMU 设备模拟              |
|                                                    |
|  容器 = Namespace + Cgroups + Union FS              |
|                                                    |
|  Namespace (8种隔离):                               |
|    PID | Network | Mount | UTS | IPC | User        |
|    Cgroup | Time                                   |
|                                                    |
|  Cgroups (资源限制):                                 |
|    CPU (quota/shares) | Memory (limit)             |
|    I/O (bps/iops) | PIDs (进程数)                   |
|                                                    |
|  Docker:                                            |
|    dockerd -> containerd -> runc -> kernel          |
|    镜像: OverlayFS 分层 + COW                       |
|    网络: bridge / host / none / container           |
|                                                    |
+----------------------------------------------------+

📝 结尾自测:检验你的收获

  1. 虚拟机和容器的核心区别是什么?画出两者的架构对比图。
  2. Linux Namespace 提供了哪些隔离维度?PID Namespace 如何实现进程 ID 的隔离?
  3. Cgroups 如何限制容器的 CPU 和内存使用?超出内存限制会发生什么?
  4. Docker 镜像的分层存储如何工作?OverlayFS 的上层和下层分别存储什么?
  5. 容器共享宿主机内核带来了什么安全风险?有哪些缓解措施?

购买课程解锁全部内容

系统底层入门:10 章掌握操作系统核心

¥29.90