Add more content

This commit is contained in:
Baohua Yang
2026-01-30 16:48:39 -08:00
parent c58f61dbed
commit fec2e506d9
39 changed files with 8202 additions and 1063 deletions

View File

@@ -1,51 +1,137 @@
# 基本架构
Docker 采用了 `C/S`客户端/服务端架构包括客户端和服务端Docker 守护进程`Daemon`作为服务端接受来自客户端的请求并处理这些请求创建运行分发容器
## 核心架构图
客户端和服务端既可以运行在一个机器上也可通过 `socket` 或者 `RESTful API` 来进行通信
![Docker 基本架构](../.gitbook/assets/docker_arch.png)
## 核心组件
Docker 的核心组件形成了一个层次化的架构
Docker 采用了 **C/S (客户端/服务端)** 架构Client Daemon 发送请求Daemon 负责构建运行和分发容器
```
┌─────────────────────────────────────────────────┐
Docker CLI
(docker 命令行工具)
├─────────────────────────────────────────────────┤
dockerd
(Docker 守护进程/引擎)
─────────────────────────────────────────────────┤
│ containerd
(容器生命周期管理器)
├─────────────────────────────────────────────────┤
runc
(OCI 容器运行时)
└─────────────────────────────────────────────────┘
┌───────────────┐ ┌────────────────────────────────────┐
客户端 Docker Host
(Docker CLI) │
│ │ │ ┌──────────┐ ┌────────────┐ │
$ docker run ├────►│ dockerd │ Containers │
$ docker pull│ (Daemon) │─────►│
│ $ docker ps │ │ └─────────┘ │ □ □ │ │
└───────────────┘ │ └────────────┘
│ ┌────────────┐
│ └───────────►│ Images │ │
└────────────────────┴────────────┘
```
* **Docker CLI**用户与 Docker 交互的命令行工具
* **dockerd**Docker 守护进程提供 Docker API管理镜像网络存储等
* **containerd**高级容器运行时管理容器的完整生命周期
* **runc**低级容器运行时根据 OCI 规范创建和运行容器
---
## 组件详解
Docker 的内部架构如同洋葱一样分层每一层专注解决特定问题
### 1. Docker CLI (客户端)
用户与 Docker 交互的主要方式它将用户命令 `docker run`转换为 API 请求发送给 dockerd
### 2. Dockerd (守护进程)
Docker 的大脑
- 监听 API 请求
- 管理 Docker 对象镜像容器网络
- 编排下层组件完成工作
### 3. Containerd (高级运行时)
行业标准的容器运行时CNCF 毕业项目
- 管理容器的完整生命周期启动停止
- 镜像拉取与存储
- **不包含** 复杂的与容器无关的功能如构建API
- Kubernetes 也可以直接使用 containerd跳过 Docker
### 4. Runc (低级运行时)
用于创建和运行容器的 CLI 工具
- 直接与内核交互Namespaces, Cgroups
- 遵循 OCI (Open Container Initiative) 规范
- **主要职责**根据配置启动一个容器然后退出将控制权交给容器进程
### 5. Shim
每个容器都有一个 shim 进程
- **解耦**允许 dockerd 重启而不影响容器运行
- **保持 IO**维持容器的标准输入输出
- **状态汇报** containerd 汇报容器退出状态
---
## 容器启动流程
当执行 `docker run -d nginx` 内部发生了什么
```
User
Docker CLI ──(REST API)──> Dockerd
Containerd ──(gRPC)──> Containerd-shim
Runc
Kernel (创建容器)
(Start & Exit)
```
1. **CLI** 发送请求给 **Dockerd**
2. **Dockerd** 解析请求调用 **Containerd**
3. **Containerd** 准备镜像转换为 OCI Bundle
4. **Containerd** 创建 **Shim** 进程
5. **Shim** 调用 **Runc**
6. **Runc** 与系统内核交互创建 Namespaces Cgroups
7. **Runc** 启动 nginx 进程后退出
8. **Shim** 接管容器 IO 和生命周期监控
---
## Docker Engine v29+ 变化
Docker Engine v29 (2025/2026) 开始架构进一步简化和标准化
- **Containerd 镜像存储 (Image Store)**默认启用Docker 直接使用 Containerd 的镜像管理能力不再维护自己的一套 graphdriver
- **优势**多平台镜像支持更好镜像拉取更快lazy pulling K8s 共享镜像
---
## Docker Desktop 架构
macOS Windows Docker Desktop 使用轻量级虚拟机运行 Linux 内核
macOS Windows 因为内核差异架构稍微复杂
* **macOS**使用 Apple Hypervisor Framework QEMU
* **Windows**使用 WSL 2推荐 Hyper-V
```
┌────────────── MacOS / Windows ──────────────┐
│ Docker CLI │
│ │ │
├──────┼──────────────────────────────────────┤
│ ▼ (Socket 映射) │
│ ┌────────── Linux VM (虚拟机) ───────────┐ │
│ │ │ │
│ │ Dockerd <--> Containerd <--> Runc │ │
│ │ │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
这意味着容器实际运行在虚拟机内的 Linux 环境中而非直接运行在宿主系统上
- 使用轻量级虚拟机Apple Virtualization / WSL 2运行 Linux 内核
- 文件挂载Bind Mount需要跨越 VM 边界这也是文件 I/O 慢的原因
- 网络端口需要从宿主机转发到 VM
## Docker Engine v29 重要变化
---
Docker Engine v29 **Containerd 镜像存储**成为新安装的默认配置这一变化
## 总结
* 简化了 Docker 的内部架构
* 提升了与 Kubernetes containerd 平台的互操作性
* Lazy Pulling 等新特性奠定基础
| 组件 | 角色 | 关键职责 |
|------|------|----------|
| **CLI** | 指挥官 | 发送指令展示结果 |
| **Dockerd** | 大管家 | API 接口整体调度 |
| **Containerd** | 经理 | 容器生命周期镜像管理 |
| **Shim** | 监工 | 保持 IO允许无守护进程重启 |
| **Runc** | 工人 | 真正干活创建容器干完就走 |
Docker 守护进程一般在宿主主机后台运行等待接收来自客户端的消息Docker 客户端则为用户提供一系列可执行命令用户用这些命令实现跟 Docker 守护进程交互
## 延伸阅读
- [命名空间](./namespace.md)Runc 如何隔离容器
- [控制组](./cgroups.md)Runc 如何限制资源
- [联合文件系统](./ufs.md)镜像如何存储

View File

@@ -1,7 +1,246 @@
# 控制组
控制组[cgroups](https://en.wikipedia.org/wiki/Cgroups))是 Linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。
## 什么是控制组
控制组技术最早是由 Google 的程序员在 2006 年提出Linux 内核 2.6.24 开始支持
控制组Control Groups简称 cgroups Linux 内核的一个特性用于**限制记录和隔离**进程组的资源使用CPU内存磁盘 I/O网络等
控制组可以提供对容器的内存CPU磁盘 IO 等资源的限制和审计管理
> **核心作用**让多个容器公平共享宿主机资源防止单个容器耗尽系统资源
```
无 cgroups 限制: 有 cgroups 限制:
┌──────────────────────┐ ┌──────────────────────┐
│ 宿主机资源 │ │ 宿主机资源 │
│ ┌─────────────┐ │ │ ┌───┬───┬───┐ │
│ │ 容器 A │ │ │ │ A │ B │ C │ │
│ │ 占用所有 │ │ │ │1GB│1GB│1GB│ ← 限制│
│ │ 内存和 CPU │ │ │ ├───┼───┼───┤ │
│ └─────────────┘ │ │ │2核│1核│1核│ │
│ 容器 B、C 饥饿 │ │ └───┴───┴───┘ │
└──────────────────────┘ └──────────────────────┘
```
---
## cgroups 的历史
| 时间 | 事件 |
|------|------|
| 2006 | Google 工程师提出 cgroups 概念 |
| 2008 | Linux 2.6.24 正式支持 cgroups v1 |
| 2016 | Linux 4.5 引入 cgroups v2 |
| 现在 | Docker 默认使用 cgroups v2如系统支持 |
---
## cgroups 可以限制的资源
| 资源类型 | 子系统 | 说明 |
|---------|--------|------|
| **CPU** | `cpu`, `cpuset` | CPU 使用时间和核心分配 |
| **内存** | `memory` | 内存使用上限和 swap |
| **块设备 I/O** | `blkio` | 磁盘读写速度限制 |
| **网络** | `net_cls`, `net_prio` | 网络带宽优先级 |
| **进程数** | `pids` | 限制进程/线程数量 |
---
## Docker 中的资源限制
### 内存限制
```bash
# 限制容器最多使用 512MB 内存
$ docker run -m 512m myapp
# 限制内存 + swap
$ docker run -m 512m --memory-swap 1g myapp
# 软限制(超过时警告,不会 OOM Kill
$ docker run --memory-reservation 256m myapp
```
| 参数 | 说明 |
|------|------|
| `-m` / `--memory` | 硬限制超过会 OOM Kill |
| `--memory-swap` | 内存 + swap 总限制 |
| `--memory-reservation` | 软限制内存竞争时生效 |
| `--oom-kill-disable` | 禁用 OOM Killer谨慎使用 |
### CPU 限制
```bash
# 限制使用 1.5 个 CPU 核心
$ docker run --cpus=1.5 myapp
# 限制使用 CPU 0 和 1
$ docker run --cpuset-cpus="0,1" myapp
# 设置 CPU 使用权重(相对值,默认 1024
$ docker run --cpu-shares=512 myapp
```
| 参数 | 说明 |
|------|------|
| `--cpus` | 限制 CPU 核心数 1.5 |
| `--cpuset-cpus` | 绑定到特定 CPU 核心 |
| `--cpu-shares` | CPU 时间片权重相对值 |
| `--cpu-period` / `--cpu-quota` | 精细控制 CPU 配额 |
### 磁盘 I/O 限制
```bash
# 限制设备写入速度为 10MB/s
$ docker run --device-write-bps /dev/sda:10mb myapp
# 限制设备读取速度
$ docker run --device-read-bps /dev/sda:10mb myapp
# 限制 IOPS
$ docker run --device-write-iops /dev/sda:100 myapp
```
### 进程数限制
```bash
# 限制最多 100 个进程
$ docker run --pids-limit=100 myapp
```
---
## 查看容器资源使用
```bash
# 实时监控所有容器的资源使用
$ docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
abc123 web 0.50% 45.5MiB / 512MiB 8.89% 1.2kB / 0B 0B / 0B
def456 db 2.30% 256MiB / 1GiB 25.00% 5.6kB / 3.2kB 4.1MB / 2.3MB
# 查看特定容器
$ docker stats mycontainer
# 查看容器的 cgroup 配置
$ docker inspect mycontainer --format '{{json .HostConfig}}' | jq
```
---
## 资源限制的效果
### 内存超限
```bash
# 启动限制 100MB 内存的容器
$ docker run -m 100m stress --vm 1 --vm-bytes 200M
# 容器会被 OOM Killer 杀死
$ docker ps -a
CONTAINER ID STATUS NAMES
abc123 Exited (137) 5 seconds ago hopeful_darwin
# 137 = 128 + 9表示被 SIGKILL (9) 杀死
```
### CPU 限制验证
```bash
# 不限制 CPU
$ docker run --rm stress --cpu 4
# 占满所有 CPU
# 限制为 1 个核心
$ docker run --rm --cpus=1 stress --cpu 4
# 只能使用约 100% CPU1 个核心)
```
---
## cgroups v1 vs v2
| 特性 | cgroups v1 | cgroups v2 |
|------|-----------|-----------|
| 层级结构 | 多层级每个资源单独 | 统一层级 |
| 管理复杂度 | 复杂 | 简化 |
| 资源分配 | 基于层级 | 基于子树 |
| PSI压力监控 | | |
| rootless 容器 | 部分支持 | 完整支持 |
### 检查系统使用的版本
```bash
# 查看 cgroup 版本
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# 如果显示 cgroup2 表示 v2
# 或者
$ cat /proc/filesystems | grep cgroup
nodev cgroup
nodev cgroup2
```
---
## Compose 中设置限制
```yaml
services:
web:
image: nginx
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
```
---
## 最佳实践
### 1. 始终设置内存限制
```bash
# 防止 OOM 影响宿主机
$ docker run -m 1g myapp
```
### 2. 为关键应用设置 CPU 保证
```bash
$ docker run --cpus=2 --cpu-shares=2048 critical-app
```
### 3. 监控资源使用
```bash
# 配合 Prometheus + cAdvisor 监控
$ docker run -d --name cadvisor \
-v /:/rootfs:ro \
-v /var/run:/var/run:ro \
-v /sys:/sys:ro \
-v /var/lib/docker:/var/lib/docker:ro \
gcr.io/cadvisor/cadvisor
```
---
## 本章小结
| 资源 | 限制参数 | 示例 |
|------|---------|------|
| **内存** | `-m` | `-m 512m` |
| **CPU 核心数** | `--cpus` | `--cpus=1.5` |
| **CPU 绑定** | `--cpuset-cpus` | `--cpuset-cpus="0,1"` |
| **磁盘 I/O** | `--device-write-bps` | `--device-write-bps /dev/sda:10mb` |
| **进程数** | `--pids-limit` | `--pids-limit=100` |
## 延伸阅读
- [命名空间](namespace.md)资源隔离
- [安全](../security/README.md)容器安全概述
- [Docker Stats](../container/README.md)监控容器资源

View File

@@ -1,23 +1,267 @@
# 命名空间
命名空间是 Linux 内核一个强大的特性每个容器都有自己单独的命名空间运行在其中的应用都像是在独立的操作系统中运行一样命名空间保证了容器之间彼此互不影响
## 什么是 Namespace
## pid 命名空间
不同用户的进程就是通过 pid 命名空间隔离开的且不同命名空间中可以有相同 pid所有的 LXC 进程在 Docker 中的父进程为 Docker 进程每个 LXC 进程具有不同的命名空间同时由于允许嵌套因此可以很方便的实现嵌套的 Docker 容器
> **Namespace Linux 内核提供的资源隔离机制它让容器内的进程仿佛运行在独立的操作系统中**
## net 命名空间
有了 pid 命名空间每个命名空间中的 pid 能够相互隔离但是网络端口还是共享 host 的端口网络隔离是通过 net 命名空间实现的 每个 net 命名空间有独立的 网络设备IP 地址路由表/proc/net 目录这样每个容器的网络就能隔离开来Docker 默认采用 veth 的方式将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起
Namespace 是容器技术的核心基础之一它回答了一个关键问题**如何让一个进程"以为"自己独占整个系统**
## ipc 命名空间
容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC) 包括信号量消息队列和共享内存等然而同 VM 不同的是容器的进程间交互实际上还是 host 上具有相同 pid 命名空间中的进程间交互因此需要在 IPC 资源申请时加入命名空间信息每个 IPC 资源有一个唯一的 32 id
```
宿主机视角: 容器内视角:
┌─────────────────────────┐ ┌─────────────────────────┐
│ PID 1: systemd │ │ PID 1: nginx │ ← 容器认为自己是 PID 1
│ PID 2: sshd │ │ PID 2: nginx worker │
│ PID 3: dockerd │ │ │
│ PID 1234: nginx ←──────│─────│ (实际是宿主机的 1234
│ PID 1235: nginx worker │ │ │
└─────────────────────────┘ └─────────────────────────┘
```
## mnt 命名空间
类似 chroot将一个进程放到一个特定的目录执行mnt 命名空间允许不同命名空间的进程看到的文件结构不同这样每个命名空间 中的进程所看到的文件目录就被隔离开了 chroot 不同每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point
## Namespace 的类型
## uts 命名空间
UTS("UNIX Time-sharing System") 命名空间允许每个容器拥有独立的 hostname domain name 使其在网络上可以被视作一个独立的节点而非 主机上的一个进程
Linux 内核提供了以下几种 NamespaceDocker 容器使用了全部
## user 命名空间
每个容器可以有不同的用户和组 id 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户
| Namespace | 隔离内容 | 容器中的效果 |
|-----------|---------|-------------|
| **PID** | 进程 ID | 容器内 PID 1 开始看不到其他容器和宿主机进程 |
| **NET** | 网络栈 | 独立的网卡IP 地址端口路由表 |
| **MNT** | 挂载点 | 独立的文件系统视图自己的根目录 |
| **UTS** | 主机名 | 独立的主机名和域名 |
| **IPC** | 进程间通信 | 独立的信号量消息队列共享内存 |
| **USER** | 用户/ ID | 容器内的 root 可以映射为宿主机的普通用户 |
| **Cgroup** | Cgroup 根目录 | 隔离 cgroup 层级视图Linux 4.6+ |
*更多关于 Linux 上命名空间的信息请阅读 [这篇文章](https://blog.scottlowe.org/2013/09/04/introducing-linux-network-namespaces/)。
---
## PID Namespace
### 作用
隔离进程 ID让每个容器有自己的进程编号空间
### 效果
```bash
# 宿主机上查看进程
$ ps aux | grep nginx
root 12345 0.0 0.1 nginx: master process
root 12346 0.0 0.1 nginx: worker process
# 容器内查看进程
$ docker exec mycontainer ps aux
PID USER COMMAND
1 root nginx: master process ← 在容器内是 PID 1
2 root nginx: worker process
```
### 关键点
- 容器内的 PID 1 进程特殊重要它是容器的主进程退出则容器停止
- 容器内无法看到宿主机或其他容器的进程
- 宿主机可以看到所有容器内的进程 PID 不同
---
## NET Namespace
### 作用
隔离网络栈每个容器拥有独立的网络环境
### 效果
```
宿主机 容器
┌─────────────────────┐ ┌─────────────────────┐
│ eth0: 192.168.1.10 │ │ eth0: 172.17.0.2 │ ← 不同的 IP
│ docker0: 172.17.0.1│◄───────►│ (veth pair 连接) │
│ 端口 80 可用 │ │ 端口 80 可用 │ ← 可以使用相同端口
└─────────────────────┘ └─────────────────────┘
```
### 关键点
- 每个容器有独立的网卡IP路由表iptables 规则
- 多个容器可以监听相同端口如都监听 80
- Docker 使用 veth pair 连接容器网络和宿主机网桥
---
## MNT Namespace
### 作用
隔离文件系统挂载点每个容器有自己的根目录
### 效果
```
宿主机文件系统: 容器内看到的:
/ / ← 容器的根目录
├── bin/ ├── bin/
├── home/ ├── home/
├── var/ ├── var/
│ └── lib/ │ └── lib/
│ └── docker/ │
│ └── overlay2/ │
│ └── merged/ ────┼─── 这个目录成为容器的 /
└── ... └── ...
```
### chroot 的区别
| 特性 | chroot | MNT Namespace |
|------|--------|---------------|
| 安全性 | 可以逃逸 | 更安全 |
| 挂载隔离 | | 完全隔离 |
| /proc/mounts | 共享 | 独立 |
---
## UTS Namespace
### 作用
隔离主机名和域名让每个容器可以有自己的主机名
### 效果
```bash
# 宿主机
$ hostname
my-server
# 容器内
$ docker run --hostname mycontainer ubuntu hostname
mycontainer
```
UTS = "UNIX Time-sharing System"是历史遗留的名称
---
## IPC Namespace
### 作用
隔离 System V IPC POSIX 消息队列
### 隔离的资源
- 信号量semaphores
- 消息队列message queues
- 共享内存shared memory
### 关键点
- 同一容器内的进程可以通过 IPC 通信
- 不同容器的进程无法通过 IPC 通信除非显式共享
---
## USER Namespace
### 作用
隔离用户和组 ID实现权限隔离
### 效果
```
容器内 宿主机
┌─────────────────┐ ┌─────────────────┐
│ UID 0 (root) │───映射────►│ UID 100000 │ ← 非特权用户
│ UID 1 (daemon) │───映射────►│ UID 100001 │
└─────────────────┘ └─────────────────┘
```
### 安全意义
容器内的 root 用户可以映射为宿主机上的普通用户即使容器被突破攻击者在宿主机上也只有普通权限
> 💡 笔者建议生产环境建议启用 User Namespace增强安全性
---
## 动手实验体验 Namespace
使用 `unshare` 命令可以在不使用 Docker 的情况下体验 Namespace
### 实验 1UTS Namespace
```bash
# 创建新的 UTS namespace 并启动 shell
$ sudo unshare --uts /bin/bash
# 修改主机名(只影响这个 namespace
$ hostname container-test
$ hostname
container-test
# 退出后查看宿主机主机名(未改变)
$ exit
$ hostname
my-server
```
### 实验 2PID Namespace
```bash
# 创建新的 PID 和 MNT namespace
$ sudo unshare --pid --mount --fork /bin/bash
# 挂载新的 /proc
$ mount -t proc proc /proc
# 查看进程(只能看到当前 shell
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 8960 4516 pts/0 S 10:00 0:00 /bin/bash
root 8 0.0 0.0 10072 3200 pts/0 R+ 10:00 0:00 ps aux
```
### 实验 3NET Namespace
```bash
# 创建新的网络 namespace
$ sudo unshare --net /bin/bash
# 查看网络接口(只有 lo
$ ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
```
---
## Namespace 的局限性
Namespace 提供了隔离但不是安全边界
| 方面 | 说明 |
|------|------|
| **共享内核** | 所有容器共享宿主机内核内核漏洞可能影响所有容器 |
| **部分资源未隔离** | /proc/sys 部分内容仍可见时间无法隔离 |
| **非虚拟化** | 比虚拟机隔离性弱 |
> 需要更强隔离时可考虑 gVisorKata Containers 等安全容器方案
---
## 本章小结
| Namespace | 隔离内容 | 一句话说明 |
|-----------|---------|-----------|
| PID | 进程 ID | 容器有自己的进程树 |
| NET | 网络 | 容器有自己的 IP 和端口 |
| MNT | 文件系统 | 容器有自己的根目录 |
| UTS | 主机名 | 容器有自己的 hostname |
| IPC | 进程间通信 | 容器间 IPC 隔离 |
| USER | 用户 ID | 容器 root 宿主机 root |
## 延伸阅读
- [控制组Cgroups](cgroups.md)资源限制机制
- [联合文件系统](ufs.md)分层存储的实现
- [安全](../security/README.md)容器安全实践
- [Linux Namespace 官方文档](https://man7.org/linux/man-pages/man7/namespaces.7.html)

View File

@@ -1,22 +1,230 @@
# 联合文件系统
联合文件系统[UnionFS](https://en.wikipedia.org/wiki/UnionFS))是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。
## 什么是联合文件系统
联合文件系统 Docker 镜像的基础镜像可以通过分层来进行继承基于基础镜像没有父镜像可以制作各种具体的应用镜像
联合文件系统UnionFS是一种**分层轻量级**的文件系统它将多个目录"联合"挂载到同一个虚拟目录形成一个统一的文件系统视图
另外不同 Docker 容器就可以共享一些基础的文件系统层同时再加上自己独有的改动层大大提高了存储的效率
> **核心思想**将多个只读层叠加最上层可写形成完整的文件系统
Docker 中使用的 AUFSAdvanced Multi-Layered Unification Filesystem就是一种联合文件系统 `AUFS` 支持为每一个成员目录类似 Git 的分支设定只读readonly读写readwrite和写出whiteout-able权限, 同时 `AUFS` 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)
```
┌─────────────────────────────────────────────────────────┐
│ 容器看到的文件系统 │
│ /bin /etc /lib /usr /var /app /data │
└────────────────────────┬────────────────────────────────┘
┌───────────────┴───────────────┐
│ UnionFS 联合挂载 │
└───────────────┬───────────────┘
┌────────────────────────┴────────────────────────────────┐
│ 容器层 (读写) │ /app/data/log.txt (新写入) │
├────────────────────┼────────────────────────────────────│
│ 镜像层3 (只读) │ /app/app.py │
├────────────────────┼────────────────────────────────────│
│ 镜像层2 (只读) │ /usr/local/bin/python │
├────────────────────┼────────────────────────────────────│
│ 镜像层1 (只读) │ /bin /etc /lib (基础系统) │
└────────────────────┴────────────────────────────────────┘
```
Docker 目前支持的联合文件系统包括 `OverlayFS`, `AUFS`, `Btrfs`, `VFS`, `ZFS` `Device Mapper`
---
Linux 发行版 Docker 推荐使用的存储驱动如下表
## 为什么 Docker 使用联合文件系统
|Linux 发行版 | Docker 推荐使用的存储驱动 |
| :-- | :-- |
|Docker on Ubuntu | `overlay2` (16.04 +) |
|Docker on Debian | `overlay2` (Debian Stretch), `aufs`, `devicemapper` |
|Docker on CentOS | `overlay2` |
|Docker on Fedora | `overlay2` |
### 1. 镜像分层复用
在可能的情况下[推荐](https://docs.docker.com/storage/storagedriver/select-storage-driver/) 使用 `overlay2` 存储驱动,`overlay2` 是目前 Docker 默认的存储驱动,以前则是 `aufs`。你可以通过配置来使用以上提到的其他类型的存储驱动。
```
nginx:alpine myapp:latest
│ │
└────────┬────────────┘
alpine:3.19 (共享基础层)
```
多个镜像共享相同的底层节省磁盘空间
### 2. 快速构建
每个 Dockerfile 指令创建一层只有变化的层需要重建
```docker
FROM node:20 # 层1基础镜像
COPY package.json ./ # 层2依赖定义
RUN npm install # 层3安装依赖
COPY . . # 层4应用代码
```
代码变化时只需重建层4层1-3 使用缓存
### 3. 容器启动快
容器启动时不需要复制镜像只需
1. 在镜像层上创建一个薄的可写层
2. 联合挂载所有层
---
## Copy-on-Write写时复制
当容器修改只读层中的文件时
```
修改前: 修改后:
┌─────────────────────┐ ┌─────────────────────┐
│ 容器层 (空) │ │ 容器层 │
├─────────────────────┤ │ /etc/nginx.conf ←──┼── 复制到容器层后修改
│ 镜像层 │ ├─────────────────────┤
│ /etc/nginx.conf │ │ 镜像层 │
└─────────────────────┘ │ /etc/nginx.conf │ (原文件仍在,但被遮蔽)
└─────────────────────┘
```
**流程**
1. 从只读层读取文件
2. 复制到容器的可写层
3. 在可写层中修改
4. 后续读取使用可写层的版本
---
## Docker 支持的存储驱动
Docker 可使用多种联合文件系统实现
| 存储驱动 | 说明 | 推荐程度 |
|---------|------|---------|
| **overlay2** | 现代 Linux 默认驱动性能优秀 | **推荐** |
| **aufs** | 早期默认兼容性好 | 遗留系统 |
| **btrfs** | 使用 Btrfs 子卷 | 特定场景 |
| **zfs** | 使用 ZFS 数据集 | 特定场景 |
| **devicemapper** | 块设备级存储 | 遗留系统 |
| **vfs** | 不使用 CoW每层完整复制 | 仅测试 |
### 各发行版推荐
| Linux 发行版 | 推荐存储驱动 |
|-------------|-------------|
| Ubuntu 16.04+ | overlay2 |
| Debian Stretch+ | overlay2 |
| CentOS 7+ | overlay2 |
| RHEL 8+ | overlay2 |
| Fedora | overlay2 |
### 查看当前存储驱动
```bash
$ docker info | grep "Storage Driver"
Storage Driver: overlay2
```
---
## overlay2 工作原理
overlay2 是目前最推荐的存储驱动
```
┌─────────────────────────────────────────────────────────────┐
│ merged合并视图
│ 容器看到的完整文件系统 │
└─────────────────────────┬───────────────────────────────────┘
┌───────────────┴───────────────┐
│ OverlayFS │
└───────────────┬───────────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ upper │ │ lower2 │ │ lower1 │
│ (容器层) │ │ (镜像层) │ │ (基础层) │
│ 读写 │ │ 只读 │ │ 只读 │
└─────────────┘ └─────────────┘ └─────────────┘
```
- **lowerdir**只读的镜像层可以有多个
- **upperdir**可写的容器层
- **workdir**OverlayFS 的工作目录
- **merged**联合挂载后的视图
### 文件操作行为
| 操作 | 行为 |
|------|------|
| **读取** | 从上到下查找第一个匹配的文件 |
| **创建** | upper 层创建 |
| **修改** | 如果在 lower 先复制到 upper 层再修改 |
| **删除** | upper 层创建 whiteout 文件标记删除 |
---
## 查看镜像层
```bash
# 查看镜像的层信息
$ docker history nginx:alpine
IMAGE CREATED CREATED BY SIZE
a6eb2a334a9f 2 weeks ago CMD ["nginx" "-g" "daemon off;"] 0B
<missing> 2 weeks ago STOPSIGNAL SIGQUIT 0B
<missing> 2 weeks ago EXPOSE map[80/tcp:{}] 0B
<missing> 2 weeks ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B
<missing> 2 weeks ago COPY 30-tune-worker-processes.sh /docker-ent… 4.62kB
...
# 查看层的存储位置
$ docker inspect nginx:alpine --format '{{json .GraphDriver.Data}}' | jq
{
"LowerDir": "/var/lib/docker/overlay2/.../diff:/var/lib/docker/overlay2/.../diff",
"MergedDir": "/var/lib/docker/overlay2/.../merged",
"UpperDir": "/var/lib/docker/overlay2/.../diff",
"WorkDir": "/var/lib/docker/overlay2/.../work"
}
```
---
## 最佳实践
### 1. 减少镜像层数
```docker
# ❌ 每条命令创建一层
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
# ✅ 合并为一层
RUN apt-get update && \
apt-get install -y nginx && \
rm -rf /var/lib/apt/lists/*
```
### 2. 避免在容器中写入大量数据
容器层的写入性能低于直接写入大量数据应使用
- 数据卷Volume
- 绑定挂载Bind Mount
### 3. 使用 .dockerignore
排除不需要的文件可以
- 减小构建上下文
- 避免创建不必要的层
---
## 本章小结
| 概念 | 说明 |
|------|------|
| **UnionFS** | 将多层目录联合挂载为一个文件系统 |
| **Copy-on-Write** | 写时复制修改时才复制到可写层 |
| **overlay2** | Docker 默认推荐的存储驱动 |
| **分层好处** | 镜像复用快速构建快速启动 |
## 延伸阅读
- [镜像](../basic_concept/image.md)理解镜像分层
- [容器](../basic_concept/container.md)容器存储层
- [构建镜像](../image/build.md)Dockerfile 层的创建