Docker是如何生成/保存镜像的?

 

前言

我们知道,Docker容器中的进程实际上就是跑在Linux Namespace中,与外部全局Namespace隔离开来(Docker容器是如何隔离的?)。但是我们在Docker容器中看见的文件视图是如何来的呢? 容器镜像是如何供容器使用的?本文将围绕Docker镜像的底层存储原理来剖析容器和镜像是如何运作的。

Union File System

UnionFS(Union File System), 是一种把其他文件系统联合到一个挂载点,提供统一视图的文件系统服务。它使用read-onlyread-write的branch把不同文件系统的文件整合,根据层级透明的覆盖低层文件来提供一个统一的文件视图。

UnionFS遵循了写时复制(copy-on-write)原则,即若文件内容没有任何修改,使用对他的使用都可以指向一个唯一的文件实体。当修改操作发生时,先复制一个新文件,之后的修改和读取操作都将基于新文件之上进行。 COW避免了无意义的文件复制,因为绝大多数文件在使用中都不会发生变化,那我们只需要对那少部分发生修改的文件进行复制就行了。

Docker使用的存储驱动

早期Docker是基于Centos开发的,所以Docker最初使用了Centos上默认集成的aufs作为存储引擎。

aufs(Advanced Multi-Layered Union Filesystem),是Docker选用的第一种存储驱动,具有快启动,内存、存储友好的优点。但是aufs一直没有合并到Linux内核主线中(据说由于aufs代码的维护性较差),所以有可能有些Linux发行版内核中默认并不支持该文件系统。

目前为止, Docker支持的存储驱动如下:

  • aufs
  • overlay/overlay2
  • btrfs
  • device mapper
  • zfs
  • vfs

本文着重解析aufsoverlay2在Docker存储中的使用。

aufs

aufs挂载和结构说明

aufs可用以下命令进行挂载:

mount -t aufs -o br={branch-0-path}={rw/ro}:...:{branch-1-path}={rw/ro} none {mount-path}
  • aufs的branch分为read-onlyread-write,可以在挂载时在options中指定,若都不指定,则默认第一个branch为read-write,其他的branch为read-only
  • branch的顺序按照参数顺序从左到右排列,第一个branch在最上层,最后一个在最下层。
  • aufs不需要dev设备,所以设备填none即可。
图1: aufs挂载结构

如图所示,对于重名文件,上层文件在统一挂载点上会覆盖下层,但是并不会影响到下层branch真实的文件内容。

修改aufs挂载点上的文件

对于只读挂载

若在挂载时制定所有分支都为read-only,那么将不被允许在挂载点上修改文件。

对于读写挂载

若branch中存在read-write,那所有修改文件的操作都会被写入可写的branch路径。若存在多个可写branch,写入的操作会遵循相应的策略,具体可查看aufs文档。这里我们按照Docker的使用方式,只考虑单个可写branch。

创建我们的工作目录:

root@master:aufs $ tree -L 2
.
├── container-layer
│   ├── container-layer.txt
├── img-layer1
│   └── img-layer1.txt
├── img-layer2
│   ├── img-layer1-fake.txt
│   └── img-layer2.txt
├── img-layer3
│   └── img-layer3.txt
├── img-layer4
│   └── img-layer4.txt
└── mnt-aufs

每个txt文件内容从上至下如下:

container-layer.txt -> I'm container-layer
img-layer1.txt -> I am layer1
img-layer1-fake.txt -> i'm fake img-layer1
img-layer2.txt -> I am layer2
img-layer3.txt -> I am layer3
img-layer4.txt -> I am layer4

ok,现在我们将container-layer作为读写层,其他作为只读层挂载到mnt-aufs路径:

root@master:aufs $ mount -t aufs -o br=./container-layer:./img-layer1:./img-layer2:./img-layer3:img-layer4 none ./mnt-aufs
root@master:aufs $ ls mnt-aufs/
container-layer.txt  img-layer1-fake.txt  img-layer1.txt  img-layer2.txt  img-layer3.txt  img-layer4.txt

挂载完成后,所有branch的文件都出现在了mnt-aufs路径下。

在mnt-aufs下修改文件

我们可以在mnt-aufs进行文件修改操作,不同的操作会对应不同的变化:

  • 对于container-layer下文件,所有变动都会直接在对应branch的原始路径下进行。即修改、删除container-layer.txt都在直接反应在container-layer路径下。
  • 对于只读层的文件,修改时会在可写层复制一个文件,之后所有基于该文件的修改都会发生在新复制的文件上,这就是COW;删除文件会在可写上创建一个.wh文件表示该文件已删除。
  • 若绕过挂载点直接去修改原始路径下的文件,不会遵循上述aufs的规则,但是修改的内容仍然会同步到挂载点下。

Docker是如何使用aufs的?

本文相关的Docker测试建议新创建Linux虚拟裸机来进行接下来的实验

修改Docker存储引擎为aufs

目前Docker默认的存储引擎是overlay2,具体信息可以通过Docker info命令查看:

root@master:aufs $ docker info | grep "Storage Driver"
 Storage Driver: aufs

可以重启docker daemon进程重新制定存储引擎:

root@master:~ $ systemctl stop docker
root@master:~ $ dockerd --storage-driver=overlay2 &

Docker Image的aufs存储

Docker的aufs存储内容默认都在/var/lib/docker/aufs路径下,结构如下:

├── diff
├── layers
└── mnt
  • diff,存储着Image Layer的内容
  • layers,存储着layer堆叠信息metadata
  • mnt, 镜像或者容器的挂载点,也就是容器最终看到的文件系统的样子

拉取一个镜像

我们在裸机上新安装的Docker没有任何镜像,所以初始状态下的aufs是空的。现在我们拉去一个ubuntu:15.04镜像.

root@master:aufs $ tree -L 2
.
├── diff
│   ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
│   ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
│   ├── a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
│   └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
├── layers
│   ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
│   ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
│   ├── a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306
│   └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
└── mnt
    ├── 0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
    ├── 2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
    ├── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
    └── b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164

## 镜像的堆叠顺序
root@master:aufs $ cat layers/b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs $ cat layers/0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs $ cat layers/2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

root@master:aufs $ cat layers/a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

可以看到,我们拉取的镜像一共有4个layer,layers中记录的堆叠信息表明了layer的顺序。

在镜像上作修改

我们创建一个Dockerfile,对镜像进行一些修改

root@master:docker $ cat Dockerfile
FROM ubuntu:15.04
RUN echo "Hello world" > /tmp/newfile
root@master:aufs $ docker build -t changed-ubuntu .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:15.04
 ---> d1b55fd07600
Step 2/2 : RUN echo "Hello world" > /tmp/newfile
 ---> Running in f88047e60424
Removing intermediate container f88047e60424
 ---> a8c4a7335b4b
Successfully built a8c4a7335b4b
Successfully tagged changed-ubuntu:latest

查看镜像使用了哪些image layer:

root@master:aufs $ docker history changed-ubuntu
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
a8c4a7335b4b   2 minutes ago   /bin/sh -c echo "Hello world" > /tmp/newfile    12B
d1b55fd07600   6 years ago     /bin/sh -c #(nop) CMD ["/bin/bash"]             0B
<missing>      6 years ago     /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$…   1.88kB
<missing>      6 years ago     /bin/sh -c echo '#!/bin/sh' > /usr/sbin/poli…   701B
<missing>      6 years ago     /bin/sh -c #(nop) ADD file:3f4708cf445dc1b53…   131MB

可以清楚看到每一层是由什么命令添加的,我们在镜像的最上层新加了一层,大小只有12B。

root@master:aufs $ tree -L 2
.
├── diff
    ...
│   └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
├── layers
    ...
│   └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
└── mnt
    ...
    └── aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da

root@master:aufs $ cat layers/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da
b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164
0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c
2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57
a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306

查看aufs下,发现多了一个layer,并且这个layer恰好加载了之前4个layer之上。查看该层diff的内容,就只有一个文件,就是我们通过Dockerfile添加的那个文件。

root@master:aufs $ cat diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/tmp/newfile
Hello world
root@master:aufs $ ls
diff  layers  mnt
root@master:aufs $ tree -L 2 diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/
diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da/
└── tmp
    └── newfile

1 directory, 1 file
root@master:aufs $

Container看见的视图

我们将容器启动:

root@master:aufs $ tree -L 2
.
├── diff
    ...
│   ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
│   └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init
├── layers
    ...
│   ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
│   └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init
└── mnt
    ...
    ├── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d
    └── 3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init

对于每一个容器,启动后会发生这么几件事:

  • 先创建一个read-only的init layer,用来存储与这个容器环境相关的内容。
  • 创建一个和init layer同名的read-write layer,用来存储容器运行时所有的文件修改操作。这个可读写层在容器被删除(docker rm)时候删除,停止容器不会被删除。
  • 容器layer堆叠的顺序为: read-write layer => read-only init layer => Image layers
图2: aufs容器挂载结构图

容器启动后,实际上就是在镜像的上方添加了read-only和read-write两个layer,我们可以清楚的查看到aufs的堆叠信息:

root@master:aufs $  cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d aufs rw,relatime,si=b35884dae88adc30,dio,dirperm1 0 0

root@master:aufs $ cat /sys/fs/aufs/si_b35884dae88adc30/*
/var/lib/docker/aufs/diff/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d=rw
/var/lib/docker/aufs/diff/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d-init=ro+wh
/var/lib/docker/aufs/diff/aaad9c0d2a6f3b3fa8485eb608101899c5d759b86899f6f572b2681f97be80da=ro+wh
/var/lib/docker/aufs/diff/b9580e61c5eb81490154112e8e5ad09490972022b7aad0fdf87a613d46758164=ro+wh
/var/lib/docker/aufs/diff/0c234f9c4a5a8d874004763367fd820ef79b355a8c112be916dfa193f5473d2c=ro+wh
/var/lib/docker/aufs/diff/2d382525b7c8a09419c1d8462ca0afad386d2fefd3276761c7d0d9519f461d57=ro+wh
/var/lib/docker/aufs/diff/a2aa1839bab06ca5c7f9cf43434b396d44725d2694bb2cc4fc1f39fe35096306=ro+wh

只有最上层是可读写的,其他全部都是只读层。而容器最终看见的文件系统,就是aufs/mnt目录下跟最上层layer同名的挂载点:

root@master:aufs $ ls mnt/3a019a3b1cc2781f20555822fe4862361ccc270cfe2d022c218821d6a0179f5d/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

至此,aufs在docker中的使用就讲解完毕了。接下来我们讲一下docker使用的另一种存储引擎overlay2,相信在我们详细解析了aufs的使用后,大家对overlay2的理解和掌握会手到擒来。

overlay2

overlay/overlay2是一种使用上和aufs非常相似的UnionFS,于Linux 3.18中被纳入到内核主线中。和aufs相比,overlay效率更快、使用简单。

overlay只能在两层的结构上工作,所以为了实现多层镜像,Docker在lower层添加了各层的硬链接,并且由于每个layer都会新建一些目录,会比较消耗系统的inode number。overlay2克服了这个问题,原生支持了多层结构(最多128层),自Docker 17.04后,默认使用overlay2作为存储引擎。

overlay2结构说明

图3: overlay2结构说明
  • lowerdir: 底层只读的branch,branch顺序越靠前则层级越高,高层会覆盖底层的同名文件,可以设置多个。
  • upperdir: 读写层,在修改时会遵循COW(copy-on-write)原则,对于底层文件复制后修改,修改upperdir则直接修改目标路径文件,层级顺序同上。需要注意的是upperdir只能设置一个目录
  • workdir: 起过渡作用,写入upper的内容会先写入到workdir,然后再移动到upper层。需要注意的是workdir必须制定一个空的路径。
  • mergedir: 统一挂载点,对外提供统一的文件视图。

用overlay2进行挂载

我们利用刚才那个文件目录,将container-layer作为upperdir,其他作为lowerdir进行挂载:

mount -t overlay overlay -o lowerdir=./img-layer1:./img-layer2:./img-layer3:./img-layer4,upperdir=container-layer,workdir=./workdir ./mnt

和aufs类似,修改mnt中的文件时有以下几种情况:

  • 修改lowerdir中的文件,遵循COW原则,在upperdir中复制一个新文件然后执行修改操作。删除lowerdir中的文件会在upperdir中生成一个特殊的字符设备文件,标记该文件已被删除。
  • 修改upperdir中的文件会直接在源路径下修改。

直接修改源路径

在刚才讲解aufs时,我们曾尝试直接去修改aufs源路径中的文件,而虽然绕过了aufs但是修改仍然同步到了联合挂载点上。这一点在overlay2上是不同的,绕过overlay2去直接修改源路径大多数情况下是不会同步到挂载点的。

root@master:overlay2 $ ls -li mnt | grep fake
1311616 -rw-r--r-- 1 root root 35 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 $ ls -li img-layer2 | grep fake
1311616 -rw-r--r-- 1 root root 35 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 $ echo "modification" >> img-layer2/img-layer1-fake.txt
root@master:overlay2 $ ls -li img-layer2 | grep fake
1311618 -rw-r--r-- 1 root root 32 Apr 19 00:34 img-layer1-fake.txt
root@master:overlay2 $ ls -li mnt
total 24
1311616 -rw-r--r-- 0 root root 35 Apr 19 00:34 img-layer1-fake.txt

原因如上所示,overlay2在挂载时会将当时各文件的inode number记录下来,而这个记录是一次性的。所以当我们直接修改底层文件时,文件的inode number发生了改变,而挂载点上的文件指向的仍然是修改前文件的inode number。

Docker是如何使用overlay2的?

Docker overlay2存储结构

当docker使用overlay2存储引擎时,镜像默认存储在宿主机的/var/lib/docker/overlay2目录下。

图4: 容器挂载视图和镜像layer结构
  • 镜像层(以该layer hashId为路径名)
name 类型 说明
link file 记录了该layer在l中的短哈希软链接名字
committed file 该layer的commit信息
lower file 记录该layer依赖的低层layer信息
work directory overlay的workdir
diff directory 该layer文件的存放为止
merged directory 仅在容器的读写层存在
  • l

    这个目录中存放的是指向各层的软连接,名字是一串短哈希。这么做的原因是如果用原始的长哈希将更容易的达到mount命令参数的页大小限制。

Container看见的视图

和aufs一样,每个Docker启动后,都会创建一个只读的init layer和一个读写layer。这是Docker在使用UnionFS一个统一的思路,将镜像层作为只读层,容器运行期间产生的文件修改都统一写入最上层的读写层。当容器被删除时,直接将这个可读写的目录删除即可。

通过docker inspect可以查看容器的挂载路径信息:

root@master:docker $ docker inspect 5860f54f3561 | grep GraphDriver -A 8
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041-init/diff:/var/lib/docker/overlay2/a0b52b932c79aa0cef156fdd9d2ac601a6bafd0bdf0ac2b44b2c7d87274c83b0/diff:/var/lib/docker/overlay2/d9da8c3a58195f856664a840bfc4459caab2b17f14ea8f99b54949eddc79a870/diff:/var/lib/docker/overlay2/955671fdd85ce04b4a71269f9d6f22114c86b24cdd8ca207e5eb9e03d4a4ed5a/diff:/var/lib/docker/overlay2/786411b49cf906bb3b49904716ace173f7be85671b0cc3eff798cde85691d6cc/diff",
                "MergedDir": "/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/merged",
                "UpperDir": "/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/diff",
                "WorkDir": "/var/lib/docker/overlay2/bcb82447d16aeb674c736f7c1e38bbd20335a1f76000bfded93794d105ee4041/work"
            },
            "Name": "overlay2"
        },

可以看到,实际的挂载点是每个layer层的diff路径,UpperDirMergedDir都是为容器创建的那个可读写的临时layer。

reference

  1. aufs 简介以及在 docker 中的使用
  2. docker-aufs文档
  3. docker-overlay文档
  4. Linux文件系统之aufs