
文章目录本篇摘要一.Docker 镜像原理操作系统再认识下docker镜像docker分层储存实现原理docker镜像加载原理镜像分层存储实战tree命令深入了解镜像的overlay2层状联合文件系统overlay 文件系统工作实战二.docker 卷原理理论基础实战操作Linux mount bind查看 Docker 联合挂载Docker 卷深度思考之commit操作对卷的影响三.本篇小结本篇摘要本文深入剖析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用aufsRedHat用devicemapper用docker info就能看你系统用的是哪一种联合文件系统 (Union FS)Docker 使用Union FS如overlay2将多个只读的镜像层和一个可写的容器层叠加呈现为一个完整的文件系统。写时复制 (Copy-on-Write, CoW)容器运行时若需修改底层镜像的文件会先将该文件复制到顶部的可写层再进行修改。这保证了镜像层的只读性实现了高效共享和资源隔离。如何操作比如容器要删除某个东西就只在自己的可写层对应文件映射位置标记without让自己看不见就行如果是修改等操作也是这样每次操作就相当于从上往下进行的覆盖操作修改镜像其实就是对这些层的位置进行从上面添加也是类似覆盖效果比如dockefile那些操作。overlay2ufs一种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 -yCentOS/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独立成就了容器轻量化与隔离性。