前言
我们知道,Docker容器中的进程实际上就是跑在Linux Namespace中,与外部全局Namespace隔离开来(Docker容器是如何隔离的?)。但是我们在Docker容器中看见的文件视图是如何来的呢? 容器镜像是如何供容器使用的?本文将围绕Docker镜像的底层存储原理来剖析容器和镜像是如何运作的。
Union File System
UnionFS(Union File System), 是一种把其他文件系统联合到一个挂载点,提供统一视图的文件系统服务。它使用read-only
或read-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
本文着重解析aufs
和overlay2
在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-only
和read-write
,可以在挂载时在options
中指定,若都不指定,则默认第一个branch为read-write,其他的branch为read-only
。 - branch的顺序按照参数顺序从左到右排列,第一个branch在最上层,最后一个在最下层。
- aufs不需要dev设备,所以设备填none即可。

如图所示,对于重名文件,上层文件在统一挂载点上会覆盖下层,但是并不会影响到下层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

容器启动后,实际上就是在镜像的上方添加了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结构说明
- 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
目录下。
- 镜像层(以该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路径,UpperDir
和MergedDir
都是为容器创建的那个可读写的临时layer。