
本篇摘要
本文深入剖析Docker镜像的分层存储机制与UnionFS联合文件系统工作原理,详解Overlay2驱动的写时复制与分层叠加特性,并通过实战演示卷挂载的底层bind mount实现及commit操作对卷数据的特殊处理逻辑。
一.Docker 镜像原理
操作系统
-
操作系统核心组成
操作系统由七大子系统构成:
- 进程调度、进程通信、内存管理、设备管理、文件管理、网络通信、作业控制
-
Linux文件系统两层结构
Linux的文件管理子系统由
bootfs和rootfs两部分组成:

bootfs (引导文件系统):
- 包含 :
bootloader(引导加载程序)和kernel(内核)。 - 作用 :系统启动时加载,用于引导内核。内核加载入内存后,
bootfs即被卸载。 - 特点:所有 Linux 系统(包括 Docker 镜像底层)都共享这一层。
rootfs (根文件系统):
- 包含 :
/dev,/proc,/bin,/etc等标准目录和文件 - 作用:定义了不同操作系统发行版的独特环境和用户空间。
- 特点:不同的发行版(如 Ubuntu、CentOS)在此层体现差异。
-
与Docker的关联
Docker 镜像的底层原理借鉴了这种分层结构:
- 所有镜像最底层都是相同的
bootfs。 - 不同的镜像(如 Ubuntu、CentOS)之间的差异体现在其
rootfs层。 - 这种共享基础层、差异化上层的模式,是 Docker 实现镜像轻量化和高效存储的理论基础。
- 所有镜像最底层都是相同的
一句话:Linux 通过 bootfs(内核层)和 rootfs(发行版层)的分离来组织文件系统,这正是 Docker 镜像分层与共享机制的底层设计原理。
再认识下docker镜像

- 联合文件系统 (Union FS) 核心机制

- 分层叠加:将多个物理位置分开的目录(只读或可写)联合挂载,呈现为一个统一的虚拟文件系统。
- 写时复制 (Copy-on-Write, CoW) :这是关键特性。对只读层的修改不会直接覆盖原文件,而是
复制一份到可写层进行修改,确保底层原始文件不被破坏,从而实现高效共享和快速创建容器。
- Docker 镜像本质
- 镜像就是一个分层、只读的 Union FS 文件系统。每一层(Layer)代表一次构建指令(如安装软件、复制文件)所产生文件变更的集合。
- Docker 容器本质
- 容器 = 镜像(只读层) + 一个顶部的可写层。容器运行时,Docker 通过 Union FS 将镜像的所有只读层与容器的可写层叠加在一起,为容器提供一个完整的文件系统视图。
- 分层结构的优势
- 共享资源,减少冗余:所有镜像共享相同的基础层(如 base image),极大节省了存储空间和网络带宽。
- 加速构建与部署:构建新镜像或启动新容器时,只需处理变化的层,无需复制整个文件系统。
- Docker 与宿主机的关系
- 共享内核:所有容器共享宿主机的 Linux Kernel,这使得容器极其轻量。
- 独立 rootfs:每个容器拥有自己通过 Union FS 组装起来的用户空间文件系统(rootfs),因此可以运行不同的 Linux 发行版(如 Ubuntu、CentOS)(也就是只用宿主机内核,其他比如系统等自己都在上层打包好了)。
一句话:Docker 利用 Union FS 的写时复制和分层叠加能力,将多个只读层和一个可写层组合成一个单一的文件系统视图,实现了镜像的轻量化、快速分发和容器的隔离性。
docker分层储存实现原理
Docker 用联合文件系统把镜像分成很多层;不同 Linux 系统用的技术不一样:
- CentOS 用
overlay2(推荐,稳定) - Debian 用
aufs - RedHat 用
devicemapper
用 docker info 就能看你系统用的是哪一种:

-
联合文件系统 (Union FS)
Docker 使用 Union FS (如
overlay2)将多个只读的镜像层 和一个可写的容器层叠加,呈现为一个完整的文件系统。

写时复制 (Copy-on-Write, CoW):
- 容器运行时,若需修改底层镜像的文件,会先将该文件复制到顶部的可写层再进行修改。这保证了镜像层的只读性,实现了高效共享和资源隔离。
如何操作:
- 比如容器要删除某个东西;就只在自己的可写层对应文件映射位置标记without;让自己看不见就行;如果是修改等操作也是这样;每次操作就相当于从上往下进行的覆盖操作(修改镜像其实就是对这些层的位置进行从上面添加也是类似覆盖效果;比如dockefile那些操作)。
- overlay2(ufs一种;docker用的) 驱动的工作流程

- 读操作 :文件在容器层 (
upperdir) 则直接读;不在则从镜像层 (lowerdir) 读取。 - 写操作 :首次修改文件会触发
copy_up,将文件从镜像层复制到容器层后再修改。 - 删操作 :删除文件时,仅在容器层创建一个
whiteout标记来遮挡镜像层的文件,并非真正删除。
- 分层存储的优势
- 共享资源:所有镜像和容器共享相同的底层只读层,极大节省了存储和网络带宽。
- 快速部署:创建新容器时无需复制整个文件系统,只需添加一个薄的可写层。
-
一个关键注意事项
使用
docker commit提交容器创建新镜像时,会将容器层的所有内容(包括数据)打包成一个新的只读层 。因此,在容器内删除文件并不会减少镜像体积,反而可能因whiteout标记的存在导致镜像越来越大。
一句话:Docker 通过 Union FS 的写时复制和分层叠加机制,实现了镜像的轻量化、快速分发和容器的隔离性。
docker镜像加载原理

-
最底层:内核 (Kernel) 所有容器都共用电脑(宿主机)的同一个操作系统内核,所以它超级轻量。
-
中间层:镜像 (Image Layers) 像千层饼一样,每一层都是一份只读的文件(比如一层是操作系统、一层是软件A、一层是软件B)。这些层可以被多个镜像和容器共享,省空间。
-
最顶层:容器层 (Container Layer) 当你要运行一个程序(容器)时,就在这个只读的千层饼上,临时加一层可写的"奶油"。你所有的操作都在这层"奶油"上进行,不会破坏下面的饼。
一句话: Docker 把应用和它的环境打包成一个只读的千层饼(镜像) ,运行时在上面抹一层可写的奶油(容器)。这样既保证了环境一致,又做到了相互隔离。
镜像分层存储实战
tree命令
-
命令功能
tree命令用于以树状图形式递归列出指定目录下的所有文件和子目录,直观展示目录结构。 -
安装方法
- Ubuntu/Debian 系统 :使用命令
apt install tree -y - CentOS/RHEL 系统 :使用命令
yum install tree -y
- 常用参数
-
-a显示所有文件和目录(包括以点.开头的隐藏文件)。 -
-d只显示目录名称,不显示目录下的具体内容。 -
-D列出文件或目录的最后更改时间。 -
-f在每个文件或目录名前,显示其完整的相对路径。 -
-i不以阶梯状的缩进形式列出名称。 -
-L level限制显示目录的层级深度(例如-L 2只显示两层)。 -
-l如果遇到符号链接(软连接)的目录,直接列出它指向的原始目录。 -
-P<范本样式>只显示符合给定范本样式(通配符模式)的文件或目录名(例如-P "*.txt")。
- 基本语法
tree [选项参数] [目录路径]不指定目录路径时,默认显示当前目录的树状结构。
总结:tree 是一个用于快速可视化目录结构的实用工具,通过简单参数即可定制输出内容。
深入了解镜像的overlay2层状联合文件系统
下面以nginx为例来认识下:

找到对应的Data位置:
下面来解释下各个字段说明:
-
LowerDir(底层只读层) : 这是容器的"基础"。它是一系列只读的镜像层 (就像一堆摞在一起的透明幻灯片),包含了操作系统、安装的软件等。所有容器共享这些层,节省空间(分多层,下面来认识下)。 -
UpperDir(上层可写层) : 这是容器的"草稿纸"。它是一个可读写的层,你在容器里创建新文件、修改或删除现有文件的所有操作,都只发生在这里。这是每个容器独有的。 -
MergedDir(合并视图层) : 这是容器最终"看到的"统一文件系统。Docker 把下面的LowerDir(只读)和上面的UpperDir(可写)叠加合并到一起,呈现出一个完整的目录给你用(只有整体镜像的编码才具有这个merged目录;否则没有意义)。 -
WorkDir(工作目录) : 这是 Docker 内部用来准备和管理文件合并的"工作区",用户一般不直接关心。
一句话: ==一堆共享的只读基础件(LowerDir) + 一个私有的可写改动层(UpperDir) = 你最终看到的完整文件系统(MergedDir)==。这就是容器既轻量又能被随意修改的秘密。
下面我们进入这个镜像的整体层编码对应目录里:

- 这里的
committed表明以这个镜像为基础创建的容器已经被提交成一个新的镜像了。 - 这里看到的diff就是当容器进行文件等修改操作;就会记录在里面(最上层的diff记录也就是容器层)。
- link是一个软链接;表明当前处于哪一层。
- lower表示当前层底下还有多少层(以软链接形式出现)。
- work就是对文件管理的区域(无需关心)。
查看整体镜像编号的当前层:

- 在整体overlay2目录有个l目录专门放着对应软链接编号,直接ll查看就行。
下面看下在整体镜像下还有多少层:

- 这里看到对应它下面还有五层。
下面再去看它的下一层31b70d3dabbf6cbbd3947266445111cee0794f599120335c6e06c0f6954a840e看看:

- 发现这里和之前一样;含义也是相同的;只不过是以此前的这层看起了(committed:当前这层曾经被修改然后提交镜像了;diff:就是之前这层在上面的时候作为可修改层的时候被修改内容的记录)。
这层下面还有四层如下:

下面我们tree下对应的diff:

- 可以看到很多被修改的内容;其实就是此前这层在最上面被修改(比如
commit dockerfile的时候)然后变成镜像就会被记录这些操作(在dockefile进行编写的时候是按照一定规则把对应的指令集合构成一层的;不是随机的)。
如下:

- 一批指令按照一定规则构成对应的不同的镜像层。
到了这里发现其他层都没显示对应merged这个目录;只有整体镜像最上层(不是容器层;只有创建容器才会出现容器层;这里还可以理解成是整个镜像的所有层的编号)才有这个merged(但是是空,只有标记):

- 发现不存在这个目录;因为这是
镜像所有层和容器层合并(整体向上映射后)后用户看到的结果;创建个容器得到对应的容器层编号;此时里面就会记录对应merged了。
下面可以inspect容器看到:

- 这里在上面又盖了一层。

- 就相当于从此时的站在容器这个层往下看到的文件系统(rootfs)。

- 和容器里面看到的是一样的(容器里面看到的ufs也就是站在自己的容器层向下看而看到的)。
小结下:
- 对应overlay2结构就是多层精选层(每层都有编号);通过inspect可以看到有个整体镜像层的编号(最上层的编号);每层就是一个目录:保存这自己这层的软连接;下面还有多少层的软连接;以及曾经被修改的记录;是否被提交成镜像过;以及文件工作区等等。
- 下面如果拿着这个镜像搞一个容器;就会在这个镜像整体层上面搞一个新的容器层(层数+1);然后此时就会出现对应的merged目录(所有镜像层和容器层叠加看到的比如容器总目录);此时如果再次在容器中修改就会在本层diff中记录;如果把它提交成新的镜像的话;此时这层即编号就会成为新的镜像的中间层;然后还会给这个整体镜像层(也就是曾经容器的最上层)起一个新的编号;最后构成新的镜像。
==下面博主总结了一张图来更清楚理解对应关系;如下:==

化抽象为具体理解镜像操作底层原理!
overlay 文件系统工作实战
下面模拟下一个目录挂载成overlay文件系统。看下操作流程:

- 分别为对应的上层(当前层);底层(下面的所有层);以及合并后(也就是用户看到的总的目录层;用户进行操作的);还有一个工作区目录-->用着些来模拟。

- 这里在不同目录搞一些文件来模拟后面瓜挂载后出现的效果;方便观看。

- 这里指明要挂载的文件系统是overlay;然后挂载后的这个文件系统设备名字也是overlay;后面的参数就是说明这个文件系统的哪些层;工作目录;合并后(用户实际看到进行操作的目录)目录等是那些来形成。

- 这里可以看出对应的merged的both是来自上层;而其他的都是来自对应层(因为上层 本层把下层映射上了的遮挡住了)。

- 下面修改用户看到的merged处的low文件;以为它是下层映射上来的;修改后low层对应文件也会改变;但是发现并没有改变;而是在上层出现了修改的对应文件。
解释下:
这里虽然修改的对应的low文件是下层映射上来的;但是由于overlay结构决定;下层是不能修改的;如果要修改就需要拷贝下层对应文件到上层来修改;改后就相当于遮挡住了;因此用户的merged看到的对应的文件就是上层修改的了(符合overlay系统特性)。

- 这里对基于overlay的用户所见的merged进行文件删除操作;删除的是在上层修改的也就是存在上层的;发现对应的merged确实没了;然后上层只标记了一个删除标记(如c;类似docker镜像那里的witheout操作;告诉merged;这个上层的文件不能再次出现用户可见了)。

- 这里发现删除对应下层的文件却能删除;因为这个overlay文件系统的挂载是模拟出来的(
本质还是目录,只是具有了文件系统的特征);也就是真正的用户看到的是merged处的文件;其他文件都是看不见的(这里就假装看不见;而真正的overlay文件系统出来就是看不见的);模拟成真的overlay就要求我们实际只能操作merged里的文件;因此在正在的overlay文件系统这个操作是不存在的。
二.docker 卷原理
理论基础
-
挂载时机
Docker 在容器进程启动后、执行
chroot(切换根目录)锁定文件系统之前,将宿主机目录挂载到容器内。 -
镜像层准备
容器镜像的
各层文件存储在/var/lib/docker/overlay2/{layer-id}/diff目录下,通过联合挂载技术合并到/var/lib/docker/{layer-id}/merged/目录,形成容器所需的完整rootfs。 -
卷挂载本质
实质是将宿主机指定目录(如
/home)直接挂载到容器目录在宿主机上的对应路径(如/var/lib/docker/aufs/mnt/[可读写层ID]/test)。 -
隔离性保证
挂载操作发生在容器进程的 Mount Namespace 启用后,因此该挂载点仅在容器内部可见,宿主机无法感知,完美保持了容器的隔离特性(这里由于挂载是容器里发生的;宿主机只是知道有这么个挂载即对应的目录共享;但是具体如何挂载,挂载点之类的都是不知道的;因为它是在容器内操作的;有隔离)。
总结:Docker 通过巧妙的挂载时机(chroot前)和Namespace技术,实现了宿主机目录到容器的透明挂载,同时严格保证了容器的隔离性。
实战操作
Linux mount bind

-
核心功能 通过
mount --bind命令将一个目录(源目录)挂载到另一个目录(目标目录),实现目录间的关联映射。 -
访问机制 所有对目标目录 的访问操作,都会被透明地重定向到源目录 ,实际访问和修改的都是源目录的内容(
实现及时同步效果,操作也同步;也就是可以理解成对俩目录都操作一遍)。 -
技术本质 其功能可以理解为目录级别的"硬链接",但它是在文件系统挂载层面实现的,而非 inode 链接。
-
命令格式
mount --bind <源目录> <目标目录>
一句话:mount --bind 命令用于将两个目录绑定,使对目标目录的操作完全映射到源目录上,实现目录的透明访问。(可以理解成实现同步性拷贝一份源目录覆盖到目标目录上,这里如果移除对应的目的目录挂载就相当于把上面的这层源目录拿去了)
下面演示下:

- 这里data1作为源目录挂载到data2上。

- 发现成功同步。

- 修改目的目录;源目录也会同步。

- 取消data2上面的挂载相当于移除上面的data1。
查看 Docker 联合挂载
首先启动对应容器:


- 发现这里对应的merged处出现一个overlay2文件系统的挂载(其实就是镜像的下层 上层 工作层 等合并一起挂载在merged上面;形成了对应容器看到的目录(底层类似调用了
mount --bind))。
下面看下mount这里验证下:

- 这里可以发现对应的merged也是一个挂载;然后它是overlay类型挂载;把对应的上面说的那几个层挂到merged上面。
Docker 卷深度思考之commit操作对卷的影响
首先这里要知道:
-
若涉及"宿主机路径绑定"(显式 -v 宿主机路径:/test)或"匿名卷"(Docker 自动在宿主机建卷),宿主机能通过对应路径看到内容(也可以是临时卷 绑定卷等;这些都是
mount --bind通过特殊处理了)。 -
若完全是
容器内自己 bind mount(没暴露宿主机路径),且 Mount Namespace 隔离生效,宿主机默认看不到,除非主动进入容器命名空间查看。 -
对于 OverlayFS 可写层,若没有额外 bind mount 覆盖,宿主机能直接看到 upperdir 里的修改;若有 bind mount 覆盖,宿主机"原生视角"下会看不到(得进容器命名空间看)。
==总之:==
也就是说通过docker提供的卷的挂载模式就是mount bind特殊处理的一种挂载(如把对应容器目录的inode指向变成对应宿主机源目录位置);此时宿主机是能看到对应容器的挂载信息以及目录内容等的,也就是可以同步的;但是如果是在容器中进行mount bind就没有这种特殊处理了;自然就是被namespace隔离的了。
下面测试下commit对docker卷的影响再得出结论:

- 首先进行容器创建。

- 可以发现对应目录管理卷创建成功完成挂载;宿主机是可以看见挂载信息以及挂载内容;这里就是被处理过了(mount bind) 呈现出的可看见效果(宿主机容器都能看见对方操作)。

- 这里进行把对应的容器(之前挂载的管理卷里面被写入内容了;容器能看到)提交成镜像;再次创建容器发现没有内容了;是个空目录。
下面解释下:
- 因为之前的docker挂载是被处理过的mount bind ;理论宿主机是可见的;而commit又是宿主机操作的;但是这个目录却以空的被提交了(说白了是commit的这个机制故意让宿主机这么做的:内容不搬;只搬目录)。
对比下:
| 特性 | 运行时 (Runtime) | 构建时 (Build/Commit) |
|---|---|---|
| 视角 | 宿主机和容器 | Docker 引擎 |
| 能看到数据吗? | 能! 数据在宿主机卷里,双方都可读写。 | "看不到"! 这里"看不到"是拟人化 的说法,实际是Docker故意不打包它,内容不打包只搞对应目录过去。 |
| 机制 | 绑定挂载 (-v) 让宿主机目录和容器目录变成同一个。 |
docker commit 命令的设计原则 就是排除所有卷中的数据。 |
| 目的 | 数据共享与持久化 | 创建纯净、可复现的应用镜像 |
通俗理解下:
想象一下,有一个U盘(宿主机上的卷 )和一台电脑(Docker镜像)。
-
运行时(电脑插着U盘):
- 您把U盘插到电脑上,电脑系统(容器 )就能看到并读取U盘里的文件。
- 这就相当于
-v绑定后,容器和宿主机都能看到数据。
-
构建时(制作电脑系统镜像):
- 现在想给这台电脑的整个系统做一个"快照"(
docker commit),把这个快照做成一个系统镜像文件,拿去给别人重装系统。 - 在制作这个"快照"时,会把U盘拔掉 !因为这个快照是电脑系统本身,不应该包含您U盘里的个人数据。
- 这就相当于
docker commit时,Docker 故意不把卷里的数据打包进新镜像。 这样得到的镜像很纯净,下次装系统时,插上U盘,数据又回来了。
- 现在想给这台电脑的整个系统做一个"快照"(
一句话: ==-v管运行时共享,commit管构建时纯净,两者分工明确,互不干涉。==
上面提到了很多次对应docker的卷操作可以理解成对mount bind的一种特化处理;比如可以从下面两点看出:对应的mount --bind 源 目标(挂载点);之前看到宿主机有东西进行挂载后是宿主机同步到容器;对应mysql的话进行绑定挂载可以发现是容器同步宿主机;这就是docker在进行特化的时候对应的mount --bind 后面传递的两个参数位置影响的。
下面再讲一下对应的挂载:
首先就是通过docker 进行挂载如管理卷 绑定卷 临时卷;这些就是特殊处理的挂载;此时宿主机和对应容器共享对应目录(可以宿主机对容器也可以是容器对宿主机(mysql容器);但是一般都是宿主机对容器挂载)。
再就是容器内是无法挂载到容器外的宿主机的无论是docker提供还是mount --bind(因为本身就有namespace隔离)。
下面就是对应的mount --bind操作:
- 执行位置决定可见性
- 宿主机执行
mount --bind:无隔离,全局可见(所有容器和宿主机都受影响,都是宿主机自己的目录;因为宿主机能看到对应目录比如可以挂载对应镜像的某些层的文件;那么启动容器就会看到对应挂载;相当于直接重定向inode了;如果不是对应镜像层文件的话;此时宿主机通过docker cp 对容器操作容器就可以看到对应挂载后的文件)。 - 容器内执行
mount --bind:受隔离 ,仅当前容器可见(宿主机和其他容器看不到;因为namespace隔离宿主机不知道容器挂载了啥;只会按照未被挂载之前的文件进行处理;也就是如果俩目录挂载了;容器进行相关操作了;容器内部可以看到的;但是宿主机不会看到对应变化;宿主机保持原挂载前状态)。
-
根本原因
隔离性由 Mount Namespace 提供,与
mount命令本身无关。Docker 为每个容器创建了独立的 Mount Namespace。 -
关键区别
- 在宿主机操作:挂载点出现在全局命名空间。
- 在容器内操作:挂载点仅出现在该容器的私有命名空间。
三.本篇小结
Docker镜像是通过UnionFS将多个只读层叠加构建,Overlay2驱动实现写时复制确保高效共享。容器运行时添加可写层形成完整视图。卷挂载本质是宿主机目录bind mount至容器空间,commit时数据不打包入镜像是为保持镜像纯净性。内核共享与rootfs独立成就了容器轻量化与隔离性。