Docker 镜像的元数据
repository元数据
repository在本地的持久化文件存放于/var/lib/docker/image/overlay2/repositories.json中
[root@service-1 overlay2]# cat repositories.json | python -mjson.tool { "Repositories": { "192.168.10.31/library/nginx": { "192.168.10.31/library/nginx:v1": "sha256:2bcb04bdb83f7c5dc30f0edaca1609a716bda1c7d2244d4f5fbbdfef33da366c", "192.168.10.31/library/nginx@sha256:dabecc7dece2fff98fb00add2f0b525b7cd4a2cacddcc27ea4a15a7922ea47ea": "sha256:2bcb04bdb83f7c5dc30f0edaca1609a716bda1c7d2244d4f5fbbdfef33da366c" }, } }
文件中存储了所有repository的名字(如192.168.10.31/library/nginx),每个repository下所有的版本镜像的名字和tag(如192.168.10.31/library/nginx:v1)以及对应的镜像ID。而referenceStore的作用便是解析不同格式的repository名字,并且管理repository与镜像ID的映射关系
image元数据
image元数据包括了镜像框架(如amd64)、操作系统(如Linux)、镜像默认配置、构建该镜像的容器ID和配置、创建时间、创建该镜像的Docker版本、构建镜像的历史信息以及rootfs组成,其中构建镜像历史信息和rootfs组成部分除了具体描述镜像的作用外,还将镜像和构建该镜像的镜像层关联起来。Docker会根据历史信息和rootfs中的diff_ids计算出构建成该镜像的镜像层的存储索引chainID,这也是Docker1.10镜像存储中基于内容寻址的核心技术。
imageStore则管理镜像ID与镜像元数据之间的映射关系,以及元数据的持久化操作。Docker1.13.1版本持久化文件默认位于/var/lib/docker/image/overlay2/imagedb/content/sha256/[image_id]中
layer元数据
layer对应镜像层的概念,在Docker 1.10版本以前,镜像通过一个graph结构管理,每一个镜像层拥有的元数据,记录了该层的构建信息以及父镜像ID,而最上面的镜像层会对多记录一些信息作为整个镜像的元数据。拥有graph则根据镜像ID(即最上层的镜像ID)和每个镜像记录的父镜像ID维护一个树状的镜像层结构
在Docker 1.10版本后,镜像元数据管理巨大改变之一便是简化了,镜像层的元数据,镜像层只包含一个具体的镜像层文件包,用户在Docker宿主机上下载某个镜像层之后,Docker会在宿主机上基于镜像层文件包个image元数据,构建本地的layer与元数据,包括diff、parent、size等。而Docker将宿主机上生产新的镜像层上传到registry时,与新镜像层相关的宿主机上元数据也不会与镜像层一块打包上传
Docker中定义了Layer与RWLayper两种接口,分别用来定义只读层和可读写层一些操作,又定义了roLayer和mountedLayer,分别实现上述两种接口。其中,roLayer用于描述不可改变得镜像层,mountedLayer用于描述可读写的容器层。
具体来说。roLayer存储内容主要有索引该镜像层的chainID、该镜像层的校验码diffID、父镜像层parent、graphdriver存储当前镜像层文件的cacheID、该镜像层的大小size等内容。这些元数据的持久化文件位于/var/lib/docker/image/overlay2/imagedb/content/sha256/[chainID],其中,ddifID和size可以通过该镜像层计算出来;chainID和父镜像层parent需要从所属的image元数据中计算得到;而cacheID是在当前宿主机上随机生成的一个UUID,在当前宿主机与该镜像层一一对应,用于标识并索引graphdriver中的镜像层文件
在layer的所有属性中,diffID采用SHA256算法,基于镜像层文件包的内容计算得到。而chainID是基于内容存储索引,它根据当前层与所有祖先镜像层diffID计算出来的
具体算法
- 如果该镜像层是最底层(没有父镜像层),该层的diffID便是chainID
- 该镜像层的chainID计算公式chainID(n)=SHA256(chainID(n-1)diffID(n)),也就是根据父镜像层的chainID加一个空格和当前层的diffID,再计算SHA256校验码
mountedLayer存储的内容主要为索引某个容器的可读写层(也叫容器层)的ID(也对应容器的ID)、容器init层在graphdriver中的ID——initID、读写层在graphdriver中的ID——mountID以及容器层的父层镜像的chainID——parent。
Docker 存储驱动
镜像层与写时复制机制,为了支持这些特性,Docker提供了存储驱动接口。存储驱动根据操作系统底层的支持提供了针对某文件系统的初始化操作以及对镜像的增删改查和差异比较等操作。目前存储系统接口已经有aufs、btrfs、devicemapper、vfs、overlay、zfs这6中具体实现,其中vfs不支持写时复制,是为使用volume提供的存储驱动,仅仅简单文件挂载操作,剩下的5中支持写时复制,它们的实现有一定的相似之处。在启动Docker服务时使用 docker daemon -s some_driver_name,来指定使用的存储驱动,当然指定的存储驱动必须被底层的操作系统支持
存储驱动的功能与管理
Docker中管理文件系统的驱动为graphdriver,其中定义了统一的接口对不同文件系统进行管理,在Docker daemon启动时就会根据不同的文件系统选择合适的驱动
存储驱动接口定义
GraphDriver主要定义了Dirver和ProtoDriver两个接口,所有驱动程序通过实行Dirver接口提供相应的功能,而ProtoDriver接口则负责定义其中的基本功能。
String()返回一个代表这个驱动的字符串,通常驱动名字
Create()创建一个新的镜像层,需要创建者传入一个唯一的ID和所需的父镜像ID
Remove()尝试根据一个ID删除一个镜像层
Get()返回指定ID的层的挂载点的绝对路径
Put()释放一个层使用的资源,比如卸载一个已挂载的层
Exists()查询指定的ID对应的层是否存在
Status()返回这个驱动的状态,这个状态用一些键值表示
Cleanup()释放由这个驱动管理的所有资源,如卸载所有层
而正常的Driver接口实现通过包含一个ProtoDriver的匿名对象来实现上面8个基本功能,除此之外,Driver还定义了4个其他方法,用于数据层之间的差异(diff)进行管理
Diff()将指定的ID层相对父镜像层改动的文件打包并返回
Changes()返回指定镜像层与父镜像层的差异列表
ApplyDiff()从差异文件包里取出差异列表,并应用到指定ID的层与父镜像层,返回新镜像层的大小
DiffSize()计算指定ID层与父镜像层的差异,并返回差异相对基础文件系统的大小
Graphdriver还提供了naiveDiffDriver结构,这个结构包含了一个ProtoDriver对象并实现了Driver接口中与差异有关的方法,可以看做Driver接口的一个实现。Docker中任何存储驱动都需要完成实现上述Driver接口,当我们在Docker中添加新的存储驱动时,可以实现Driver的全部12个方法,或者实现ProtoDriver的8个方法在使用naiveDiffriver进一步封装。不管哪种做法,只要集成了基本的存储操作和差异操作的实现,一个存储驱动就算开发完成
存储驱动的创建
首先各类存储驱动都需要定义一个属于自己的初始化过程,并在初始化过程中向Graphdriver注册自己,Graphdriver维护了一张drivers列表,提供从驱动名到驱动初始化的映射,这用于将来根据驱动名称查找对应驱动的初始化方法
所谓注册过程就是存储驱动通过调用Graphdriver提供自己的名字和对应的初始化函数,这样Graphdriver就可以将驱动名和这个初始化方法保存到drivers。当需要创建一个存储驱动时(如aufs的驱动),Graphdriver会根据名字从对应的drivers中查找,这个驱动对应的初始化方法,然后调用初始化函数得到对应的Driver对象。
常用存储驱动分析
aufs
aufs是一种支持联合挂载的文件系统,简单来说就是支持将不同的目录挂载到同一个目录下,这些挂载操作对用户而言是透明的,用户操作该目录时并不会觉得与其他目录有什么不同。这些目录挂载是分层次的,通常而言最上层是可读写层,下层只读层,所有,aufs的每一层都是一个其他文件系统
当需要读取一个A文件时,会从最顶层的读写层开始向下查找,本层没有,则根据层之间的关系到下一层,直到找到文件A并打开它;当需要写入一个文件时,如果这个文件不存在,则在读写层新建一个;否则像上面的过程一样从顶层开始查找,直到找到A文件,aufs会把这个文件复制到读写层进行修改。由此可以看出,在第一次修改已有文件时,如果这个文件很大,即使修改几行字节,也会产出巨大磁盘开销。当需要删除一个文件时,如果这个文件仅仅存在于读写层中,则可以直接删除这个文件,否则需要先删除读层中的备份,再在读写层中创建一个whiteou文件来标识这个文件不存在,而不是真正删除底层文件;新建一个文件时,如果这个文件在读写层对应的whiteou文件,则先删除whiteou文件再新建。否则直接在读写层新建即可。
镜像文件在本地存放目录;我们知道Docker工作目录时/var/lib/docker
配置系统支持aufs文件系统,默认centos7不支持
[root@service-3 ~]# wget https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo --2019-04-09 09:56:33-- https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo 正在解析主机 yum.spaceduck.org (yum.spaceduck.org)... 63.211.111.86 正在连接 yum.spaceduck.org (yum.spaceduck.org)|63.211.111.86|:443... 已连接。 已发出 HTTP 请求,正在等待回应... 200 OK 长度:133 [application/octet-stream] 正在保存至: “kernel-ml-aufs.repo.1” 100%[=====================================================================================================================================================>] 133 --.-K/s 用时 0s [root@service-3 ~]# mv kernel-ml-aufs.repo /etc/yum.repos.d/ [root@service-3 ~]# yum -y install kernel-ml-aufs 已加载插件:fastestmirror Loading mirror speeds from cached hostfile * base: mirrors.huaweicloud.com * elrepo: hkg.mirror.rackspace.com * extras: mirrors.neusoft.edu.cn * updates: mirrors.aliyun.com kernel-ml-aufs/7/x86_64/primary_db | 9.5 MB 00:04:39 正在解决依赖关系 --> 正在检查事务 ---> 软件包 kernel-ml-aufs.x86_64.0.5.0.7-1.el7 将被 安装 --> 解决依赖关系完成 依赖关系解决 =============================================================================================================================================================================================== Package 架构 版本 源 大小 =============================================================================================================================================================================================== 正在安装: kernel-ml-aufs x86_64 5.0.7-1.el7 kernel-ml-aufs 46 M 事务概要 =============================================================================================================================================================================================== 安装 1 软件包 总下载量:46 M 安装大小:235 M Downloading packages: kernel-ml-aufs-5.0.7-1.el7.x86_64.rpm | 46 MB 00:25:30 Running transaction check Running transaction test Transaction test succeeded Running transaction 正在安装 : kernel-ml-aufs-5.0.7-1.el7.x86_64 1/1 验证中 : kernel-ml-aufs-5.0.7-1.el7.x86_64 1/1 已安装: kernel-ml-aufs.x86_64 0:5.0.7-1.el7 完毕! [root@service-3 ~]# vi /etc/default/grub GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" GRUB_DEFAULT=saved GRUB_DISABLE_SUBMENU=true GRUB_TERMINAL_OUTPUT="console" GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet" GRUB_DISABLE_RECOVERY="true" GRUB_DEFAULT=0 添加此行 [root@service-3 ~]# grub2-mkconfig -o /boot/grub2/grub.cfg Generating grub configuration file ... Found linux image: /boot/vmlinuz-5.0.7-1.el7.x86_64 Found initrd image: /boot/initramfs-5.0.7-1.el7.x86_64.img Found linux image: /boot/vmlinuz-3.10.0-862.el7.x86_64 Found initrd image: /boot/initramfs-3.10.0-862.el7.x86_64.img Found linux image: /boot/vmlinuz-0-rescue-f169d743559a49d98d5ff78bd9df15d8 Found initrd image: /boot/initramfs-0-rescue-f169d743559a49d98d5ff78bd9df15d8.img done [root@service-3 ~]# reboot [root@service-3 ~]# grep aufs /proc/filesystems nodev aufs
切换Docker默认的文件系统
[root@service-3 ~]# vi /etc/docker/daemon.json { "storage-driver" : "aufs" } [root@service-3 ~]# systemctl daemon-reload [root@service-3 ~]# systemctl restart docker
查看/var/lib/docker下另一个aufs
[root@service-3 ~]# ls /var/lib/docker/aufs/ diff layers mnt
进入其中可以看到3个目录,其中mnt 为aufs的挂载目录,diff为实际的数据来源,包括只读层和读写层,所以这些层最终一起被挂载mnt上的目录,layers下为与每一层依赖有关的层描述文件。
最初,mnt和layers都是空目录,文件数据都在diff目录下。一个Docker容器创建与启动的过程中,会在/var/lib/docker/aufs下面新建出对应的文件和目录。由于改版后,Docker镜像管理部分与存储驱动在设计上完全分离了,镜像层或者容器层在存储驱动中拥有一个新的标示ID,在镜像层(roLayer)中称为cacheID,容器层(mountedLayer)中mountID。在Unix环境下,mountID是随机生成的并保存在mountedLayer的元数据mountID中,持久化在/var/lib/docker/image/aufs/layerdb/mount-id 中,Docker创建过程中新创建过程中新创建的读写层,下面以mountID
(1)分别在mnt和diff目录下创建与该层的mountID同名的子文件夹
(2)在layers目录下创建与该层的mountID同名的文件,用来记录该层所依赖的所有的其他层。
(3)如果参数中的parent项不为空(这里由于是创建容器,parent就是镜像的最上层),说明该层依赖于其他的层。Graphdriver就需要将parent的mountID写入到该层在layers下对mountID的文件里。然后Graphdriver还需要在layers目录下读取与上述parent同mountID的文件,将parent层的所有依赖层也复制到这个新创建层对应的层描述文件中,这样这个文件才记录了该层的所有依赖。
随后Graphdriver会将diff中所属于容器镜像的所有层目录以只读方式挂载到mnt下,然后diff中生成一个以当前容器对应的<mountID>-init 命名的文件夹作为最后一层只读层,可以看到这个文件与这个容器内的环境息息相关,但不适合被打包作为镜像的文件内容,同时这些内容有不应该直接修改在宿主机文件上,所有Docker容器文件存储中设计了mountID-init这么一层单独处理这些文件,这一层只在容器启动时添加,并会根据系统环境和用户配置自动生成具体内容(DNS配置等),只有当这些文件在运行过程中被改后并且docker commit了才会持久化修改,否则保存镜像时不会包含这一层的内容
所以严格的说,Docker文件系统有3层,读写层(将来被commit的内容)、init层和只读层,但这并不影响我们传统认识上可读写层+只读层组成的容器文件系统:因为init层对用户完全透明的
接下来会在diff中生成一个容器对mountID为名的可读写目录,也挂载到mnt目录下。所以,将来用户在容器中新建文件就会出现在mnt下面mountID为名的目录,而该层对应的实际内容则保存在diff目录下
至此我们需要明确,所有文件的实际内容均保存在diff目录下,包括读写层也会以mountID命名出现在diff目录下,最终会整合到一起联合挂载到mnt目录以mountID为名的文件夹下
[root@service-3 ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a939531b372f ubuntu "/bin/bash" 4 seconds ago Up 2 seconds serene_franklin 63943ce7e7ea ubuntu "/bin/sh" 26 seconds ago Up 24 seconds modest_antonelli [root@service-3 docker]# cat image/aufs/layerdb/mounts/a939531b372f0193e60f53ec3d4e5cd40e6987c8b0289597d39acba5900c4485/mount-id 2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
查看该容器运行前对应的mnt目录,看到对应mountID文件夹下是空的
[root@service-3 mnt]# du -h . --max-depth=1 | grep 2c493293b69 0 ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1-init 73M ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
在容器添加1G的文件
[root@service-3 mnt]# docker exec -it a939531b3 /bin/bash root@a939531b372f:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var root@a939531b372f:/# mkdir test root@a939531b372f:/# cd test/ root@a939531b372f:/test# ls root@a939531b372f:/test# dd if=/dev/zero of=test.txt bs=1M count=1024 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 58.9444 s, 18.2 MB/s root@a939531b372f:/test# df -h Filesystem Size Used Avail Use% Mounted on none 17G 2.7G 15G 16% / tmpfs 64M 0 64M 0% /dev tmpfs 983M 0 983M 0% /sys/fs/cgroup /dev/mapper/centos-root 17G 2.7G 15G 16% /etc/hosts shm 64M 0 64M 0% /dev/shm tmpfs 983M 0 983M 0% /proc/asound tmpfs 983M 0 983M 0% /proc/acpi tmpfs 983M 0 983M 0% /proc/scsi tmpfs 983M 0 983M 0% /sys/firmware root@a939531b372f:/test# df -h test.txt Filesystem Size Used Avail Use% Mounted on none 17G 2.7G 15G 16% /
查看容器外文件变化
[root@service-3 mnt]# du -h . --max-depth=1 | grep 2c493293b69 0 ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1-init 1.1G ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
在容器里生成的对应文件出现在对应容器mountID文件夹中的root文件夹内,而当我们停止容器时,mnt下相应mountID的目录被卸载,而diff下相应文件夹中的文件依然存在。当然这仅限于当前宿主机,当需要迁移时需要重新做镜像
最后,当我们用docker commit把容器提交成镜像时,就会在diff目录下生成一个新的cacheID命名的文件,存放在最新的差异变化文件,这时一个新的镜像层就诞生了,原来的一mountID为名的文件夹已然存在,之间对应的容器被删除
Device Mapper
Device Mapper是Linux2.6内核中提供的一种从逻辑设备到物理设备的映射框架机制,该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理。
简单来说,Docker Mapper包括3个概念:映射设备、映射表和目标设备。映射设备是内核向外提供的逻辑设备。一个映射设备通过一个映射表与多个目标设备映射起来,映射表包含了多个多元素,每个多元素记录了这个映射设备的起始地址、范围与下一个目标设备的地址偏移量映射关系。目标设备可以是物理设备,也可以是一个映射设备,这个映射设备可以继续向下迭代。一个映射设备最终通过一个映射树映射到物理设备上。Device Mapper本质功能就是根据映射关系描述IO处理规则,当映射设备接受到IO请求的时候,这个IO请求会根据映射表逐级准发,直到请求传到最终物理设备上
Docker 下面的devicemapper存储驱动是使用的Device Mapper的精简配置和快照功能实现镜像分层。这个模块用了两快设备(一个用于存储数据,一个用于存储元数据),并将其构建成一个资源池用于创建其他存储镜像的块设备。数据区为生成其他块设备提供资源,元数据存储了虚拟设备和物理设备的映射关系,Copy onWrite发送在块存储级别。devicemapper在构建一个资源池后,会先创建一个有文件系统的基础设备,再通过从已有的设备创建快照的方式创建新设备,这些新设备的块设备在写入新内容之前并不会分配资源。所有的容器层和镜像层都有自己的块设备,都是通过其父镜像创建快照的方法来创建;值得说明的是,devicemapper存储驱动根据使用的两个基础块设备是真正的块设备和稀疏文件挂载的loop设备分为两种模式,前者称为direct-lvm模式,后者是Docker默认的loop-lvm模式。存储方式不同导致两者性能差别很大。考虑到loop-lvm不需要额外配置的易用性,Docker将其作为devicemapper的默认工作模式,生产推荐使用direct-lvm模式
与aufs一样,如果Docker使用过devicemapper存储驱动,在/var/lib/docker/下创建devicemapper以及image/devicemappe目录,同样image/devicemapper,也存储镜像和逻辑镜像层的原数据信息。最终具体文件夹下有3个子文件,其中mnt为设备挂目录,devicemapper下存储了loop-lvm模式下的两个稀疏文件,metadata下存储了每个块设备驱动层的元数据
overlay
一个 overlay 文件系统包含两个文件系统,一个 upper 文件系统和一个 lower 文件系统,是一种新型的联合文件系统。overlay是“覆盖…上面”的意思,overlay文件系统则表示一个文件系统覆盖在另一个文件系统上面。为了更好的展示 overlay 文件系统的原理,现新构建一个overlay文件系统。