Docker 镜像是如何分层(Layering)的?什么是写时复制(Copy-on-Write)机制?
在 Docker 的世界中,镜像分层(Layering)和写时复制(Copy-on-Write, 简称 CoW)是两个最核心的技术基石。它们不仅让 Docker 变得极其轻量和快速,还极大地节省了磁盘空间和网络带宽。
下面我们用通俗易懂的语言和图解来深入拆解这两个概念。
一、 Docker 镜像是如何分层(Layering)的?
1. 什么是分层?
Docker 镜像并不是一个单一的大文件,而是由一组只读的层(Layers)叠加而成的。
每一层都代表了 Dockerfile 中的一条指令(如 FROM, RUN, COPY 等)。
2. 联合文件系统(UnionFS)
Docker 利用 联合文件系统(Union File System) 技术,将这些不同的层“挂载”并合并成一个统一的视图。对用户来说,你看不到底层的多个目录,只能看到一个完整的文件系统(比如一个干净的 Ubuntu 系统加上你的应用)。
3. 镜像分层的结构(以一个简单的 Node.js 应用为例)
假设你的 Dockerfile 如下:
plaintext
FROM ubuntu:20.04 # Layer 1: 基础操作系统
RUN apt-get update && apt-get install -y nodejs # Layer 2: 安装 Node.js
COPY . /app # Layer 3: 复制你的代码
CMD ["node", "/app/index.js"] # Layer 4: 设定启动命令
在 Docker 内部,它的结构就像建楼房:
plaintext
┌──────────────────────────────────────────────────┐
│ [读写层] 容器层 (Container Layer) - 运行容器时创建 │ <-- 唯一可写的地方
├──────────────────────────────────────────────────┤
│ [只读层] Layer 4: CMD ["node", "/app/index.js"] │ ^
├──────────────────────────────────────────────────┤ |
│ [只读层] Layer 3: COPY . /app │ | 镜像层 (Image Layers)
├──────────────────────────────────────────────────┤ | (一经构建,终身只读)
│ [只读层] Layer 2: RUN apt-get install... │ |
├──────────────────────────────────────────────────┤ |
│ [只读层] Layer 1: FROM ubuntu:20.04 │ v
└──────────────────────────────────────────────────┘
4. 分层的好处:
- 共享资源(节省空间):如果你有 10 个镜像都是基于
ubuntu:20.04构建的,那么在你的宿主机上,ubuntu:20.04这一层只会被下载和存储一次。 - 快速分发(节省带宽):当你推送(Push)或拉取(Pull)镜像时,只有发生变化的层需要传输。如果本地已经有了基础层,Docker 会直接跳过下载。
- 缓存构建:在重新构建镜像时,如果某一步之前的指令没有变化,Docker 会直接复用缓存的层,极大地加快构建速度。
二、 什么是写时复制(Copy-on-Write, CoW)机制?
既然镜像的每一层都是只读的,那么问题来了:当我们运行容器并在里面修改文件、写日志或保存数据时,数据写到哪里去了?
这就是 写时复制(Copy-on-Write) 机制发挥作用的地方。
1. 核心原理
当一个容器启动时,Docker 会在最顶部的只读镜像层之上,添加一个薄薄的可写层(Writable Layer),通常被称为“容器层”(Container Layer)。
- 读文件:如果容器需要读取一个文件,它会从上往下在各个镜像层中寻找。一旦找到,就直接读取。
- 写/修改文件(CoW 触发):如果容器需要修改一个已存在的文件(这个文件存在于底层的只读镜像层中):
- Docker 会在底层的只读层中找到该文件。
- 将该文件复制(Copy)一份到最顶部的可写层。
- 容器在可写层中对该文件的副本进行修改(Write)。
- 底层的原始文件保持不变,但由于联合文件系统的“遮罩”效应,容器只能看到顶部可写层中修改后的新版本文件。
- 删除文件:如果容器要删除一个文件,它会在可写层创建一个“遮罩文件”(Whiteout),在用户看来这个文件被删除了,但实际上底层镜像中的文件依然完好无损。
2. 图解写时复制
假设我们要修改镜像层中的 config.txt 文件:
plaintext
【修改前】
[可写层] ── (空)
[镜像层] ── config.txt (只读)
【修改时触发 CoW】
[可写层] ── config.txt (复制到这里并进行修改) <── 容器写入这里
^ (复制)
[镜像层] ── config.txt (原始文件,保持只读)
3. CoW 的好处:
- 极速启动:启动一个新容器时,Docker 不需要复制整套镜像文件,只需要创建一个空的可写层。这个过程几乎是瞬间完成的(几毫秒)。
- 超高空间利用率:100 个运行相同镜像的容器,可以共享同一个底层的只读镜像。每个容器只占用极少量的空间(只记录它们自己修改过的数据)。
总结
- 分层(Layering) 解决了镜像的存储和分发问题。通过把镜像拆成多层,实现了资源的共享和复用。
- 写时复制(CoW) 解决了容器运行时的读写效率问题。它保证了镜像的不可变性,同时允许容器在不污染镜像的前提下,快速、轻量地进行读写操作。