阅读完本文后,您将能够:
- 说清 Linux 容器赖以实现的三大内核机制——命名空间(namespaces)、控制组(cgroups)与联合文件系统(UnionFS);
- 描述 Docker 的分层运行时架构(dockerd → containerd → runc)及各组件职责;
- 解释镜像分层、写时复制与构建缓存的工作原理;
- 识别 Docker 的网络与存储模型,以及其安全边界与加固手段;
- 将上述原理转化为生产环境下的容器架构设计实践。
重要 本文聚焦 Docker 引擎(Docker Engine,社区版/企业版)的原理与架构。容器编排(Kubernetes、Docker Swarm)与镜像分发(Harbor 等)属于上层主题,仅在必要时提及。
目录
- 概述:为什么需要容器
- 容器化的底层原理
- Docker 的组件架构
- 镜像分层与构建原理
- 容器运行原理与生命周期
- 网络与存储模型
- 架构演进历史
- 安全模型与加固
- 原理对架构设计的启示
- 常见误区与故障排除
- 后续步骤
1. 概述:为什么需要容器
传统应用部署长期面临「在我机器上能跑」的环境一致性问题:依赖版本、系统库、配置文件、运行时环境的细微差异,都会导致应用在开发、测试、生产之间行为不一致。虚拟机虽能解决一致性问题,但每个虚拟机都要运行一整套客户操作系统,体积大、启动慢、资源开销高。
容器是一种轻量级的操作系统级虚拟化技术。它将应用及其全部依赖打包为一个标准化单元,共享宿主机内核,无需启动完整的客户操作系统,因此具备以下特点:
| 特性 | 容器 | 虚拟机 |
|---|---|---|
| 虚拟化层级 | 操作系统级(共享内核) | 硬件级(每实例一整套 OS) |
| 启动时间 | 秒级 | 分钟级 |
| 资源开销 | 低(MB 级) | 高(GB 级) |
| 隔离强度 | 较弱(共享内核) | 强(硬件隔离) |
| 单机密度 | 高(数百实例) | 低(数十实例) |
Docker 并不是容器的发明者——Linux 内核的容器原语(namespaces、cgroups)在 Docker 诞生(2013 年)前就已存在——但 Docker 首次把这些底层能力封装成「镜像 + 容器 + 一致的工作流」,让容器技术得以大规模普及,并推动形成了今天的 OCI(Open Container Initiative)标准。
提示 **一句话概括:**容器 = 受隔离与限流的进程。它本质上仍是宿主机上的普通进程,只是被内核「骗」成了以为自己独占整台机器。
2. 容器化的底层原理
容器的「隔离」「限流」「分层」三大能力,分别由 Linux 内核的三类机制实现。理解这三点,就理解了容器的本质。
2.1 命名空间(Namespaces):实现隔离
命名空间让一组进程看到一份****独立、被裁剪过的系统资源视图,从而彼此隔离。Docker 主要使用以下命名空间:
| 命名空间 | 隔离对象 | 效果 |
|---|---|---|
| PID | 进程号 | 容器内进程从 PID 1 重新编号,看不到宿主机其他进程 |
| NET | 网络栈 | 独立的网卡、路由表、iptables、端口空间 |
| IPC | 进程间通信 | 独立的 System V IPC、POSIX 消息队列 |
| MNT | 挂载点 | 独立的文件系统视图 |
| UTS | 主机名与域名 | 独立的 hostname |
| USER | 用户与用户组 ID | 容器内的 root 可映射为宿主机上的非特权用户 |
| Cgroup | cgroup 视图 | 独立的 cgroup 层次结构视图(Linux 4.6+) |
可以亲手验证。在宿主机启动一个容器,然后观察其命名空间:
# 启动一个长期运行的容器
docker run -d --name demo alpine sleep 3600
# 找到容器内 sleep 进程在宿主机上的真实 PID
PID=$(docker inspect -f '{{.State.Pid}}' demo)
echo $PID
# 查看该进程所属的所有命名空间
ls -l /proc/$PID/ns
提示 **也可以用 **
lsns命令列出宿主机上当前所有的命名空间及其属主进程,直观看到 Docker 为每个容器创建的独立命名空间集合。
2.2 控制组(Cgroups):实现资源限制
命名空间负责「隔离」,但隔离后的进程仍可能耗尽宿主机的 CPU、内存、磁盘 I/O。**cgroups(Control Groups) 负责对一组进程进行资源计量与限制**,是防止「吵闹的邻居」的关键。
主要资源控制器包括:
| 控制器 | 限制对象 | Docker 对应参数 |
|---|---|---|
| cpu / cpuacct | CPU 配额与使用统计 | --cpus、--cpu-shares |
| memory | 内存与交换空间 | --memory、--memory-swap |
| pids | 进程数(防 fork 炸弹) | --pids-limit |
| blkio | 块设备 I/O | --device-read-bps等 |
| devices | 设备访问白名单 | --device |
cgroups 有 v1(按控制器分层次)与 v2(统一层级)两代。现代发行版默认逐步转向 cgroup v2。同样可以实地观察:
# 查看上面 demo 容器进程的 cgroup 归属
cat /proc/$PID/cgroup
重要 生产环境务必为每个容器显式设置 CPU 与内存上限。否则单个容器的异常(如内存泄漏)可能拖垮整台宿主机。
2.3 联合文件系统(UnionFS):实现分层
联合文件系统将****多个目录「叠加」成一个逻辑文件系统,是 Docker 镜像分层与「写时复制」的基础。
- 镜像(image) 由若干**只读层(read-only layer)**堆叠而成,每一层对应 Dockerfile 中的一条指令(如
RUN、COPY)。 - 容器(container) 在镜像之上再加一层可写层(writable layer)。所有对容器的修改都写入这一层,底层镜像保持不变。
- 写时复制(Copy-on-Write, CoW):当容器需要修改位于只读层中的某个文件时,系统并不直接改写原文件,而是先把它复制到可写层再修改,从而保证镜像层的不变性与可共享性。
Docker 支持多种存储驱动,overlay2 是当前默认且性能最佳的实现:
| 存储驱动 | 说明 |
|---|---|
overlay2 |
默认推荐,基于 OverlayFS,性能与稳定性最佳 |
fuse-overlayfs |
用于 rootless 模式 |
btrfs/zfs |
依赖对应文件系统的高级特性 |
devicemapper/aufs |
早期方案,现已不推荐 |
查看当前使用的存储驱动:
docker info | grep -i "storage driver"
2.4 一条命令的全过程
**把三大机制串联起来,一次 **docker run 在底层大致发生:
- 命名空间为容器构建独立的进程/网络/文件系统视图;
- cgroups套上 CPU、内存等资源限制;
- 联合文件系统把镜像各层叠加,并附加可写层;
- 最终 exec 出容器入口进程——它是一个****被隔离、被限流、拥有叠加文件系统视图的普通宿主机进程。
3. Docker 的组件架构
Docker 采用****客户端/服务端(C/S)架构,并自 1.11 版起拆分为「高层运行时 + 低层运行时」的分层设计。整体结构如下图所示。
3.1 架构总览
┌─────────────────────────────────────────────────────────────────┐
│ 宿主机 (Host) │
│ │
│ ┌──────────────┐ REST API ┌──────────────────────────┐ │
│ │ Docker CLI │ ─────────────▶│ dockerd (守护进程) │ │
│ │ (client) │ Unix socket │ 镜像/网络/卷/API 管理 │ │
│ └──────────────┘ /var/run/ └─────────────┬────────────┘ │
│ docker.sock │ gRPC │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ containerd │ │
│ ┌──────────────────┐ │ 容器生命周期/镜像分发 │ │
│ │ Registry │◀──────────▶│ /快照管理 │ │
│ │ (Hub/Harbor) │ 拉取/推送 └──────────┬───────────┘ │
│ └──────────────────┘ │ 每容器一个 │
│ ┌──────────┴───────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌────────┐ │
│ │containerd- │ │ ... │ │
│ │ shim │ │ │ │
│ │ │ runc │ │ │ │
│ │ ▼ │ │ │ │
│ │ 容器进程 │ │ │ │
│ └─────────────┘ └────────┘ │
│ │
│ Linux 内核: namespaces | cgroups | UnionFS | netfilter │
└─────────────────────────────────────────────────────────────────┘
3.2 各组件职责
| 组件 | 角色 | 职责 |
|---|---|---|
Docker Client(dockerCLI) |
客户端 | 解析用户命令,通过 REST API 与守护进程通信 |
| dockerd | 守护进程/高层管理 | 监听 API、管理镜像/容器/网络/卷、构建镜像 |
| containerd | 高层容器运行时 | 管理容器完整生命周期、镜像拉取与分发、快照、supervision |
| containerd-shim | 每容器一个的垫片 | 作为容器进程的父进程,接管 STDIO 与退出码,使守护进程重启不影响容器 |
| runc | 低层 OCI 运行时 | 依据 OCI 运行时规范,真正「组装」namespaces + cgroups 并启动容器进程 |
| Registry | 镜像仓库 | 存储与分发镜像(Docker Hub、私有仓库、Harbor 等) |
3.3 调用链:从命令到进程
**用户执行 **docker run 后,请求沿分层架构逐级下发:
docker CLI ──REST──▶ dockerd ──gRPC──▶ containerd
──创建 shim──▶ containerd-shim ──OCI runtime spec──▶ runc
──setns+cgroup──▶ 容器入口进程
提示 **注意 **
runc在启动容器进程后会随即退出,由常驻的containerd-shim充当容器进程的父进程。正是这一设计,使得dockerd甚至containerd重启升级时,容器仍能继续运行——这是分层架构带来的关键收益。
3.4 OCI 标准
Docker 在 2015 年联合业界发起 ****OCI(Open Container Initiative),定义了两项事实标准:
- 镜像规范(image-spec):镜像的格式、清单(manifest)、层与配置的标准结构;
- 运行时规范(runtime-spec):描述「如何把一个容器跑起来」的环境与行为(即 runc 读取的那份配置)。
标准化使得 Docker 镜像可以在 containerd、CRI-O、Kubernetes 等不同运行时之间无缝流通,也使得低层运行时(runc、kata、gVisor)可以互换。
4. 镜像分层与构建原理
4.1 镜像、层与容器
- 镜像(Image):只读的分层模板,包含应用代码、运行时、库与配置。镜像通过内容寻址——每层与镜像整体都以 SHA256 摘要标识。
- 层(Layer):文件系统的增量变更集,可被多个镜像共享复用,节省存储与传输。
- 容器(Container):镜像的运行实例,等于「全部只读镜像层 + 一个可写层 + 隔离环境」。
┌───────────────────────┐ ◀── 可写层(容器独有,随容器删除而消失)
│ Writable layer │
├───────────────────────┤
│ Layer N: CMD/ENTRYPOINT│ ◀── 只读层(来自镜像,多容器共享)
├───────────────────────┤
│ Layer 2: COPY 源码 │
├───────────────────────┤
│ Layer 1: 安装依赖 │
├───────────────────────┤
│ Layer 0: 基础镜像 │
└───────────────────────┘
4.2 Dockerfile 与构建过程
Dockerfile 中****每条会修改文件系统的指令(RUN、COPY、ADD)都会产生一个新层。构建时:
- 客户端将****构建上下文(build context)发送给 dockerd;
- dockerd 逐条执行指令,每条指令基于上一层产生新层;
- 构建缓存:若某条指令及其上层均未变化,则直接复用缓存层,跳过实际执行——这是加速重复构建的核心机制。
一个简化的示例:
# 基础镜像(Layer 0)
FROM alpine:3.20
# 安装依赖(Layer 1)——变动频率低,放前面以命中缓存
RUN apk add --no-cache curl
# 拷贝源码(Layer 2)——变动频率高,放后面
COPY app.sh /app/app.sh
# 入口(不产生文件层,仅修改镜像配置)
CMD ["/app/app.sh"]
重要 缓存是「指令 + 上文」共同决定的。把****变动频繁的内容(如源代码)放在 Dockerfile 末尾,把变动少的内容(如依赖安装)放在前面,可最大化缓存命中率,显著缩短构建时间。
4.3 多阶段构建与镜像瘦身
**最终镜像往往不需要编译工具链。**多阶段构建(multi-stage build) 允许在一个 Dockerfile 中用多个 FROM,只在最终阶段拷入必要产物:
# 阶段 1:构建(含编译器,体积大)
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app .
# 阶段 2:运行(仅含二进制,体积小)
FROM alpine:3.20
COPY --from=builder /src/app /usr/local/bin/app
CMD ["app"]
**最终镜像只包含第二阶段的内容,体积可从数百 MB 降至数 MB。常见瘦身手段还包括:合并 **RUN 指令、清理包缓存、选用 alpine/distroless 基础镜像。
5. 容器运行原理与生命周期
5.1 容器的状态
一个容器在其生命周期中处于以下状态之一:
created ──start──▶ running ──stop──▶ exited
│ │
└───── pause ──────▶ paused ──unpause──▶ running
| 状态 | 含义 |
|---|---|
created |
已创建(已分配命名空间与文件系统)但未启动入口进程 |
running |
入口进程正在运行 |
paused |
冻结容器内所有进程(cgroup freezer) |
exited |
进程已退出(容器与可写层仍保留,可重启) |
removed |
容器及其可写层被删除 |
5.2 PID 1 与信号处理
**容器入口进程是容器命名空间内的 **PID 1,需要承担两项传统上由 init 系统完成的职责:
- 信号转发:接收
SIGTERM等并优雅退出; - 回收僵尸进程(reaping)。
重要 **若直接以 shell 脚本或后台 fork 的进程作为入口,PID 1 常无法正确转发信号,导致 **
docker stop超时后被SIGKILL强杀,造成数据不一致。建议入口直接指向应用主进程,或使用tini、dumb-init等轻量 init。
6. 网络与存储模型
6.1 存储模型
**容器文件系统位于可写层,**容器删除即丢失。持久化数据应使用以下机制:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| volume(卷) | 由 Docker 管理,位于/var/lib/docker/volumes |
生产首选,跨平台、易备份迁移 |
| bind mount(绑定挂载) | 直接挂载宿主机任意路径 | 开发期挂源码、配置文件 |
| tmpfs(内存盘) | 数据存于内存 | 临时、敏感数据,重启即失 |
# 使用命名卷持久化数据
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
6.2 网络模型
Docker 提供多种网络驱动:
| 网络模式 | 说明 | 典型场景 |
|---|---|---|
| bridge | 默认。通过docker0网桥 + NAT(iptables/netfilter)访问外部 |
单机多容器互通 |
| host | 直接使用宿主机网络栈,无隔离 | 对网络性能要求极高、无需端口隔离 |
| none | 仅 loopback,无网络 | 离线计算、安全沙箱 |
| overlay | 跨主机 VXLAN 网络 | Docker Swarm 多主机组网 |
| macvlan | 容器拥有独立 MAC/IP,直连物理网络 | 传统网络迁移、低延迟 |
提示 自定义 bridge 网络会自动为其中的容器提供基于容器名的 DNS 解析,因此多容器协作时应使用自定义网络而非默认
bridge:
docker network create appnet
docker run -d --name app --network appnet myapp
docker run -d --name db --network appnet postgres:16
# 在 app 容器内可直接用主机名 db 连接数据库
7. 架构演进历史
Docker 的架构并非一蹴而就,理解其演进有助于看清设计取舍。
| 阶段 | 时间 | 架构要点 |
|---|---|---|
| LXC 时代 | 2013 初版 | 直接调用 LXC 工具创建容器,强依赖宿主机 LXC |
| libcontainer | 2014 | 自研 Go 实现的运行时库,摆脱对 LXC 的依赖 |
| 运行时分层 | 2016(v1.11) | 拆分为 dockerd → containerd → runc 三层,引入 OCI 标准 |
| containerd 独立 | 2017 | containerd 作为 CNCF 项目独立演进,并被 Kubernetes 用作 CRI 运行时 |
| Rootless / v2 | 近年 | 支持 rootless 模式、cgroup v2、BuildKit 默认构建器 |
注意 演进的核心趋势是****关注点分离:把「镜像/网络/卷管理」「容器生命周期」「低层系统调用」分别交给 dockerd、containerd、runc,并统一于 OCI 标准。这使得各组件可独立升级、可被其他平台(如 Kubernetes)复用。
8. 安全模型与加固
容器的隔离****弱于虚拟机——所有容器共享宿主机内核。因此容器安全是一套纵深防御组合拳,而非单一机制。
| 机制 | 作用 | 默认是否启用 |
|---|---|---|
| Namespaces | 资源视图隔离 | 是 |
| Cgroups | 资源用量限制 | 是(需显式配置上限) |
| Linux Capabilities | 细粒度权限裁剪,丢弃大部分特权能力 | 是(默认丢弃多数) |
| seccomp | 过滤危险系统调用(默认 profile 屏蔽数十个) | 是 |
| AppArmor / SELinux | 强制访问控制(MAC) | 视发行版而定 |
| NoNewPrivs | 禁止通过 setuid 等提权 | 是 |
| Rootless / USER namespace | 容器内 root 映射为宿主机非特权用户 | 需显式开启 |
常见加固实践:
- 以****非 root 用户运行容器进程(Dockerfile 中
USER指令); - **启用 **
--read-only只读根文件系统,数据写入卷; - **显式 **
--cap-drop ALL后仅按需--cap-add; - 生产开启 ****rootless 模式或 USER 命名空间映射;
- 使用可信基础镜像并****定期扫描漏洞。
警告 **切勿以 **
--privileged运行容器——它相当于把容器几乎变成宿主机 root,会绕过上述大部分安全机制。仅在受控的调试场景临时使用。
9. 原理对架构设计的启示
理解原理之后,可将其转化为容器化应用的设计准则。
- 无状态优先:将状态(数据、会话)外置到卷或外部存储,容器本身保持无状态、可随时销毁重建。
- 单容器单职责:一个容器只运行一个主进程;多服务组合交给编排系统而非塞进一个容器。
- 显式声明资源:始终设置
--memory与--cpus,让 cgroups 成为可预测的资源护栏。 - 镜像即契约:把依赖、配置固化进镜像,实现「构建一次,处处运行」;配置与密钥通过环境变量或 secret 注入,而非写死在镜像里。
- 分层与缓存意识:合理安排 Dockerfile 指令顺序与多阶段构建,兼顾镜像体积与构建效率。
- 网络与持久化分离设计:用自定义网络 + DNS 做服务发现,用卷做数据持久化,避免依赖容器可写层。
- 安全纵深:非 root、最小权限、只读根、漏洞扫描,作为镜像出厂标准。
- 面向编排设计:即使当前用单机 Docker,也应按「可被 Kubernetes/Swarm 编排」的方式设计容器(健康检查、优雅退出、可水平扩展)。
10. 常见误区与故障排除
| 现象或误区 | 根因与处理 |
|---|---|
| 误以为「容器是迷你虚拟机」 | 容器是共享内核的被隔离进程,无独立内核;不要在容器内跑 init 系统或多服务 |
docker stop长时间后才停止 |
入口进程不是 PID 1,或未处理SIGTERM。让应用直接作为入口或用tini |
| 容器重启后数据丢失 | 数据写在了可写层。改用 volume 或绑定挂载持久化 |
| 容器间无法用名字互连 | 使用的是默认 bridge,它不提供 DNS。改用自定义 bridge 网络 |
| 磁盘空间莫名被占满 | 废弃镜像/层与停止的容器堆积。用docker system prune清理 |
| 构建很慢、缓存总失效 | 变动频繁的指令放在了 Dockerfile 前部。调整指令顺序,把易变内容后置 |
| 容器内 root 能影响宿主机 | 默认未启用 USER 命名空间。生产建议 rootless 或用户映射,并避免--privileged |
实时排查常用命令:
docker ps -a # 查看所有容器及状态
docker logs -f <容器> # 跟踪日志
docker inspect <容器> # 查看挂载、网络、资源限制等详细配置
docker stats # 实时资源占用(CPU/内存/IO)
docker system df # 磁盘占用分布
11. 后续步骤
- BuildKit:学习新一代构建器,支持并行构建、更好的缓存与密钥挂载。
- Compose:用
docker compose描述并管理多容器应用。 - 编排:进阶到 Docker Swarm 或 Kubernetes,实现服务的调度、扩缩容与自愈。
- 镜像分发:搭建 Harbor 等私有镜像仓库,纳入漏洞扫描与签名。
- 可观测性:将容器的 metrics、日志、追踪接入 Prometheus、Loki、Jaeger 等体系。
参考
- Docker 官方架构文档:https://docs.docker.com/get-started/docker-overview/
- Docker 引擎参考:https://docs.docker.com/engine/
- **Linux **
namespaces手册:https://man7.org/linux/man-pages/man7/namespaces.7.html - **Linux **
cgroups手册:https://man7.org/linux/man-pages/man7/cgroups.7.html - OCI 规范:https://opencontainers.org/
- containerd 项目:https://containerd.io/