跳到主要内容 Docker 镜像原理与分层存储机制详解 | 极客日志
Shell / Bash
Docker 镜像原理与分层存储机制详解 Docker 镜像基于联合文件系统(UnionFS)实现分层存储,包含只读层与可写层。通过写时复制(CoW)机制确保镜像不可变性与存储效率。实战演示了使用 docker history 和 inspect 命令分析镜像层结构,以及手动配置 overlay 文件系统验证 merged、upper、lower 目录的文件读写与删除逻辑。
战神 发布于 2026/3/15 更新于 2026/4/18 2 浏览Docker 是操作系统层的虚拟化,所以 Docker 镜像的本质是在模拟操作系统。
联合文件系统(UnionFS)
联合文件系统(UnionFS) 是 Docker 镜像实现分层存储的核心技术,它通过将多个只读层(Image Layers)和一个可写层(Container Layer)叠加,形成一个虚拟的、统一的文件系统。
核心概念
分层存储 :
Docker 镜像由多个只读层组成,每一层对应一个构建步骤(如 RUN、COPY 指令)。
这些层通过 UnionFS 叠加在一起,形成一个逻辑上的完整文件系统。
只读层与可写层 :
只读层:镜像的每一层都是只读的,确保了镜像的一致性和不可变性。
可写层:当容器启动时,Docker 会在镜像的只读层之上添加一个可写的容器层,用于记录容器的文件修改。
共享与复用 :
多个容器可以共享同一个镜像的只读层,节省存储空间。
不同容器之间的可写层相互隔离,互不影响。
工作原理
文件系统的叠加 :
UnionFS 将多个只读层和一个可写层按照顺序叠加,形成一个统一的视图。
当访问文件时,UnionFS 会从顶层开始查找,直到找到目标文件为止。
写时复制(Copy-on-Write, CoW) :
如果容器需要修改只读层中的文件,UnionFS 会先在可写层中创建一个该文件的副本,然后对副本进行修改。
这种机制避免了直接修改只读层,确保了镜像的不可变性。
透明性 :
对于用户来说,UnionFS 提供的文件系统视图是透明的,用户无需关心文件的实际存储位置。
实现方式
常见实现 :
AUFS:早期 Docker 使用的 UnionFS 实现,但已逐渐被其他实现取代。
OverlayFS:现代 Linux 内核中广泛使用的 UnionFS 实现,性能更优。
Btrfs、ZFS:其他支持 UnionFS 的文件系统,但使用较少。
OverlayFS 的分层结构 :
LowerDir:镜像的只读层,由多个目录组成。
UpperDir:容器的可写层。
WorkDir:用于存储 OverlayFS 的临时数据。
MergedDir:最终呈现给容器的统一文件系统视图。
在 Docker 中的应用场景
镜像构建 :
Docker 通过 UnionFS 将多个构建步骤的层叠加,形成一个完整的镜像。
每个层都可以被其他镜像共享,减少了存储空间的占用。
容器运行 :
容器启动时,Docker 会在镜像的只读层之上添加一个可写层,用于记录容器的文件修改。
这种设计使得容器可以快速启动,并且多个容器可以共享同一个镜像。
镜像分发 :
由于镜像的层是共享的,Docker 在分发镜像时只需要传输新增的层,大大减少了网络带宽的占用。
UnionFS 的优势
存储效率 :
通过共享层,减少了存储空间的占用。
写时复制机制避免了重复的数据复制。
性能优化 :
OverlayFS 等现代 UnionFS 实现具有较高的性能,能够满足容器化应用的需求。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
Docker 分层存储机制 Docker 分层存储机制是 Docker 镜像构建与运行的核心技术,通过将镜像和容器的数据存储在多个独立的层中,实现了高效、灵活的镜像管理和容器运行。
基本概念
Docker 镜像由多个只读层组成,每一层对应一个构建步骤(如 RUN、COPY、ADD 指令)。
这些层通过 联合文件系统(UnionFS) 技术叠加,形成一个逻辑上的完整文件系统。
每个层只存储与前一层相比的增量变化,避免重复数据存储。
当容器启动时,Docker 会在镜像的只读层之上添加一个可写的容器层。
容器运行时的所有修改(如文件创建、修改、删除)都记录在容器层中,不会影响镜像层。
工作原理
构建镜像时,每执行一条 Dockerfile 指令,都会生成一个新的镜像层。
例如,执行 RUN apt-get update && apt-get install -y nginx 会在基础镜像层之上新增一层,记录安装的 Nginx 软件包。
容器启动时,Docker 会将镜像的只读层与容器层联合挂载,形成一个可读写的文件系统视图。
容器层是临时的,当容器停止或删除时,容器层的修改会丢失(除非通过卷或绑定挂载持久化数据)。
当容器需要修改只读层中的文件时,Docker 会将该文件复制到容器层,然后在容器层中进行修改。
这种机制保证了镜像层的不可变性,同时提高了容器启动速度。
分层存储的优势
多个容器可以共享同一个镜像的只读层,减少存储空间占用。
例如,多个基于同一基础镜像构建的应用镜像可以共享基础镜像层。
镜像构建时,Docker 会利用缓存机制。如果某一层的内容没有变化,Docker 会直接使用缓存的层,而不需要重新构建。
镜像的分层存储使得镜像的传输和存储更加高效。
每一层的变化都可以被追踪,开发者可以轻松地回滚到之前的版本,或者在不同版本之间切换。
由于容器启动时只需添加一个轻量级的可写层,而不是重新创建整个文件系统,因此容器启动速度非常快。
实现细节
每个镜像层都有一个唯一的标识符,用于在不同的镜像之间共享。
Docker 支持多种存储驱动,如 AUFS、OverlayFS、Device Mapper 等,这些驱动都实现了分层存储的机制。
例如,OverlayFS 是现代 Linux 系统中广泛使用的存储驱动,性能更优。
每一层包含了文件系统的变化,例如添加、删除或修改的文件。这些变化以增量方式存储,只有发生变化的部分会被存储。
镜像分层存储实战 通过 docker image history 查看如下
root@VM-8-12-ubuntu:/data/ahri# docker history nginx:1.21.1
IMAGE CREATED CREATED BY SIZE COMMENT
822b7ec2aaf2 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c set -x && addgroup --system -… 63.9MB
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
<missing> 3 years ago /bin/sh -c
可以看到 dockerfile 和做出来的镜像是对应的,而且不是每一层都占用空间的。
我们再通过 inspect 命令查看该镜像的存储位置。
root@VM-8-12-ubuntu:/data/ahri# docker image inspect nginx:1.21.1
[
{
"GraphDriver" : {
"Data" : {
"LowerDir" : "/data/var/lib/docker/overlay2/ec2f5f43a9a6f4e7063fb6ef633103b1bca417f34488a4a48736758f9eb6019f/diff:/data/var/lib/docker/overlay2/e368569b4eb5117dabbb84864913883ecf8f50130097619ce128a7bcdf141092/diff:..." ,
"MergedDir" : "/data/var/lib/docker/overlay2/b35408cd1036fd7aa5d4fb5220f06fe9a56606fb2ad1316a8beca766c886e692/merged" ,
"UpperDir" : "/data/var/lib/docker/overlay2/b35408cd1036fd7aa5d4fb5220f06fe9a56606fb2ad1316a8beca766c886e692/diff" ,
"WorkDir" : "/data/var/lib/docker/overlay2/b35408cd1036fd7aa5d4fb5220f06fe9a56606fb2ad1316a8beca766c886e692/work"
},
"Name" : "overlay2"
},
"RootFS" : { "Type" : "layers" , "Layers" : [...] },
"Metadata" : { "LastTagTime" : "0001-01-01T00:00:00Z" }
}
]
可以看到 GraphDriver 也就是我们的存储驱动,是 overlay2 的存储驱动;nginx 的 overlay2 的四个目录也都显示出来了。我们知道 Docker 的默认目录是 /var/lib/docker,之所以在 /data/var/lib/docker 下面是因为我们规划了磁盘,调整了默认的存储目录。
可以看到 lowerdir,upperdir,都位于 /data/var/lib/docker/overlay2 下面,因为我们调整过默认存储位置所以对比默认的 /var/lib/docker 多了 /data。我们进入到
cd /data/var/lib/docker/overlay2
该目录下,查找我们的 nginx,看下文件的怎么存储的
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# tree -P nginx -f | grep "/sbin/nginx"
./0b85066e21c3238a8d0214b82399b41b6fedfbebe48ba88ab45d0947089685/diff/usr/sbin/nginx
./db3ef3edafc0f2fe808eb68b6f53aec88b9c2e6fd525bb7e7cf9bbfec464992b/diff/usr/sbin/nginx
...
-P nginx 选项表示只显示匹配模式 nginx 的文件或目录名。从当前目录开始,递归地列出所有匹配 nginx 的文件或目录(通过 tree -P nginx -f)。然后,通过 grep 进一步过滤,只显示路径中包含 /sbin/nginx 的行。
搜索后可以看到我们找到了多个 nginx 文件,因为我们本地有多个 nginx 镜像所以搜到了多个 nginx 文件,通过 lowerdir 的值我们可以确定有一个是和我们 nginx:1.12.1 的匹配上的。
同样的方式我们通过 Dockerfile 发现,nginx 还存储了个 docker-entrypoint.sh,我们搜索这个文件,我们发现这个文件也被放到了 diff 目录下面,和我们的 lowerdir 中一个 layer 是对应的。
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# tree -P "docker-entrypoint.sh" -f | grep "docker-entrypoint.sh"
./2b130b497b862c6acff53f31ebdbc5ecf772345c2b9ce4326f620bcef741507d/diff/usr/local/bin/docker-entrypoint.sh
./4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1/diff/docker-entrypoint.sh
...
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# cd 4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2/4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1# ls committed diff link lower work
可以看到 link 文件,里面是每一个 diff 目录的短名称,或者说软链接
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2/4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1# cat ./link
J6U3CHDJ5RRYP6ONH2VJLP34P2
通过遍历 l 目录我们会发现,整个 Docker 的镜像的 diff 目录都被做了对应的软链接,或者说起了个短名称。
每一个 diff 是一个层级的内容,层级的关系是存放到了 lower 文件中,里面存放着父级的层级。
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# cd 4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2/4d5612682090b9800340353f0550207d69d15ecf7135353cf09a8c8db252afb1# cat lower
l/HUIS7U7MEMOI47VSVORK45VAB4:l/BDTHLGWDJQYNWQQP4AH2NAT3BE
最后我们查看下 mergeddir,发现 merged dir 是不存在的,当我们启动为容器的时候才是有有效的。
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# ll b35408cd1036fd7aa5d4fb5220f06fe9a56606fb2ad1316a8beca766c886e692/merged
ls : cannot access 'b35408cd1036fd7aa5d4fb5220f06fe9a56606fb2ad1316a8beca766c886e692/merged' : No such file or directory
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# docker run -d --name mylayer nginx:1.21.1 f1a5126c83a4a010a115b3afb340618302e68834ba95b7e6b3afe4c60e3432ea
root@VM-8-12-ubuntu:/data/var/lib/docker/overlay2# docker inspect mylayer
[
{
"Id" : "f1a5126c83a4a010a115b3afb340618302e68834ba95b7e6b3afe4c60e3432ea" ,
"Created" : "2025-04-15T12:33:55.683157888Z" ,
"Path" : "/docker-entrypoint.sh" ,
"Args" : ["nginx" , "-g" , "daemon off;" ],
"State" : { "Status" : "running" , "Running" : true , ... },
"Image" : "sha256:822b7ec2aaf2..." ,
"GraphDriver" : { "Data" : { "LowerDir" : "..." , "MergedDir" : "..." , "UpperDir" : "..." , "WorkDir" : "..." }, "Name" : "overlay2" },
...
}
]
我们通过镜像实际存储位置可以看到镜像在存储的时候,通过分层来实现,并通过 link 和 lower 完成层与层之间链接关系配置,diff 存放了我们的内容,并且没有什么加密。
overlay 文件系统工作实战 mkdir -p /data/myworkdir/fs
root@VM-8-12-ubuntu:/data# cd /data/myworkdir/fs
root@VM-8-12-ubuntu:/data/myworkdir/fs# ls
root@VM-8-12-ubuntu:/data/myworkdir/fs# mkdir upper lower merged work
root@VM-8-12-ubuntu:/data/myworkdir/fs# echo "in lower" > lower/in_lower.txt
root@VM-8-12-ubuntu:/data/myworkdir/fs# echo "in upper" > upper/in_upper.txt
root@VM-8-12-ubuntu:/data/myworkdir/fs# echo "In both. from lower" > lower/in_both.txt
root@VM-8-12-ubuntu:/data/myworkdir/fs# echo "In both. from upper" > upper/in_both.txt
mount -t overlay overlay -o lowerdir=./lower,upperdir=./upper,workdir=./work ./merged
root@VM-8-12-ubuntu:/data/myworkdir/fs# df -h
Filesystem Size Used Avail Use% Mounted on
...
overlay 50G 29G 19G 61% /data/myworkdir/fs/merged
此时看下目录结构,然后发现 merged 目录自动生成了 3 个文件,可以看到 both, lower, upper 都在。merged 目录其实就是用户看到的目录,用户的实际文件操作在这里进行。
root@VM-8-12-ubuntu:/data/myworkdir/fs# tree -a .
.
├── lower
│ ├── in_both.txt
│ └── in_lower.txt
├── merged
│ ├── in_both.txt
│ ├── in_lower.txt
│ └── in_upper.txt
├── upper
│ ├── in_both.txt
│ └── in_upper.txt
└── work
└── work
5 directories, 8 files
merged 目录下编辑一下 in_lower.txt,upper 目录下就会马上出现一个 in_lower.txt,而且内容就是编辑后的。而 lower 目录下的 in_lower.txt 内容不变
root@VM-8-12-ubuntu:/data/myworkdir/fs/merged# vi in_lower.txt
root@VM-8-12-ubuntu:/data/myworkdir/fs/merged# cat in_lower.txt
in lower! after edit!
root@VM-8-12-ubuntu:/data/myworkdir/fs/merged# cd ..
root@VM-8-12-ubuntu:/data/myworkdir/fs# tree -a .
...
root@VM-8-12-ubuntu:/data/myworkdir/fs# cat upper/in_lower.txt
in lower! after edit!
如果我们删除 in_lower.txt,lower 目录里的 in_lower.txt 文件不会有变化,只是在 upper/ 目录中增加了一个特殊文件来告诉 OverlayFS,in_lower.txt 这个文件不能出现在 merged/ 里了,类似 AuFS 的 whiteout
root@VM-8-12-ubuntu:/data/myworkdir/fs# rm -f merged/in_lower.txt
root@VM-8-12-ubuntu:/data/myworkdir/fs# tree -a .
...
root@VM-8-12-ubuntu:/data/myworkdir/fs# ll upper/
total 16
drwxr-xr-x 2 root root 4096 Apr 15 20:49 ./
drwxr-xr-x 6 root root 4096 Apr 15 20:39 ../
-rw-r--r-- 1 root root 20 Apr 15 20:40 in_both.txt
c--------- 1 root root 0, 0 Apr 15 20:49 in_lower.txt
-rw-r--r-- 1 root root 9 Apr 15 20:39 in_upper.txt
注意到 upper 下 in_lower.txt 的文件类型没有 -,而是 c 不是 - 或者 d。
可以看到这种文件系统对于底层来说不影响,共享比较容易,但是如果编辑,删除频繁的话,性能还是比较差的。要不停的拷贝或者标记。