Docker 镜像的元数据node
repository元数据python
repository在本地的持久化文件存放于/var/lib/docker/image/overlay2/repositories.json中linux
[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的映射关系nginx
image元数据算法
image元数据包括了镜像框架(如amd64)、操做系统(如Linux)、镜像默认配置、构建该镜像的容器ID和配置、建立时间、建立该镜像的Docker版本、构建镜像的历史信息以及rootfs组成,其中构建镜像历史信息和rootfs组成部分除了具体描述镜像的做用外,还将镜像和构建该镜像的镜像层关联起来。Docker会根据历史信息和rootfs中的diff_ids计算出构建成该镜像的镜像层的存储索引chainID,这也是Docker1.10镜像存储中基于内容寻址的核心技术。docker
imageStore则管理镜像ID与镜像元数据之间的映射关系,以及元数据的持久化操做。Docker1.13.1版本持久化文件默认位于/var/lib/docker/image/overlay2/imagedb/content/sha256/[image_id]中json
layer元数据ubuntu
layer对应镜像层的概念,在Docker 1.10版本之前,镜像经过一个graph结构管理,每个镜像层拥有的元数据,记录了该层的构建信息以及父镜像ID,而最上面的镜像层会对多记录一些信息做为整个镜像的元数据。拥有graph则根据镜像ID(即最上层的镜像ID)和每一个镜像记录的父镜像ID维护一个树状的镜像层结构centos
在Docker 1.10版本后,镜像元数据管理巨大改变之一即是简化了,镜像层的元数据,镜像层只包含一个具体的镜像层文件包,用户在Docker宿主机上下载某个镜像层以后,Docker会在宿主机上基于镜像层文件包个image元数据,构建本地的layer与元数据,包括diff、parent、size等。而Docker将宿主机上生产新的镜像层上传到registry时,与新镜像层相关的宿主机上元数据也不会与镜像层一块打包上传bash
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文件系统。