docker 2 - 特性
docker 2: 特性
Docker的本质是通过分层共享机制实现资源原子化。
分层结构
假设现在有一个在 docker 中部署 tomcat
项目的交付包, 其内容结构如下:
1 | myapp-docker/ |
只读层
可能的例子: dockerfile
将分层实现部署;
1 | # Dockerfile |
只读层的文件存放在 /var/lib/docker/overlay2
目录下, 而可写层的数据在容器的独立目录中。只读层的内容是不可修改的, 在容器重启后保持不变, 而可写层的内容是实时读写的, 默认情况下容器重启后会丢失。
哈希
示例 dockerfile
:
1 | # 层 1: 基础层 (FROM) |
- 层 1: 基础只读层
ubuntu:22.04
镜像本身也是由多个层构成(例如base-files
,apt
,bash
等包各自的层)。为了简化,我们把这些基础层合并视为Layer1-base
。- 哈希计算 (H1): Docker 引擎会对构成
ubuntu:22.04
镜像最终状态的所有文件(/bin
,/etc
,/usr
,/var
等)进行快照,打包成一个tar
文件,计算其SHA256
哈希值,得到H1
。这个H1
就是ubuntu:22.04
镜像最顶层(即最终状态层)的唯一标识。Layer1-base
的 ID =sha256:H1
。
- 层 2: 新的只读层
- Docker 启动一个临时容器,基于
Layer1-base
。 - 在临时容器中执行各条终端指令, 如安装 apache2 服务;
- .捕获差异: 执行完毕后, Docker 引擎会比较当前容器的文件系统和启动时 (
Layer1-base
) 的差异, 然后捕获: 获取所有修改和新增的文件, 并记录删除的文件。 - Docker 将这个差异集合打包成一个新的 tar 文件,计算其 SHA256 哈希值,得到
H2
。Layer2-run
的 ID =sha256:H2
。注意:
H2
的计算依赖于RUN
命令产生的 所有 文件变化,不是只算修改的部分。
- Docker 启动一个临时容器,基于
- 层 3:
COPY index.html
- Docker 基于
Layer1-base
+Layer2-run
(H1
+H2
) 的状态启动另一个临时容器(或复用)。 - 将宿主机上的
index.html
文件复制到容器内的/var/www/html/
目录下。这是一个新增文件操作。 - 捕获差异: Docker 捕获到新增文件
/var/www/html/index.html
。 - 哈希计算 (
H3
): Docker 将这个只包含新增文件/var/www/html/index.htm
l 及其完整内容 (<h1>Hello Docker Layers!</h1>
) 的集合打包成一个新的 tar 文件,计算其 SHA256 哈希值,得到H3
。Layer3-copy
的 ID =sha256:H3
。
- Docker 基于
镜像本身的 ID 通常是其最顶层 (
Layer3-copy
) 的哈希H3
或其配置清单 (Manifest) 的哈希。
构建结果:镜像 my-app:latest
, 由 3 个只读层组成(按顺序堆叠):
Layer1-base
:sha256:H1
(Ubuntu 22.04 的最终层)Layer2-run
:sha256:H2
(修改配置 + 安装 Apache2)Layer3-copy
:sha256:H3
(添加index.html
)
可写层
运行容器和共享的概念发生在 docker run
时。
Docker 引擎的执行步骤:
- 找到镜像
my-app:latest
对应的层列表 (H1
,H2
,H3
)。 - 检查本地存储(通常在
/var/lib/docker/overlay2/
)是否已有这些层(对照哈希)。 - 为容器创建一个新的、空的、可写层 (Container Writable Layer)。
- 使用联合文件系统 (如 OverlayFS) 将这 4 层挂载起来:
lowerdir
=Layer1-base:H1,Layer2-run:H2,Layer3-copy:H3
(只读)upperdir
=Container Writable Layer
(可写)merged
= 容器内看到的统一视图。
共享
基于分层结构的共享
在多个容器运行时, 有相同哈希的只读层会共享存储空间。举个例子:
场景 I : 容器 A 和容器 B 都直接基于
my-app:latest
运行:- 层共享情况:
Layer1-base
(H1
): 共享 (磁盘上一份,Page Cache 共享只读文件)Layer2-run
(H2
): 共享Layer3-copy
(H3
): 共享
- 哈希比较过程: 两个容器指定了同一个镜像:
my-app:latest
, docker 引擎只需要检查H1
,H2
,H3
均已知且存在, 就可以直接复用。操作系统层面上, 运行时,内核通过 Page Cache 共享相同只读文件的内存页。
- 层共享情况:
场景 II : 容器 C 基于
ubuntu:22.04
运行- 仅共享 FROM 层
H1
; - 层共享情况:
Layer1-base
(H1
): 共享 (和my-app
容器共用同一份Ubuntu 22.04
基础层)Layer2-run
,Layer3-copy
(H2
,H3
): 不共享 (容器 C 没有这个两个层)
- 哈希比较过程:
- 当运行
docker run ubuntu:22.04
时,引擎解析出ubuntu:22.04
镜像的最顶层哈希是H1
。 - 引擎检查本地存储:
H1
已存在! (因为之前构建my-app
时拉取过)。 - 引擎直接复用本地已有的
Layer1-base (H1)
的磁盘数据。无需重新下载或存储。 - 容器 C 有自己的可写层,在
H1
之上。
- 当运行
- 仅共享 FROM 层
考虑情景: 容器 X 和容器 Y 有相同的
Layer1
, 不同的Layer2
。 而在构建Layer3
时的文件操作指令完全相同, 那么Layer3:X
和Layer3:Y
会共享吗?实际上, 即使指令完全一致, 由于父层存在差异, 导致即使执行了相同的变更操作,由于变更操作所作用的底层文件状态不同,最终导致子层的内容 (tar 包) 几乎必然不同,因此它们的哈希值 (Layer3:X
和Layer3:Y
) 也几乎必然不同,无法共享。可以说, 共享只会在极其特殊且刻意控制的情况下发生。
内存共享 (Page Cache)
当容器进程读取文件系统中的文件(无论是来自底层的只读镜像层还是自己的可写层)时,内核会将文件内容缓存到内存(Page Cache)中以加速后续访问。
如果多个容器访问同一个底层只读镜像层中的同一个文件(例如 /usr/lib/libc.so
),并且这些容器运行在同一个主机上,那么内核只会将这个文件在物理内存中加载一份,并通过 Page Cache 共享给所有访问它的容器进程。这极大地减少了内存消耗。
其他重要特性
进程级隔离
利用 Linux 内核的
pid
,net
,ipc
,mnt
,uts
,user
, 实现独立进程树(容器内 PID = 1),独立网络栈, 主机名隔离, 独立挂载点。一次构建,处处运行
镜像包含应用及其完整依赖链(libc、环境变量、配置文件)。运行时通过 Docker Engine 抽象底层差异。
网络模型
可以近似理解为 Docker 设置了一个内网, 宿主机充当 网关 + 交换机 的作用
数据持久化: Volume
Volume 是由 Docker 管理的独立于容器和镜像之外的存储区域。
生命周期:
- Volume 的创建/销毁与容器解耦(
docker volume create/rm
)。 - 容器删除时,Volume 默认保留(除非使用
docker rm -v
)。
挂载
联合文件系统绕过:Volume 通过 bind mount
直接挂载到容器路径,完全绕过联合文件系统(OverlayFS)。
对 Volume 的读写操作不触发
Copy-on-Write
,直接操作宿主机磁盘。
首次挂载时如果容器内目标文件夹不为空, 将被宿主机覆盖。