一、什么是Docker镜像

Docker镜像是一个只读的Docker容器模板,含有启动Docker容器所需的文件系统结构及其内容,是启动一个Docker容器的基础。Docker镜像的文件内容以及一些运行Docker容器的配置文件组成了Docker容器的静态文件系统运行环境―—rootfs。Docker镜像是Docker容器的静态视角,Docker容器是Docker镜像的运行状态。

rootfs

rootfs是Docker容器在启动时内部进程可见的文件系统,即Docker容器的根目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix操作系统中的目录系统,如/dev、/proc、/bin、/etc、/lib、/usr、/tmp及运行Docker容器所需的配置文件、工具等。

传统的Linux操作系统内核启动时,首先挂载一个只读( read-only)的rootfs,当系统检测其完整性之后,再将其切换为读写( read-write)模式。而在Docker架构中,当Docker daemon为Docker容器挂载rootfs时,沿用了Linux内核启动时的方法,即将rootfs设为只读模式。在挂载完毕之后,利用联合挂载( union mount)技术在已有的只读rootfs上再挂载一个读写层。这样,可读写层处于Docker容器文件系统的最顶层,其下可能联合挂载多个只读层,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。

主要特点

分层

Docker镜像是采用分层的方式构建的,每个镜像都由一系列的“镜像层”组成。分层结构是Docker镜像如此轻量的重要原因,当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版文件所隐藏。当使用docker commit提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。分层达到了在不同镜像之间共享镜像层的效果

写时复制

Docker镜像使用了写时复制( copy-on-write)策略,在多个容器之间共享镜像,每个容器在启动的时候并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点,再在上面覆盖一个可读写的容器层。在未更改文件内容时,所有容器共享同一份数据,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间。

内容寻址

  • 根据文件内容索引镜像和镜像层。
  • 对镜像层的内容计算校验和,生成一个内容哈希值,作为唯一标识。
  • 提高了镜像的安全性,在pull、push、load和save操作后检测数据的完整性。
  • 一定程度上减少了ID冲突并且增强了镜像共享。不同构建的镜像层,拥有相同内容哈希,也能被不同的镜像共享。

联合挂载

联合文件系统。在一个挂载点挂载多个文件系统,将挂载点原目录和被挂载内容整合,最终可见的文件系统将会包含整合后的各层文件和目录。联合挂载是用于将多个镜像层的文件系统挂载到一个挂载点来实现一个统一文件系统视图的途径,是下层存储驱动(如aufs、overlay等)实现分层合并的方式。所以严格来说,联合挂载并不是Docker镜像的必需技术,比如我们在使用Device Mapper存储驱动时,其实是使用了快照技术来达到分层的效果,没有联合挂载这一概念。

Docker镜像存储方式

从图中我们可以看到,除了echo hello进程所在的cgroupsnamespace环境之外,容器文件系统其实是一个相对独立的组织可读写部分( read-write layer以及volumes )init-layer只读层( read-only layer)这3部分结构共同组成了一个容器所需的下层文件系统,它们通过联合挂载的方式巧妙地表现为一层,使得容器进程对这些层的存在一点都不知道。

docker文件系统全局概览

二、Docker镜像关键概念

1.registry

registry用以保存Docker镜像,其中还包括镜像层次结构和关于镜像的元数据。

用户可以在自己的数据中心搭建私有的registry,也可以使用Docker官方的公用registry服务,即Docker Hub

2.repository

repository即由具有某个功能的Docker镜像的所有迭代版本构成的镜像组。registry由一系列经过命名的repository组成, repository通过命名规范对用户仓库和顶层仓库进行组织。用户仓库的命名由用户名和repository名两部分组成,中间以”/“隔开,即username/repository.name的形式,repository名通常表示镜像所具有的功能,如ansible/ubuntu14.04-ansible;而顶层仓库则只包含repository名的部分,如ubuntu。

3.manifest

manifest(描述文件)主要存在于registry中作为Docker镜像的元数据文件,在pull、push,save和load中作为镜像结构和基础信息的描述文件。在镜像被pull或者load到Docker宿主机时,manifest被转化为本地的镜像配置文件config。新版本(v2,schema 2 )的manifest list可以组合不同架构实现同名Docker镜像的manifest,用以支持多架构Docker镜像。

4.image和layer

Docker内部的image概念是用来存储一组镜像相关的元数据信息,主要包括镜像的架构(如amd64)、镜像默认配置信息、构建镜像的容器配置信息、包含所有镜像层信息的rootfs。Docker利用rootfs中的diff_id计算出内容寻址的索引 ( chainID)来获取layer相关信息,进而获取每一个镜像层的文件内容。
layer(镜像层)是一个Docker用来管理镜像层的中间概念,前面提到镜像是由镜像层组成的,而单个镜像层可能被多个镜像共享,所以Docker将layer与image的概念分离。Docker镜像管理中的layer主要存放了镜像层的diff_id、size、cache-id和parent等内容,实际的文件内容则是由存储驱动来管理,并可以通过cache-id在本地索引到。

三、Docker镜像的分发方法

docker pushdocker pull、或docker savedocker load命令进行分发。docker pull是通过Docker Hub的方式迁移,docker save是通过线下包分发的方式迁移。

对容器进行持久化和使用进行进行持久化区别:

  • docker export用于持久化容器docker pushdocker save用于持久化镜像
  • 将容器导出在导入(exported-imported)后的容器会丢失所有历史,而保存后在加载(saved-loaded)镜像没有丢失历史和层,后者可以通过docker tag实现历史层回滚。

docker export导出容器

Docker server接收到相应的HTTP请求后,会通过daemon实例调用ContainerExport方法来进行具体的操作,这个过程的主要步骤如下。

  1. 根据命令行参数(容器名称)找到待导出的容器。

  2. 对该容器调用containerExport()函数导出容器中的所有数据,包括:

    • 挂载待导出容器的文件系统;

    • 打包该容器basefs (即graphdriver上的挂载点)下的所有文件。以aufs为例,basefs对应的是aufs/mnt下对应容器ID的目录;

    • 返回打包文档的结果并卸载该容器的文件系统。

  3. 将导出的数据回写到HTTP请求应答中。

docker save命令保存镜像

Docker client发来的请求由getImagesGet Handler进行处理,该Handler调用ExportImage函数进行具体的处理。

ExportImage会根据imageStore、layerStore、referenceStore构建一个imageExporter,调用其save函数导出所有镜像。

save函数负责查询到所有被要求export的镜像ID((如果用户没有指定镜像标签,会指定默认标签latest),并生成对应的镜像描述结构体。然后生成一个saveSession并调用其save函数来处理所有镜像的导出工作。
save函数会创建一个临时文件夹用于保存镜像json文件。然后循环遍历所有待导出的镜像,对每一个镜像执行saveImage函数来导出该镜像。另外,为了与老版本repository兼容,还会将被导出的repository的名称、标签及ID信息以JSON格式写入到名为repositories的文件中。而新版本中被导出的镜像配置文件名repository的名称标签以及镜像层描述信息则是写入到名为manifest.json的文件中。最后执行文件压缩并写入到输出流。

saveImage函数首先根据镜像ID在imageStore中获取image结构体。其次是一个for循环,遍历该镜像RootFS中所有layer,对各个依赖layer进行export工作,即从顶层layer、其父layer及至baselayer

  1. 为每个被要求导出的镜像创建一个文件夹,以其镜像ID命名。
  2. 在该文件夹下创建VERSION文件,写入“1.0”。
  3. 在该文件夹下创建json文件,在该文件中写入镜像的元数据信息,包括镜像ID、父镜像ID以及对应的Docker容器ID等。
  4. 在该文件夹下创建layer.tar文件,压缩镜像的filesystem。该过程的核心函数为TarLayer,对存储镜像的diff路径中的文件进行打包。
  5. 对该layer的父layer执行下一次循环。

四、Docker存储管理

Docker镜像元数据管理

在设计上将镜像元数据与镜像文件的存储完全隔离开。在管理元数据时,采用从上到下repositoryimagelayer三个层次。由于Docker采用分层形式存储镜像,所以repositoryimage这两类元数据并无物理上的镜像文件与之对应,而layer这种元数据存在物理上的镜像层与之对应。

  1. repository元数据

    repository在本地持久化文件存放与/var/lib/docker/image/[some_graph_driver]/repositories.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /var/lib/docker/image/aufs# cat repositories.json | python -mjson.tool
    "Repositories": {
    "busybox" : {
    "busybox: latest" :
    "sha256:47bcc53f74dc94b1920fob34f6036096526296767650f223433fe65c35f149eb"
    },
    "fedora" : {
    "fedora: latest" :
    "sha256:ddd5c9c1dof2a08c5d53958a2590495d4f8a6166e2c1331380178af425ac9f3c"
    },
    "ubuntu" : {
    "ubuntu: 14.04" :
    "sha256:90d5884b1ee07f7f791f51bab92933943c87357bcd2fa6beoe82c48411bbb653"
    }
    }

    文件存储了所有repository的名字、每个repository下所有版本镜像的名字以及对应的镜像ID。referenceStore的作用是解析不同格式的repository名字,并管理repository与镜像ID的关系。

  2. image元数据

    包括镜像架构(如amd64)、操作系统(如Linux)、镜像默认配置、构建该镜像的容器ID、创建时间、创建该镜像的Docker版本、构建进行的历史信息以及rootfs组成。

    构建镜像的历史信息和rootfs组成部分除了具有描述镜像的作用外,还将镜像和构成该镜像的镜像层关联了起来。Docker会根据历史信息和rootfs中的diff_ids计算出构成该镜像的镜像层的存储索引chainID

    imageStore则管理镜像ID与镜像元数据之间的映射关系以及元数据的持久化操作,持久化文件位于/var/lib/docker/image/[graph_driver]/imagedb/content/sha256/[image_id]中。

    1
    2
    cat 7faaec68323851b2265bddb239bd9476c7d4e4335e9fd88cbfcc1df374dded2f 
    ...

    我们重点来看看rootfs信息,docker inspect下这个image,可以看到有个RootFS项,里面记录了一些sha256 哈希值,这又是什么呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [...
    "RootFS": {
    "Type": "layers",
    "Layers": [
    "sha256:e8b689711f21f9301c40bf2131ce1a1905c3aa09def1de5ec43cf0adf652576e",
    "sha256:b43651130521eb89ffc3234909373dc42557557b3a6609b9fed183abaa0c4085",
    "sha256:8b9770153666c1eef1bc685abfc407242d31e34f180ad0e36aff1a7feaeb3d9c",
    "sha256:6b01cc47a390133785a4dd0d161de0cb333fe72e541d1618829353410c4facef",
    "sha256:0bd13b42de4de0a0d0cc3f1f162cd0d4b8cb4ee20cbea7302164fdc6894955fd",
    "sha256:146262eb38412d6eb44be1710bfe0f05d3493831f82b1c2be8dc8d9558c9f033"
    ]
    },

    ...
    ]

    后面的哈希值称为diff_id,其排列也是有顺序的,从上到下依次表示镜像层的最低层到最顶层。每层文件都存储/var/lib/docker/overlay2/<cache_id>目录下,docker 利用 rootfs 中的每个diff_id历史信息计算出与之对应的内容寻址的索引(chainID) ,而chaiID则关联了cache_id,进而关联到每一个镜像层的镜像文件。

    1
    diff_id -> chain_id -> cache_id
    • cache_id: 可以在/var/lib/docker/overlay2中查看,也可以通过docker inpect 查看GraphDriver中的dir ID。

    • diff_id:通过docker inpect查看RootFS中的Layers项目

    • chain_id: 可以在/var/lib/docker/image/overlay2/layerdb/sha256查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #本例中
    # layer_id有:
    # ls /var/lib/docker/overlay2
    049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9
    0c11dee18f1e2556cec649be416d7b4221ddd6d224449b547ca983651edf4391
    35df5518615bb8c17558526dd1ce0ef2eab8d562129be3ec3b32831b1c41f827
    514dda44fa241d5bdc1cf5973a675161aebf77e7c47e9d20df0cb66a7fa8bf46
    93d168d9964b1a43c296b0dbc9caadefe079bccac58add92d07f883d16734610
    a20939fbb9a6e0bd5b185f293453748b67e89927b3ebad29a9e65acfbc684df4
    bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a

    # diff_id有
    "sha256:e8b689711f21f9301c40bf2131ce1a1905c3aa09def1de5ec43cf0adf652576e", "sha256:b43651130521eb89ffc3234909373dc42557557b3a6609b9fed183abaa0c4085", "sha256:8b9770153666c1eef1bc685abfc407242d31e34f180ad0e36aff1a7feaeb3d9c", "sha256:6b01cc47a390133785a4dd0d161de0cb333fe72e541d1618829353410c4facef",
    "sha256:0bd13b42de4de0a0d0cc3f1f162cd0d4b8cb4ee20cbea7302164fdc6894955fd",
    "sha256:146262eb38412d6eb44be1710bfe0f05d3493831f82b1c2be8dc8d9558c9f033"

    # chain_id有
    # ls /var/lib/docker/image/overlay2/layerdb/sha256
    l 24
    2649acad13241d9c8d81e49357bc66cce459b352ded7f423d70ede7bd3bb7b89
    64007bba5fc220df4d3da33cecdc2d55dd6a73528c138b0fa1acd79fd6a9c217
    b2cc2f1bf8b1cca8ba7c19e1697f7b73755903ad8f880b83673fd6a697aca935
    e6deb90762475cda72e21895911f830ed99fd1cc6d920d92873270be91235274
    e8b689711f21f9301c40bf2131ce1a1905c3aa09def1de5ec43cf0adf652576e
    fbd1283ab782925be4d990bd4bebe9ad5e5cf9a525abfb6fa87465e072da9d31

diff_id到chain_id的算法为:

  • 如果该镜像层是最底层(没有父镜像层),该层的 diff_id 便是 chain_id。

  • 该镜像层的 chain_id 计算公式为 chainID=sha256(父层chain_id+" "+本层diff_id),也就是根据父镜像层的 chain_id 加上一个空格和当前层的 diff_id,再计算 SHA256 校验码。

    layerID: 压缩数据的sha256的值 ,也就是pull一个镜像时的显示的id。

    diffID: 是 docker inspect 查看到的 镜像层 hash ID,此时 镜像层文件是解压缩的,解压缩态rootfs layers的值是diffID.distribution 目录

    • diffid-by-digest 保存了digest(layerID)->diffID的映射关系
    • v2metadata-by-diffid 保存了diffid -> (digest,repository)的映射关系

    chainID: layerdb/sha256下的目录名称是以layer的chainID来命名的,通过diffID计算而得。layerdb/sha256/目录下,存放着parent,size,cache-id,diff

    cache-id:存储驱动通过cache-id索引到layer的实际文件内容

  1. layer元数据

    layer对应镜像层的概念,在Docker 1.10版本以前,镜像通过一个graph结构管理,每一个镜像层都拥有元数据,记录了该层的构建信息以及父镜像层ID,而最上面的镜像层会多记录一些信息作为整个镜像的元数据。graph则根据镜像ID(即最上层的镜像层ID)和每个镜像层记录的父镜像层ID维护了一个树状的镜像层结构。

    在Docker 1.10版本后,镜像元数据管理巨大改变之一便是简化了镜像层的元数据,镜像层只包含一个具体的镜像层文件包。用户在Docker宿主机上下载了某个镜像层之后,Docker会在宿主机上基于镜像层文件包和image元数据,构建本地的layer元数据,包括diff、parent、size等。而将在宿主机上产生新的镜像层上传到registry时,与新镜像层相关的宿主机上的元数据也不会与镜像层一块打包上传。

    Docker中定义了LayerRWLayper两种接口,分别用来定义只读层和可读写层的一些操作,又定义了roLayermountedLayer,分别实现了上述两种接口。其中,roLayer用于描述不可改变的镜像层,mountedLayer用于描述可读写的容器层。

    • roLayer 用于描述不可改变的镜像层,它的元数据位于 /var/lib/docker/image/<storage_driver>/layerdb/sha256/<chain_id>
    • mountedLayer 用于描述可读写的容器层。它的元数据位于/var/lib/docker/image/<storage_driver>/layerdb/mounts/<container_id>/

    roLayer存储的内容主要有索引该镜像层的chainID该镜像层的校验码diffID父镜像层parentgraphdriver存储当前镜像层文件的cacheID、该镜像层的大小size等内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    tree -L 2
    .
    |-- 2649acad13241d9c8d81e49357bc66cce459b352ded7f423d70ede7bd3bb7b89
    | |-- cache-id
    | |-- diff
    | |-- parent
    | |-- size
    | `-- tar-split.json.gz
    |-- 64007bba5fc220df4d3da33cecdc2d55dd6a73528c138b0fa1acd79fd6a9c217
    | |-- cache-id
    | |-- diff
    | |-- parent
    | |-- size
    | `-- tar-split.json.gz
    |-- b2cc2f1bf8b1cca8ba7c19e1697f7b73755903ad8f880b83673fd6a697aca935
    | |-- cache-id
    | |-- diff
    | |-- parent
    | |-- size
    | `-- tar-split.json.gz
    |-- e6deb90762475cda72e21895911f830ed99fd1cc6d920d92873270be91235274
    | |-- cache-id
    | |-- diff
    | |-- parent
    | |-- size
    | `-- tar-split.json.gz
    |-- e8b689711f21f9301c40bf2131ce1a1905c3aa09def1de5ec43cf0adf652576e
    | |-- cache-id
    | |-- diff
    | |-- size
    | `-- tar-split.json.gz
    `-- fbd1283ab782925be4d990bd4bebe9ad5e5cf9a525abfb6fa87465e072da9d31
    |-- cache-id
    |-- diff
    |-- parent
    |-- size
    `-- tar-split.json.gz

    6 directories, 29 files

    可以看到每个文件夹的名字都是chain_id,每个文件夹下面有5个文件,分别是:

    • cache-id:cache-id是docker下载layer的时候在本地生成的一个随机uuid,指向真正存放layer文件的地方。
    • diff:文件存放layer的diff_id。
    • parent:parent文件存放当前layer的父layer的diff_id,注意:对于最底层的layer来说,由于没有父layer,所以没有这个文件,例如本例子中的e8b689711f21f9301c40bf2131ce1a1905c3aa09def1de5ec43cf0adf652576e
    • size:当前layer的大小,单位是字节。
    • tar-split.json.gz:layer压缩包的split文件,通过这个文件可以还原layer的tar包,在docker save导出image的时候会用到

    通过cat cache-id就可以知道layer真正存放文件的位置。

mountedLayer存储的内容主要为索引某个容器的可读写层(也叫容器层)的ID(也对应容器的ID)

查看该container的init层与容器层元数据

1
2
3
4
5
6
7
8
9
cd /var/lib/docker/image/[graph_driver]/layerdb/mounts/[container_id]/
ls
init-id mount-id parent
cat init-id
bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a-init
cat mount-id
bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a
cat parent
sha256:e6deb90762475cda72e21895911f830ed99fd1cc6d920d92873270be91235274
  • mount-id:存储在/var/lib/docker/overlay2/的目录名称。
  • init-id:initID是在mountID后加了一个-init,同时initID就是存储在/var/lib/docker/overlay2/的目录名称。
  • parent:容器所基于的镜像的最上层的chain_id。(注意这个parent和roLayer元数据的parent的不同之处)。

五、存储驱动

Docker为了支持镜像分层和写时复制,Docker提供了存储驱动的接口。存储驱动根据操作系统的支持提供了针对某种文件系统的初始化操作和镜像层的增、删、改、查和差异比较。目前的接口有aufs、btrfs、devicemapper、vfs、overlay、zfs。vfs不支持写时复制,是为使用volume提供存储驱动,只做简单文件挂载操作。

存储驱动的功能与管理

Docker中管理文件系统的驱动为graphdriver。其中定义了统一的接口对不同的文件系统进行管理,在Docker daemon启动时就会根据不同的文件系统选择合适的驱动。

常用的存储驱动overlay

OverlayFS是一种新型联合文件系统,允许用户将一个文件系统与另一个文件系统重叠(overlay),在上层的文件系统中记录更改,下层文件系保持不变。

主要使用4类目录完成工作:被联合挂载的两个目录lowerupper,作为统一视图联合挂载点的merged目录,作为辅助功能的work目录。

作为upper和lower被联合挂载的统一视图,当同一路径的文件分别存在于2个目录时,位于upper中的文件会屏蔽下层lower的文件夹,同文件夹的文件则会合并。OverlayFS会执行一个copy_up将文件从下层复制到上层.

overlay2的目录结构

overlay2是上最新的Docker CE版本18.06.0上的默认存储驱动.

overlay2

通过redis镜像看/var/lib/docker/overlay2

image-20211016133323087

这个文件夹

1
2
3
4
5
6
7
8
9
10
[root@VM-4-9-centos l]# ll
total 32
lrwxrwxrwx 1 root root 72 Oct 15 23:08 CTDTMMF65E3BMSWDBORDZWNKMZ -> ../bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a/diff
lrwxrwxrwx 1 root root 77 Oct 15 23:08 EIFKNFA4XWG2Y23M42BB3UOAR3 -> ../bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a-init/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 L6L4ECZTQI2TRRFXXFMG7IS4HJ -> ../a20939fbb9a6e0bd5b185f293453748b67e89927b3ebad29a9e65acfbc684df4/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 LX6ZEQ54QRUVOVLE36C5Q4R3HL -> ../049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 SMLOMKRFYWOKNGUBP5WDV3SRSP -> ../93d168d9964b1a43c296b0dbc9caadefe079bccac58add92d07f883d16734610/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 YQCFTHN4NK637K6MG6JPWJLK2T -> ../35df5518615bb8c17558526dd1ce0ef2eab8d562129be3ec3b32831b1c41f827/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 ZUO6UAVQPN3PQDP2LMW4DBLHAX -> ../514dda44fa241d5bdc1cf5973a675161aebf77e7c47e9d20df0cb66a7fa8bf46/diff
lrwxrwxrwx 1 root root 72 Oct 15 22:57 ZZK7WDD5HNYHP3PKBTF54O666R -> ../0c11dee18f1e2556cec649be416d7b4221ddd6d224449b547ca983651edf4391/diff

全部都是到各层diff之间的软链接,以CTDTMMF65E3BMSWDBORDZWNKMZ为例子,观察一下这个链接目录:

1
2
3
4
[root@VM-4-9-centos l]# cd CTDTMMF65E3BMSWDBORDZWNKMZ/
[root@VM-4-9-centos CTDTMMF65E3BMSWDBORDZWNKMZ]# ll
total 8
drwxr-xr-x 3 root root 4096 Oct 15 23:08 etc

只有一个etc目录,再查看EIFKNFA4XWG2Y23M42BB3UOAR3

1
2
3
4
[root@VM-4-9-centos EIFKNFA4XWG2Y23M42BB3UOAR3]# ll
total 8
drwxr-xr-x 4 root root 4096 Oct 15 23:08 dev
drwxr-xr-x 2 root root 4096 Oct 15 23:08 etc

事实上,每层的diff即是文件系统在统一挂载时的挂载点,我们可以再进一步地观察最后一层ZZK7WDD5HNYHP3PKBTF54O666R的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
drwxr-xr-x  2 root root 4096 Oct 11 08:00 bin
drwxr-xr-x 2 root root 4096 Oct 3 17:15 boot
drwxr-xr-x 2 root root 4096 Oct 11 08:00 dev
drwxr-xr-x 30 root root 4096 Oct 11 08:00 etc
drwxr-xr-x 2 root root 4096 Oct 3 17:15 home
drwxr-xr-x 8 root root 4096 Oct 11 08:00 lib
drwxr-xr-x 2 root root 4096 Oct 11 08:00 lib64
drwxr-xr-x 2 root root 4096 Oct 11 08:00 media
drwxr-xr-x 2 root root 4096 Oct 11 08:00 mnt
drwxr-xr-x 2 root root 4096 Oct 11 08:00 opt
drwxr-xr-x 2 root root 4096 Oct 3 17:15 proc
drwx------ 2 root root 4096 Oct 11 08:00 root
drwxr-xr-x 3 root root 4096 Oct 11 08:00 run
drwxr-xr-x 2 root root 4096 Oct 11 08:00 sbin
drwxr-xr-x 2 root root 4096 Oct 11 08:00 srv
drwxr-xr-x 2 root root 4096 Oct 3 17:15 sys
drwxrwxrwt 2 root root 4096 Oct 11 08:00 tmp
drwxr-xr-x 11 root root 4096 Oct 11 08:00 usr
drwxr-xr-x 11 root root 4096 Oct 11 08:00 var

可以发现这一层仿佛就是一个Centos了,这些文件是只读的,每层具体的文件存放在层标识符下的diff目录下。

进入到第二个目录0c11dee18f1e2556cec649be416d7b4221ddd6d224449b547ca983651edf4391目录下。查看目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@VM-4-9-centos 0c11dee18f1e2556cec649be416d7b4221ddd6d224449b547ca983651edf4391]# tree -L 2
.
|-- committed
|-- diff
| |-- bin
| |-- boot
| |-- dev
| |-- etc
| |-- home
| |-- lib
| |-- lib64
| |-- media
| |-- mnt
| |-- opt
| |-- proc
| |-- root
| |-- run
| |-- sbin
| |-- srv
| |-- sys
| |-- tmp
| |-- usr
| `-- var
`-- link

20 directories, 2 files

进入第一个目录049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@VM-4-9-centos 049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9]# tree -L 2
.
|-- committed
|-- diff
| `-- data
|-- link
|-- lower
`-- work

[root@VM-4-9-centos 049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9]#
cat lower
l/ZUO6UAVQPN3PQDP2LMW4DBLHAX:l/YQCFTHN4NK637K6MG6JPWJLK2T:l/L6L4ECZTQI2TRRFXXFMG7IS4HJ:l/ZZK7WDD5HNYHP3PKBTF54O666R
[root@VM-4-9-centos 049991c5c6099d5999c3d4c186cb3cc4ba568c31e6420fa72dff87d82b29fcb9]#
cat link
LX6ZEQ54QRUVOVLE36C5Q4R3HL

link文件描述了该层标识符的精简版,而lower文件描述了层序的组织关系。

启动一个容器,查看挂载情况:

1
2
mount | grep overlay
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/EIFKNFA4XWG2Y23M42BB3UOAR3:/var/lib/docker/overlay2/l/SMLOMKRFYWOKNGUBP5WDV3SRSP:/var/lib/docker/overlay2/l/LX6ZEQ54QRUVOVLE36C5Q4R3HL:/var/lib/docker/overlay2/l/ZUO6UAVQPN3PQDP2LMW4DBLHAX:/var/lib/docker/overlay2/l/YQCFTHN4NK637K6MG6JPWJLK2T:/var/lib/docker/overlay2/l/L6L4ECZTQI2TRRFXXFMG7IS4HJ:/var/lib/docker/overlay2/l/ZZK7WDD5HNYHP3PKBTF54O666R,upperdir=/var/lib/docker/overlay2/bf88c56ff03c6eee19d8a2ece7c69c3

而在overlay2文件夹中会有2层:

1
2
3
4
5
drwx--x--- 5 root root 4096 Oct 15 23:08 bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a
drwx--x--- 4 root root 4096 Oct 15 23:08 bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a-init

CTDTMMF65E3BMSWDBORDZWNKMZ -> ../bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a/diff
lrwxrwxrwx 1 root root 77 Oct 15 23:08 EIFKNFA4XWG2Y23M42BB3UOAR3 -> ../bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a-init/diff

这一层是动态生成的:

1
2
3
4
5
6
7
8
9
10
11
12
[root@VM-4-9-centos bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a-init]# tree -L 2
.
|-- committed
|-- diff
| |-- dev
| `-- etc
|-- link
|-- lower
`-- work
`-- work

5 directories, 3 files

主要是一些配置文件构成的层。

而不带init后缀的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
|-- diff
| |-- etc
| `-- root
|-- link
|-- lower
|-- merged
| |-- bin
| |-- boot
| |-- data
| |-- dev
| |-- etc
| |-- home
| |-- lib
| |-- lib64
| |-- media
| |-- mnt
| |-- opt
| |-- proc
| |-- root
| |-- run
| |-- sbin
| |-- srv
| |-- sys
| |-- tmp
| |-- usr
| `-- var
`-- work
`-- work

cat lower
l/EIFKNFA4XWG2Y23M42BB3UOAR3:l/SMLOMKRFYWOKNGUBP5WDV3SRSP:l/LX6ZEQ54QRUVOVLE36C5Q4R3HL:l/ZUO6UAVQPN3PQDP2LMW4DBLHAX:l/YQCFTHN4NK637K6MG6JPWJLK2T:l/L6L4ECZTQI2TRRFXXFMG7IS4HJ:l/ZZK7WDD5HNYHP3PKBTF54O666R

这个目录结构与与底层的centos相像,标识符多了一个文件夹merged,而与底层的0c11dee18f1e2556cec649be416d7b4221ddd6d224449b547ca983651edf4391,的diff文件夹相似,它正是容器的可读可写层。回头来观察overlay2联合挂载情况:

1
2
mount | grep overlay
overlay on / type overlay overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/EIFKNFA4XWG2Y23M42BB3UOAR3:/var/lib/docker/overlay2/l/SMLOMKRFYWOKNGUBP5WDV3SRSP:/var/lib/docker/overlay2/l/LX6ZEQ54QRUVOVLE36C5Q4R3HL:/var/lib/docker/overlay2/l/ZUO6UAVQPN3PQDP2LMW4DBLHAX:/var/lib/docker/overlay2/l/YQCFTHN4NK637K6MG6JPWJLK2T:/var/lib/docker/overlay2/l/L6L4ECZTQI2TRRFXXFMG7IS4HJ:/var/lib/docker/overlay2/l/ZZK7WDD5HNYHP3PKBTF54O666R,upperdir=/var/lib/docker/overlay2/bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a/diff,workdir=/var/lib/docker/overlay2/bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a/work)

overlay2将lowerdirupperdirworkdir联合挂载,形成最终的merged挂载点,其中lowerdir是镜像只读层,upperdir是容器可读可写层,workdir是执行涉及修改lowerdir执行copy_up操作的中转层

在容器内创建一个文件:

1
2
root@e96786091371:/data# cat /root/hello 
hello

观测镜像的可读写层:

1
2
3
4
5
[root@VM-4-9-centos root]# pwd
/var/lib/docker/overlay2/bf88c56ff03c6eee19d8a2ece7c69c3d903b56350f4bef83d5ae5918143c320a/diff/root
[root@VM-4-9-centos root]# ll
total 4
-rw-r--r-- 1 root root 6 Oct 15 23:30 hello

可以发现,新创建的文件被存在了可读写层,而此时如果我们通过以下命令:

1
docker commit CONTAINER_ID

提交容器更改,则会将该容器的当前可读可写层转化为只读层,更新镜像。镜像大体上,可以认为是多个只读层通过某些特定的方式组织起来,而容器则是在其之上的一个可读写层,我们可以保存一个可读写层的更改,将它转化为一个只读层。

overlay2和overlay1的区别:

  1. overlay实际上通过硬链接在层和层之间共享文件,而overlay2的每一层都是完全独立的,通过每层的 lower文件。如果容器启动的话,它会将多层lowerdir 挂载到它的rootfs。

    linux系统会限制系统中硬链接的数量,如果用户下载了很多容器,那么docker就会在系统中到处创建硬链接,达到最大值后将无法创建新容器。

  2. overlay2中link文件描述了该层标识符的精简版,在overlay2每层的内容都是不一样的,diff是文件系统的统一挂载点,link文件描述的是该层的标识符lower文件描述了层与层之间的组织关系,overlay2是将底层多个lowerdir和upperdir和workdir联合挂载,形成最终的merged挂载点。

  3. overlay2为什么比overlay不消耗inode,根本原因在于那些文件夹,每层的root目录内存放的都是完整的rootfs文件夹,但它们都是新建出来的,它们inode都不一样,所以在overlay下一个容器镜像层数越多,占用的inode就越多。

参考:

https://www.cnblogs.com/robinunix/p/12157910.html

https://zhuanlan.zhihu.com/p/41958018

https://zhuanlan.zhihu.com/p/374924046