Docker的本质(二):存储

简介

Docker的存储目录是由一系列层叠有序的目录构成。Docker镜像是只读的,对于Docker镜像而言,所有层都是只读的。Docker容器是在镜像层的基础上堆叠了一个可读写层(也称作容器层), 文件的新增,修改和删除只会影响到容器层,不会对镜像层做任何改动。


本文仅仅介绍OverlayFS, 他是docker官方推荐的union filesystem,类似AUFS,但是要比AUFS高效。本为所使用的OverlayFS为overlay2,与overlay最大的不同是可挂载多个lower层。

图中upperdir对应可读写层,只能有一层;lowerdir为只读层,可以有多个层;merged是联合挂载upperdir和多个lowerdir后的合并视图。Docker容器在启动后只操作merged目录。upperdir层由OverlayFS驱动来操作。

可执行 docker info |grep Storage 查看overlayfs2是否开启

本篇文章介绍的存储不包含volume

OverlayFS 示例

下面这个例子演示了overlay2的特性
首先创建lower1 lower2 upper merged work目录,并建一些演示文件:

1
2
3
4
5
mkdir lower1 lower2 upper merged work
mkdir -p lower1/usr/bin lower2/usr/bin
touch lower1/usr/bin/ps lower2/usr/bin/top
echo 1 >lower1/foo
echo 2 >lower2/foo

执行后的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── merged
├── upper
├── lower1
│   ├── foo
│   └── usr
│   └── bin
│   └── ps
├── lower2
│   ├── foo
│   └── usr
│   └── bin
│   └── top
└── work

  • lower1,lower2,lower目录,并定义层级lower1 > lower2;lower层只读;
  • upper 代表 图中的 upper,读写层。读写操作有OverlayFS完成;
  • merged 代表lower和upper合并后的视图,正常操作均在该目录下进行。merged目录的操作与普通目录无差别;
  • work 为OverlayFS的工作目录,可用来解决原子性等问题,用户无需关心。

执行overlay挂载

1
mount -t overlay overlaytest -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged

挂载后可看到merged目录已经有文件了,并且是lower,upper合并后的视图。

1
2
3
4
5
6
merged
├── foo
└── usr
└── bin
├── ps
└── top

lower1会覆盖lower2中的同名文件,例如执行cat merged/foo返回为1。
下列操作如果无特殊说明,均在merged目录执行。

1. 新增文件

新增加的文件会写到upper目录,例如执行touch bar

1
2
upper
└── bar

2. 文件更改

文件更改会采用copy-on-write的策略,如果改写某个文件,就会将改写后的内容写到upper层。比如我们用vim来编辑merged/foo文件,再写回硬盘,从merged目录来看就是文件被更改了。

1
2
3
upper
├── bar
└── foo

以后再访问到的foo文件就仅仅是upper层中的foo文件。

3. 文件删除

  • 如果文件仅仅在upper层中存在,则直接删除
  • 如果文件在lower层中也存在,则会在upper层中创建whiteout文件(相当于删除标记),所以对镜像中文件的删除并不会使总体积减小。

例如我们删除刚刚新建的bar文件和变更的foo文件, 再查看upper目录

1
2
total 0
c--------- 1 root root 0, 0 Dec 31 23:21 foo

由于bar文件是新建的,所以会被直接删除。foo文件是变更的文件,在删除刚刚在upper中创建的foo文件同时,会创建一个主次设备号均为0的字符设备文件作为删除标记,也称作whiteout文件。它会屏蔽在merged文件中查看到底层的foo文件。如果我们把该whiteout文件删除,就可以重新在merged目录查看到lower层中的foo文件。也可以通过mknod foo c 0 0命令在upper目录手工创建一个主次设备号均为0的字符设备文件,相当于在merged目录执行了对foo文件的删除操作。

删除src目录,然后查看upper目录。可以看到无论删除的是目录或者文件,所创建的whiteout文件是相同类型的

1
2
3
total 0
c--------- 1 root root 0, 0 Dec 31 23:30 foo
c--------- 1 root root 0, 0 Dec 31 23:30 usr

这里有一个原子性的问题,以删除foo文件为例:foo文件是lower中的文件,但是由于文件变更,导致upper层中存在foo文件。此时如果删除foo文件,如果不借助其他目录,需要有两步操作即删除upper层中的foo文件和创建whiteout文件;这样做存在一个问题,就是操作不是原子的。如果机器突然断电,可能会导致的不一致的状态。此时就需要用到work目录。OverlayFS驱动不会先删除upper中的foo文件,而是在work目录中创建foo的whiteout文件, 然后通过类似mv work/foo upper/foo的方式来实现,这样就保证了操作的原子性。

Docker 的存储

1. 一个运行时容器的存储结构

Docker是如何应用OverlayFS的呢?

我们首先启动一个nginx容器docker run --name nginx -d nginx (本例执行时latest是1.15.8)
然后执行mount -l -t overlayfs,可以看到nginx容器也是挂载了overlay类型的目录

1
overlay on /var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/SFPKFCKIBLEDS4GLUZEMANESLJ:/var/lib/docker/overlay2/l/UB726CVMTDAFJONXKWXQVFUD5S:/var/lib/docker/overlay2/l/QSBRVI4V3HWII324Q3PNN6GH65:/var/lib/docker/overlay2/l/BCI7OZD4QZDMT5GCW33JX4Y6JM,upperdir=/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/diff,workdir=/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/work)

对上述输出的目录整理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MergedDir:
/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/merged

UpperDir:
/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/diff

LowerDir:
/var/lib/docker/overlay2/l/SFPKFCKIBLEDS4GLUZEMANESLJ -> ../ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a-init/diff
/var/lib/docker/overlay2/l/UB726CVMTDAFJONXKWXQVFUD5S -> ../99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/diff
/var/lib/docker/overlay2/l/QSBRVI4V3HWII324Q3PNN6GH65 -> ../de526fca511cf0d38fe616df2d15b727e0fb8b2f3236e6524662888423c3035b/diff
/var/lib/docker/overlay2/l/BCI7OZD4QZDMT5GCW33JX4Y6JM -> ../b573fc5211e51727cd00d335b663006b0a54e12aee8543fda98a0ad078b97194/diff

WorkDir:
/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/work

lowerdir中的/var/lib/docker/overlay2/l/*文件是层文件的符号链接。主要是为了避免mount的参数超过页大小。

这里的目录信息其实和用docker inspect查到的信息是一致的:
docker inspect --format '' nginx | python -m json.tool

1
2
3
4
5
6
{
"LowerDir": "/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a-init/diff:/var/lib/docker/overlay2/99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/diff:/var/lib/docker/overlay2/de526fca511cf0d38fe616df2d15b727e0fb8b2f3236e6524662888423c3035b/diff:/var/lib/docker/overlay2/b573fc5211e51727cd00d335b663006b0a54e12aee8543fda98a0ad078b97194/diff",
"MergedDir": "/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/merged",
"UpperDir": "/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/diff",
"WorkDir": "/var/lib/docker/overlay2/ff688af2cfcf817bfb0abb4dcb351c2bbcd9be0ad393d8b5fe7cd17b9419068a/work"
}

容器实际chroot的就是MergedDir目录。

下面着重关注LowerDir:
99cd44e6.../diffb573fc52.../diff 是镜像层

  1. b573fc52... 是最底层,仅包含两diff目录和link文件。link文件的内容是BCI7OZD4QZDMT5GCW33JX4Y6JM,他是盖层目录ID的简写, 其实就是对应/var/lib/docker/overlay2/l/BCI7OZD4QZDMT5GCW33JX4Y6JM。diff目录中存放的是就是该层镜像的内容
  2. de526fca... 其实对于每一层而言,每一层本身就相当于upper层,对应diff目录,然后叠加在lower层之上。所以除了最底层之外的层都包含了一个lower文件,里面的内容是相对于改层的lower层的简写ID。回从/var/lib/docker/overlay2/l/映射回真实目录
  3. 99cd44e6... 该层是nginx镜像的顶层,lower文件的内容是l/QSBRVI4V3HWII324Q3PNN6GH65:l/BCI7OZD4QZDMT5GCW33JX4Y6JM。可见较高的层中的lower会写出所有的lower层,并且lower层ID越靠前,该lower层的层级越高
  4. ff688af2...-init -init层不是不是镜像层,也不是容器层。是Docker特意生成的层,仅存放变动的/etc/hostname, /etc/hosts等文件,在容器提交时为避免吧hostname等提交了,所以该层会忽略
  5. ff688af2.../diff 可读写层。对merged的增删改会在该层反应出来
  6. ff688af2.../merged 合并视图,容器真正的root目录
  7. 如果存在多个nginx容器,则多个容器会创建单独的,-init层和容器层。复用99cd44e6.../diffb573fc52.../diff

2. 容器镜像

我们再来关注下nginx容器镜像
docker image inspect --format '' nginx | python -m json.tool

1
2
3
4
5
6
{
"LowerDir": "/var/lib/docker/overlay2/de526fca511cf0d38fe616df2d15b727e0fb8b2f3236e6524662888423c3035b/diff:/var/lib/docker/overlay2/b573fc5211e51727cd00d335b663006b0a54e12aee8543fda98a0ad078b97194/diff",
"MergedDir": "/var/lib/docker/overlay2/99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/merged",
"UpperDir": "/var/lib/docker/overlay2/99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/diff",
"WorkDir": "/var/lib/docker/overlay2/99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/work"
}

整理一下:

1
2
3
4
5
6
UpperDir:
/var/lib/docker/overlay2/99cd44e60ddf02499cf842292006b7d0ca10339c40c7f3dcbd88b0c2916b8adb/diff

LowerDir:
/var/lib/docker/overlay2/de526fca511cf0d38fe616df2d15b727e0fb8b2f3236e6524662888423c3035b/diff
/var/lib/docker/overlay2/b573fc5211e51727cd00d335b663006b0a54e12aee8543fda98a0ad078b97194/diff

可以看到该镜像的层是从 99cd44e6.../diffb573fc52.../diff.
镜像在pull的时候会到仓库分层下载镜像。如果层存在,则不会重复下载,如果不存在或下载archive后的层文件并解压到/var/lib/docker/overlay2/目录

虽然镜像层也有UpperDir。但是我们并不会去改动镜像层的东西,因为镜像是只读的。

3. 关于ID

执行docker image inspect --format '' nginx | python -m json.tool
查看nginx镜像所包含的层

1
2
3
4
5
[
"sha256:7b4e562e58dcb7fbe1e27bb274f0ff8bfeb2fd965203380436e159df9f218900",
"sha256:c9c2a36960802924221f5b8fab90ed09b5900b346129979da9488810d8669e06",
"sha256:b7efe781401dfe8d05a9e4c920dd3cd430593a483c442831a14413e2738cd968"
]

可以看到sha256后跟的ID和之前描述的目录不是对应的。/var/lib/docker/overlay2/目录下的ID其实只是每层目录的cache id。cache id是随机生成的,他与层id的映射关系是由docker来完成的。

Docker v1.10之前,镜像提交的时候,docker会创建对应的镜像。用随机生成的256-bit UUID标识作为层的ID。docker用来存储镜像内容的文件目录也与该ID关联(ID的HEX),目录中包含父镜像ID,Docker通过可以通过每个镜像的父ID遍历镜像的所有层。这种方式是废弃的,原因是每个层没有校验码,存在安全问题。

Docker v1.10之后,镜像和层不再是等同的。而是一个镜像直接关联几个层,通过这些层导出容器的文件系统。层的ID就将该层内容经过sha256计算后得到的hash值。镜像就是一个mainfest文件,文件中定义了该镜像的元数据和所包含的层,该mainfest文件的sha256散列就是镜像的ID。

在下载镜像时首先拉取镜像的mainfest文件获取层信息,然后拉取各个本地不存在的层。层在下载完成后会计算sha256散列值,如果与层的ID符合,说明该层数据是正确的,起到对层的校验作用。

层ID的sha256散列是安全散列算法,因此可以确保不会出现ID碰撞。可参考此处了解该特性

因此对于镜像的拉取,有了额外的补充,就是通过sha256的id来拉取镜像。
docker image inspect --format '' nginx | python -m json.tool

1
2
3
[
"nginx@sha256:b543f6d0983fbc25b9874e22f4fe257a567111da96fd1d8f1b44315f1236398c"
]

因为docker仓库会被攻击,tag可被轻易的覆盖,通过指定tag拉取的镜像并不能保证就是正确的镜像。而sha256的ID是唯一的,镜像一旦生成,该ID就确定了,即使docker仓库被攻击tag被覆盖了,通过sha256拉取的还是正确的镜像。

参考文章

https://www.datalight.com/blog/2016/01/27/explaining-overlayfs-%E2%80%93-what-it-does-and-how-it-works/
https://docs.docker.com/storage/storagedriver/overlayfs-driver/
https://windsock.io/explaining-docker-image-ids/
https://blog.csdn.net/luckyapple1028/article/details/78075358

0%