第02章:Docker 架构原理
本章目标:深入理解 Docker 的 C/S 架构、核心组件及其交互方式,掌握镜像分层的底层实现原理。
2.1 Docker 整体架构
2.1.1 C/S(客户端-服务端)架构
Docker 采用经典的 C/S 架构,分为三个核心部分:
┌──────────────────────────────────────────────────────────┐
│ Docker 架构总览 │
│ │
│ ┌─────────────┐ │
│ │ Docker │ docker build / docker pull │
│ │ Client │ docker run / docker ps │
│ │ (CLI 工具) │ │
│ └──────┬──────┘ │
│ │ REST API (Unix Socket / TCP) │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Docker Daemon (dockerd) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 镜像管理 │ │ 网络管理 │ │ 卷管理 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ containerd │ │ │
│ │ │ ┌────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ containerd │ │ containerd │ │ │ │
│ │ │ │ -shim │ │ -shim │ │ │ │
│ │ │ └──────┬─────┘ └──────┬──────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────┴───┐ ┌─────┴────┐ │ │ │
│ │ │ │容器 A │ │容器 B │ │ │ │
│ │ │ └────────┘ └──────────┘ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
2.1.2 三大核心组件详解
① Docker Client(客户端)
Docker 客户端是你与 Docker 交互的主要方式:
bash
# 你输入的所有 docker 命令都由客户端接收和解析
docker run -d -p 80:80 nginx
docker ps -a
docker images
客户端的职责:
- 解析命令行参数
- 构造 REST API 请求
- 将请求发送给 Docker Daemon
- 格式化并展示返回结果
客户端可以通过以下方式连接 Daemon:
- Unix Socket (默认,仅本机):
unix:///var/run/docker.sock - TCP (远程管理):
tcp://remote-host:2375 - SSH(安全远程):通过 SSH 隧道连接
② Docker Daemon(守护进程 / dockerd)
Docker Daemon 是 Docker 的核心服务,负责:
- 监听 API 请求
- 管理 Docker 对象(镜像、容器、网络、卷)
- 与其他 Daemon 通信(集群模式)
bash
# 查看 dockerd 进程
ps aux | grep dockerd
# 查看 Docker 系统信息
docker info
# 查看 Docker 版本
docker version
③ containerd + runc(容器运行时)
Docker 的容器运行时分为两层:
dockerd
└── containerd(容器运行时管理器)
└── containerd-shim(容器进程的父进程)
└── runc(OCI 标准容器运行时)
└── 容器进程
| 组件 | 职责 |
|---|---|
| containerd | 容器生命周期管理、镜像分发、存储管理 |
| containerd-shim | 作为容器进程的父进程,允许 dockerd 重启而不影响容器 |
| runc | 实际创建和运行容器(调用 Namespace、Cgroups 等内核接口) |
bash
# 查看 containerd 状态
systemctl status containerd
# 查看 runc 版本
runc --version
2.2 Docker 镜像的分层原理
2.2.1 镜像 = 只读层的叠加
Docker 镜像不是一整块文件,而是由多个只读层(Layer) 堆叠而成:
Docker 镜像的层级结构:
┌─────────────────────────────────┐
│ Layer 4: COPY app.py /app/ │ ← 3KB (hash: sha256:abc123...)
├─────────────────────────────────┤
│ Layer 3: RUN pip install ... │ ← 15MB (hash: sha256:def456...)
├─────────────────────────────────┤
│ Layer 2: RUN apt-get install │ ← 80MB (hash: sha256:789ghi...)
├─────────────────────────────────┤
│ Layer 1: FROM ubuntu:22.04 │ ← 77MB (hash: sha256:jkl012...)
└─────────────────────────────────┘
每层只存储与上一层的差异(delta)
2.2.2 分层的好处
传统方式(无分层):
项目A: [OS + Python 3.8 + App A] = 800MB
项目B: [OS + Python 3.8 + App B] = 800MB
项目C: [OS + Python 3.11 + App C] = 850MB
总占用: 2450MB
Docker 分层方式:
基础层: [OS (Ubuntu)] = 77MB ← 三个项目共享
Python层: [Python 3.8] = 120MB ← A和B共享
Python层: [Python 3.11] = 130MB ← C使用
App A层: [App A 代码] = 5MB
App B层: [App B 代码] = 8MB
App C层: [App C 代码] = 6MB
总占用: ~346MB(去重后实际更少)
关键优势:
- 存储效率:相同层只存储一份,多个镜像共享基础层
- 传输效率 :
docker pull时只下载本地不存在的层 - 构建效率 :
docker build时,未修改的层直接使用缓存 - 安全审计:每层都有唯一哈希标识,可追溯变更来源
2.2.3 写时复制(Copy-on-Write)
容器启动时,Docker 在镜像层之上添加一个可写层(Container Layer):
运行中的容器:
┌─────────────────────────────────┐
│ Container Layer (可写) │ ← 修改、新增的文件在这里
│ - 修改了 /etc/config │
│ - 新增了 /tmp/data.log │
├─────────────────────────────────┤
│ Layer 4: COPY app.py /app/ │ ← 只读(镜像层)
├─────────────────────────────────┤
│ Layer 3: RUN pip install ... │ ← 只读(镜像层)
├─────────────────────────────────┤
│ Layer 2: RUN apt-get install │ ← 只读(镜像层)
├─────────────────────────────────┤
│ Layer 1: FROM ubuntu:22.04 │ ← 只读(镜像层)
└─────────────────────────────────┘
读取文件时的查找顺序:
1. 先在 Container Layer 查找
2. 没找到 → 在 Layer 4 查找
3. 没找到 → 在 Layer 3 查找
4. ... 依次向下查找
修改文件时(写时复制):
1. 从镜像层将文件复制到 Container Layer
2. 在 Container Layer 中修改副本
3. 原始镜像层文件不受影响
2.2.4 镜像的唯一标识
每个层都通过 SHA256 哈希 作为唯一标识:
bash
# 查看镜像的分层信息
docker image inspect nginx:latest
# 输出示例(简化):
{
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:2edcec35b5139b8d5c4d7cf15d1f5b5e2...",
"sha256:e2f5911f58b28f5d0f5b3a8f23d8b3e1b...",
"sha256:12d84a1c9e0e8f23a4b5c6d7e8f9a0b1c..."
]
}
}
# 查看镜像的完整历史(每一层的构建命令)
docker history nginx:latest
2.3 Docker 的存储驱动
2.3.1 什么是存储驱动
存储驱动负责管理镜像层和容器可写层的具体存储实现。不同的存储驱动在性能、稳定性和资源使用上有差异。
2.3.2 常见存储驱动
| 存储驱动 | 底层技术 | 适用场景 | 性能特点 |
|---|---|---|---|
| overlay2 | OverlayFS | Linux 默认推荐 | 读写性能优秀,内存效率高 |
| btrfs | Btrfs 文件系统 | Btrfs 分区 | 支持快照,适合特定场景 |
| zfs | ZFS 文件系统 | ZFS 分区 | 数据完整性高,资源消耗大 |
| vfs | 直接复制 | 调试用 | 性能最差,每层都完整复制 |
| fuse-overlayfs | FUSE 实现的 OverlayFS | Rootless 模式 | 非 root 用户可用 |
bash
# 查看当前使用的存储驱动
docker info | grep "Storage Driver"
# 输出:Storage Driver: overlay2
2.3.3 overlay2 详解
overlay2 是目前最推荐的存储驱动,它将层叠加为一个统一视图:
overlay2 工作原理:
lowerdir (只读层,可以有多个)
↓
┌─────┐
│ L3 │ ← 最底层(基础镜像)
├─────┤
│ L2 │
├─────┤
│ L1 │
└──┬──┘
│
merged (联合挂载点 - 容器看到的视图)
↓
┌──────────────┐
│ 统一文件系统 │
└──────┬───────┘
│
upperdir (可写层 - 容器的修改)
↓
┌─────┐
│ Lw │ ← 容器运行时的修改
└─────┘
workdir (OverlayFS 内部使用的工作目录)
2.4 Docker 的网络架构
2.4.1 libnetwork
Docker 使用 libnetwork 库来管理容器网络,它抽象了多种网络实现:
┌──────────────────────────────────────────┐
│ Docker 网络架构 │
│ │
│ ┌──────────┐ │
│ │ daemon │ │
│ └────┬─────┘ │
│ │ │
│ ┌────┴─────────────────────┐ │
│ │ libnetwork │ │
│ │ ┌─────────┐ ┌────────┐ │ │
│ │ │ CNM │ │ drivers │ │ │
│ │ │ 模型 │ │ │ │ │
│ │ └─────────┘ │- bridge│ │ │
│ │ │- host │ │ │
│ │ │- overlay│ │ │
│ │ │- macvlan│ │ │
│ │ │- none │ │ │
│ └──────────────┴────────┘ │ │
└──────────────────────────────────────────┘
2.4.2 网络模型(CNM)
Docker 网络遵循 容器网络模型(Container Network Model,CNM):
CNM 三大组件:
Sandbox(沙盒) Endpoint(端点) Network(网络)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ - 网络栈 │ │ - veth pair │ │ - 网络名称 │
│ - IP地址 │◄────►│ - 连接点 │◄────►│ - IP子网 │
│ - 路由表 │ │ │ │ - 网关 │
│ - DNS │ │ │ │ - 驱动 │
└─────────────┘ └─────────────┘ └─────────────┘
每个容器 = 1个 Sandbox
每个容器连接到网络 = 1个 Endpoint
每个网络可以连接多个容器
2.5 Docker 的镜像分发机制
2.5.1 镜像分发协议
Docker 使用 OCI Distribution Spec 来分发镜像:
推送/拉取镜像的流程:
docker push myapp:v1.0
│
▼
┌──────────────────┐
│ Docker Client │
└────────┬─────────┘
│ HTTPS API
▼
┌──────────────────┐
│ Registry Server │ (如 Docker Hub, Harbor)
│ │
│ ┌────────────┐ │
│ │ Manifest │ │ ← 清单文件,描述镜像由哪些层组成
│ ├────────────┤ │
│ │ Layer 1 │ │ ← 每层是独立的 blob 对象
│ │ Layer 2 │ │
│ │ Layer 3 │ │
│ └────────────┘ │
└──────────────────┘
2.5.2 镜像清单(Manifest)
Manifest 是一个 JSON 文件,描述了镜像的完整信息:
json
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c54748232..."
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:e692418e4cb..."
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:3c3dda4e7a..."
}
]
}
2.6 Docker 的完整工作流程
2.6.1 docker run 的完整链路
docker run -d -p 8080:80 nginx:latest
执行步骤:
1. Docker Client 解析命令
└── 构造 API 请求:POST /containers/create
2. Docker Daemon 接收请求
├── 检查本地是否有 nginx:latest 镜像
│ ├── 有 → 直接使用
│ └── 没有 → 调用 Registry 拉取
│ ├── 拉取 manifest
│ ├── 对比本地层,下载缺失的层
│ └── 组装镜像
│
├── 创建容器配置
│ ├── 设置网络(端口映射 8080→80)
│ ├── 设置存储(可写层)
│ └── 设置资源限制
│
└── 调用 containerd 创建容器
├── containerd 调用 containerd-shim
├── shim 调用 runc 创建容器
│ ├── 创建 namespace(隔离进程、网络等)
│ ├── 设置 cgroups(限制 CPU、内存等)
│ ├── 挂载文件系统(overlay2 层叠)
│ └── 启动容器进程(nginx -g 'daemon off;')
└── 容器进程运行在独立的隔离环境中
3. 返回容器 ID
└── Docker Client 显示容器 ID
2.6.2 docker build 的完整链路
docker build -t myapp:v1.0 .
执行步骤:
1. 发送构建上下文(Build Context)
└── 将当前目录(.)打包发送给 Docker Daemon
2. 解析 Dockerfile
└── 逐条执行指令
3. 每条指令生成一个新的镜像层
FROM ubuntu:22.04 → 使用已有层或拉取
RUN apt-get update → 创建新层(安装包)
COPY . /app → 创建新层(复制文件)
CMD ["python", "app.py"] → 仅修改元数据,不创建新层
4. 构建缓存机制
├── 检查每一层是否有缓存
│ ├── 有缓存 → 使用缓存层(⚡ 快速)
│ └── 无缓存 → 执行指令,创建新层
└── 一旦某层无缓存,后续所有层都重新构建
2.7 Docker Daemon 的配置
2.7.1 配置文件位置
| 操作系统 | 配置文件路径 |
|---|---|
| Linux | /etc/docker/daemon.json |
| Windows | C:\ProgramData\docker\config\daemon.json |
| macOS | Docker Desktop → Settings → Docker Engine |
2.7.2 常用配置项
json
{
"data-root": "/data/docker",
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com"
],
"insecure-registries": [],
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 5,
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 }
],
"live-restore": true,
"userland-proxy": false
}
| 配置项 | 说明 |
|---|---|
data-root |
Docker 数据存储目录(镜像、容器、卷等) |
storage-driver |
存储驱动类型 |
log-driver |
容器日志驱动 |
log-opts |
日志驱动选项(大小限制) |
registry-mirrors |
镜像加速器 |
live-restore |
dockerd 重启时保持容器运行 |
userland-proxy |
禁用用户态代理,提高性能 |
bash
# 重新加载 Docker 配置
sudo systemctl daemon-reload
sudo systemctl restart docker
# 验证配置
docker info
2.8 Docker 的命名空间详解
2.8.1 六大命名空间
bash
# 进入一个运行中的容器
docker run -it --rm ubuntu:22.04 /bin/bash
# 在容器内查看(隔离效果)
# 1. PID Namespace - 只能看到容器自己的进程
ps aux
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# root 1 0.0 0.0 2488 1320 pts/0 Ss 10:00 0:00 /bin/bash
# 2. NET Namespace - 独立的网络栈
ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> ...
# 15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# inet 172.17.0.2/16 ...
# 3. MNT Namespace - 独立的文件系统
ls /
# bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
# 4. UTS Namespace - 独立的主机名
hostname
# a1b2c3d4e5f6 (容器ID的前12位)
2.8.2 Namespace 的系统调用
c
// Linux 提供的 namespace 系统调用
// clone() - 创建新进程并放入新的 namespace
int clone(int (*fn)(void *), void *stack, int flags, void *arg);
// 常用 flags
CLONE_NEWPID // 新的 PID namespace
CLONE_NEWNET // 新的 NET namespace
CLONE_NEWNS // 新的 MNT namespace
CLONE_NEWUTS // 新的 UTS namespace
CLONE_NEWIPC // 新的 IPC namespace
CLONE_NEWUSER // 新的 USER namespace
// unshare() - 将当前进程放入新的 namespace
int unshare(int flags);
// setns() - 将当前进程加入已有的 namespace
int setns(int fd, int nstype);
2.9 Docker 的 Cgroups 详解
2.9.1 Cgroups v1 vs v2
| 特性 | Cgroups v1 | Cgroups v2 |
|---|---|---|
| 层级结构 | 每个控制器独立层级 | 统一层级 |
| 资源分配 | 更灵活但复杂 | 更直观、统一 |
| 内存限制 | memory.limit_in_bytes | memory.max |
| CPU 限制 | cpu.cfs_quota_us | cpu.max |
| 默认版本 | CentOS 7/8 | Ubuntu 20.04+、Fedora |
2.9.2 查看容器的 Cgroups 配置
bash
# 启动一个限制资源的容器
docker run -d --name test-container \
--cpus="1.5" \
--memory="512m" \
--memory-swap="512m" \
nginx:latest
# 查看容器的 cgroup 限制
# Cgroups v2 方式
cat /sys/fs/cgroup/system.slice/docker-<container-id>.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-<container-id>.scope/cpu.max
2.10 动手实验
实验 2.1:观察镜像的分层结构
bash
# 拉取一个镜像
docker pull python:3.11-slim
# 查看镜像的每一层
docker history python:3.11-slim
# 查看详细的层信息
docker inspect python:3.11-slim
实验 2.2:观察容器的隔离性
bash
# 终端1:在宿主机上查看进程
ps aux | grep nginx
# 终端2:启动 Nginx 容器
docker run -d --name nginx-test -p 8080:80 nginx:latest
# 终端3:在宿主机查看进程(能看见容器进程)
ps aux | grep nginx
# root 1234 ... nginx: master process nginx -g daemon off;
# system+ 1235 ... nginx: worker process
# 终端4:进入容器查看进程(只看到自己的进程)
docker exec -it nginx-test ps aux
# PID TTY STAT TIME COMMAND
# 1 ? Ss 0:00 nginx: master process nginx -g daemon off;
# 21 ? S+ 0:00 ps aux
实验 2.3:验证写时复制
bash
# 启动一个 Ubuntu 容器
docker run -it --name copy-test ubuntu:22.04 /bin/bash
# 在容器内创建文件
echo "Hello Container" > /tmp/test.txt
cat /tmp/test.txt
# Hello Container
# 退出容器
exit
# 查看容器的可写层大小
docker inspect copy-test --format='{{.GraphDriver.Data}}'
# 再次启动容器,文件仍然存在(在可写层中)
docker start -ai copy-test
cat /tmp/test.txt
# Hello Container
exit
2.11 本章小结
| 要点 | 内容 |
|---|---|
| 架构模式 | C/S 架构:Client(CLI)→ Daemon(dockerd)→ containerd → runc |
| 镜像分层 | 每条 Dockerfile 指令生成一层,层通过 SHA256 哈希唯一标识 |
| 写时复制 | 容器启动时添加可写层,修改操作通过 CoW 实现 |
| 存储驱动 | 推荐 overlay2,管理层的叠加和读写 |
| 命名空间 | PID/NET/MNT/UTS/IPC/USER 六种隔离机制 |
| Cgroups | 限制 CPU、内存、磁盘 I/O、网络带宽等资源 |
2.12 课后练习
- 理解题:解释 Docker 镜像的分层存储如何提高存储效率和构建速度。
- 实操题 :使用
docker history查看 3 个不同镜像的层结构,分析每层的内容。 - 进阶题:配置 Docker 使用远程 Daemon(TCP 模式),体验 C/S 架构的网络通信。
📖 下一章:Docker 安装部署 ------ 在不同操作系统上搭建 Docker 环境