Storage Driver查询
可以使用docker info命令查看你的Docker使用的storage driver,信息如下:
[root@84 tmp]# docker info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 6
Server Version: 18.03.0-ce
Storage Driver: overlay2 #使用的storage driver
Backing Filesystem: xfs
Storage对比
可以看到的本机上使用的storage driver
是overlay2
。此外,还有一个Backing Filesystem
它只你本机的文件系统,我的是extfs
,aufs
是在xfs
之上创建的。你能够使用的storage driver
是与你主机上的Backing Filesystem
有关的。
Storage driver | 后端文件系统 | 不支持的后端文件系统 |
---|---|---|
overlay | ext4 xfs | btrfs aufs overlay zfs eCryptfs |
overlay2 | ext4 xfs | btrfs aufs overlay zfs eCryptfs |
aufs | ext4 xfs | btrfs aufs eCryptfs |
btrfs | btrfs only | N/A |
devicemapper | direct-lvm | N/A |
vfs | debugging only | N/A |
zfs | zfs only | N/A |
你可以通过在docker daemon
命令中添加--storage-driver=<name>
标识来指定要使用的storage driver
,或者在/etc/default/docker
文件中通过DOCKER_OPTS
指定。
选择的storage driver
对容器中的应用是有影响的。
Storage Driver 详解
AUFS
AUFS是Docker最先使用的storage driver,它技术很成熟,社区支持也很好,它的特性使得它成为storage driver的一个好选择,使用它作为storage driver,Docker会:
- 容器启动速度很快
- 存储空间利用很高效
- 内存的利用很高效
尽管如此,仍有一些Linux发行版不支持AUFS,主要是它没有被并入Linux内核。
AUFS是一种联合文件系统,意思是它将同一个主机下的不同目录堆叠起来(类似于栈)成为一个整体,对外提供统一的视图。AUFS是用联合挂载来做到这一点。
AUFS使用单一挂载点将多个目录挂载到一起,组成一个栈,对外提供统一的视图,栈中的每个目录作为一个分支。栈中的每个目录包括联合挂载点都必须在同一个主机上。
在Docker中,AUFS实现了镜像的分层。AUFS中的分支对应镜像中的层。
此外,容器启动时创建的读写层也作为AUFS的一个分支挂载在联合挂载点上。
AUFS通过写时复制策略来实现镜像镜像的共享和最小化磁盘开销。AUFS工作在文件的层次上,也就是说AUFS对文件的操作需要将整个文件复制到读写层内,哪怕只是文件的一小部分被改变,也需要复制整个文件。这在一定成度上会影响容器的性能,尤其是当要复制的文件很大,文件在栈的下面几层或文件在目录中很深的位置时,对性能的影响会很显著。
例如:当要在一个包含很长字符串的文件中追加一个字符串时,如果这是对这个文件的第一次修改操作,意味着它当前不在最顶层的读写层。AUFS就会在下面额读写层中查找它,查找是自顶向下,逐层查找的。找到之后,就把整个文件拷贝到读写层,再对它进行修改。文件比较大时,复制操作就会很耗时间。当文件在最下面几层时,查找它的时间开销也比较大。
幸运的是,一个文件只需复制一次,此后对它的操作就在读写层进行了。
AUFS在性能方面的特性可以总结如下:
- 在容器密度比较告的场景下,AUFS是非常好的选择,因为AUFS的容器间共享镜像层的特性使其磁盘利用率很高,容器的启动时间很短;
- AUFS中容器之间的共享使对系统页缓存的利用率很高;
- AUFS的写时复制策略会带来很高的性能开销,因为AUFS对文件的第一次更改需要将整个文件复制带读写层,当容器层数很多或文件所在目录很深时尤其明显;
最后,需要说明的是,数据卷(data volumes)可以带来很好的性能表现,这是因为它绕过storage driver直接将文件卸载宿主机上,不需要使用写时复制策略。正因如此,当需要大量的文件写操作时最好使用数据卷。
Device mapper 详解
Docker
在Debian,Ubuntu系的系统中默认使用aufs,在RedHat系中使用device mapper。device mapper在Linux2.6内核中被并入内核,它很稳定,也有很好的社区支持。
device mapper
将所有的镜像和容器存储在它自己的虚拟设备上,这些虚拟设备是一些支持写时复制策略的快照设备。device mapper工作在块层次上而不是文件层次上,这意味着它的写时复制策略不需要拷贝整个文件。
device mapper创建镜像的过程如下:
- 使用device mapper的storge driver创建一个精简配置池;精简配置池由块设备或稀疏文件创建。
- 接下来创建一个基础设备;
- 每个镜像和镜像层都是基础设备的快照;这写快照支持写时复制策略,这意味着它们起始都是空的,当有数据写入时才耗费空间。
在device mapper作为storage driver的系统中,容器层container layer
是它依赖的镜像的快照。与镜像一样,container layer也支持写时复制策略,它保存了所有对容器的更改。当有数据需要写入时,device mapper就为它们在资源池中分配空间;
下图展示了资源池,基础设备和两个镜像之间的关系
device mapper中的读操作
下图展示了容器中的某个进程读取块号为0x44f的数据:
- 某个进程发出读取文件的请求;由于容器只是镜像的精简快照(thin snapshot),它并没有这个文件。但它有指向这个文件在下面层中存储位置的指针。
- device mapper由指针找到在镜像层号为a005e中的块号为0xf33的数据;
- device mapper将这个位置的文件复制到容器的存储区内;
- device mapper将数据返回给应用进程;
device mapper中的写操作
在device mapper中,对容器的写操作由“需要时分配”策略完成。更新已有数据由“写时复制”策略完成,这些操作都在块的层次上完成,每个块的大小为64KB。
向容器写入56KB的新数据的步骤如下:
- 进程向容器发出写56KB数据的请求;
- device mapper的“需要时分配”策略分配一个64KB的块给容器快照(container snapshot);如果要写入的数据大于64KB,就分配多个大小为64KB的块。
- 将数据写入新分配的块中;
device mapper在Docker中的性能表现
device mapper的性能主要受“需要时分配”策略和“写时复制”策略影响,下面分别介绍:
需要时分配(allocate-on-demand)
device mapperdriver通过allocate-on-demand策略为需要写入的数据分配数据块。也就是说,每当容器中的进程需要向容器写入数据时,device mapper就从资源池中分配一些数据块并将其映射到容器。
当容器频繁进行小数据的写操作时,这种机制非常影响影响性能。
一旦数据块被分配给了容器,对它进行的读写操作都直接对块进行操作了。
写时复制(copy-on-write)
与aufs一样,device mapper也支持写时复制策略。容器中第一次更新某个文件时,device mapper调用写时复制策略,将数据块从镜像快照中复制到容器快照中。
device mapper的写时复制策略以64KB作为粒度,意味着无论是对32KB的文件还是对1GB大小的文件的修改都仅复制64KB大小的文件。这相对于在文件层面进行的读操作具有很明显的性能优势。
但是,如果容器频繁对小于64KB的文件进行改写,device mapper的性能是低于aufs的。
存储空间使用效率
device mapper不是最有效使用存储空间的storage driver,启动n个相同的容器就复制了n份文件在内存中,这对内存的影响很大。所以device mapper并不适合容器密度高的场景。
overlayfs
OverlayFS
与AUFS相似,也是一种联合文件系统(union filesystem),与AUFS相比,OverlayFS:
- 设计更简单;
- 被加入Linux3.18版本内核
- 可能更快
overlayfs在Docker社区中获得了很高的人气,被认为比AUFS具有很多优势。但它还很年轻,在成产环境中使用要谨慎。
overlayfs中镜像的分层和共享
OverlayFS将一个Linux主机中的两个目录组合起来,一个在上,一个在下,对外提供统一的视图。这两个目录就是层layer
,将两个层组合在一起的技术被成为联合挂载union mount
。在OverlayFS中,上层的目录被称作upperdir
,下层的,目录被称作lowerdir
,对外提
供的统一视图被称作merged
。
下图展示了容器和镜像的层与OverlayFS的upperdir
,lowerdir
以及merged
之间的对应关系(图来自Docker官网Docker docs):
由上图可以看出,在一个容器中,容器层container layer也就是读写层对应与OverlayFS的upperdir,容器使用的对象对应于OverlayFS的lowerdir,容器文件系统的挂载点对应merged。
注意到,镜像层和容器曾可以有相同的文件,这中情况下,upperdir中的文件覆盖lowerdir中的文件。
OverlayFS仅有两层,也就是说镜像中的每一层并不对应OverlayFS中的层,而是,镜像中的每一层对应/var/lib/docker/overlay中的一个文件夹,文件夹以该层的UUID命名。然后使用硬连接将下面层的文件引用到上层。这在一定程度上节省了磁盘空间。这样,OverlayFS中的lowerdir就对应镜像层的最上层,并且是只读的。在创建镜像时,Docker会新建一个文件夹作为OverlayFS的upperdir,它是可写的。
overlayfs中镜像和容器的结构
执行docker images -a查看ubuntu镜像都由哪些层组成:
$ docker images -a
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu latest 1d073211c498 7 days ago 187.9 MB
<none> <none> 5a4526e952f0 7 days ago 187.9 MB
<none> <none> 99fcaefe76ef 7 days ago 187.9 MB
<none> <none> c63fb41c2213 7 days ago 187.7 MB
然后查看/var/lib/docker/overlay下的文件夹:
$ ls -l /var/lib/docker/overlay/
total 24
drwx------ 3 root root 4096 Oct 28 11:02 1d073211c498fd5022699b46a936b4e4bdacb04f637ad64d3475f558783f5c3e
drwx------ 3 root root 4096 Oct 28 11:02 5a4526e952f0aa24f3fcc1b6971f7744eb5465d572a48d47c492cb6bbf9cbcda
drwx------ 5 root root 4096 Oct 28 11:06 99fcaefe76ef1aa4077b90a413af57fd17d19dce4e50d7964a273aae67055235
drwx------ 3 root root 4096 Oct 28 11:01 c63fb41c2213f511f12f294dd729b9903a64d88f098c20d2350905ac1fdbcbba
可以看出,镜像中的每一层在/var/lib/docker/overlay文件夹下都有一个文件夹和它对应,文件夹以镜像层的UUID命名。文件夹存储了本层独有的文件和指向它下面各层文件的硬连接。
使用docker ps命令查看当前正在运行的容器的ID:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
73de7176c223 ubuntu "bash" 2 days ago Up 2 days stupefied_nobel
这个容器的数据存储在/var/lib/docker/overlay/73de7176c223...文件夹下,文件夹以容器的ID命名。执行ls -a命令查看具体文件:
$ ls -l /var/lib/docker/overlay/73de7176c223a6c82fd46c48c5f152f2c8a7e49ecb795a7197c3bb795c4d879e
total 16
-rw-r--r-- 1 root root 64 Oct 28 11:06 lower-id
drwxr-xr-x 1 root root 4096 Oct 28 11:06 merged
drwxr-xr-x 4 root root 4096 Oct 28 11:06 upper
drwx------ 3 root root 4096 Oct 28 11:06 work
这就是OverlayFS的核心内容了。lower-id文件保存了当前容器依赖镜像的最上层的UUID,并将其作为lowerdir;upper文件夹就是容器的读写层read-write layer,对容器的所有修改都保存在这个文件夹里;merged文件夹就是容器文件系统的挂载点,容器通过它提供统一的视角。对容器的任何修改都会立即在这个文件夹里得到反应;work文件夹需要OverlayFS来发挥作用,它用来支持像copy-up这样的操作。
overlayfs中容器的读写操作
读文件:
- 要读的文件不在container layer中:那就从lowerdir中读,会耗费一点性能;
- 要读的文件之存在于container layer中:直接从upperdir中读;
- 要读的文件在container layer和image layer中都存在:从upperdir中读文件;
修改文件
- 第一次修改一个文件的内容:第一次修改时,文件不在container layer(upperdir)中,overlay driver调用copy-up操作将文件从lowerdir读到upperdir中,然后对文件的副本做出修改。
需要说明的是,overlay的copy-up操作工作在文件层面,不是块层面,这意味着对文件的修改需要将整个文件拷贝到upperdir中。索性下面两个事实使这一操作的开销很小: - copy-up操作仅发生在文件第一次被修改时,此后对文件的读写都直接在upperdir中进行;
- overlayfs中仅有两层,这使得文件的查找效率很高(相对于aufs)。
- 删除文件和目录:
- 删除文件:文件被删除时,和aufs一样,相应的whiteout文件被创建在upperdir。并不删除容器层(lowerdir)中的文件,whiteout文件屏蔽了它的存在。
- 删除文件夹:删除一个文件夹时,一个“遮挡目录”(opaque dir)被创建在upperdir中,它的作用与whitout文件一样,屏蔽了lowerdir中文件夹的存在。
在Docker中使用overlayfs
OverlayFS在Linux3.18版本中被并入内核,所以要使用overlayfs,请确保你的系统的内核版本大于等于3.18。overlayfs可以工作在各种Linux文件系统上,但目前比较推荐extfs。
在进行下列操作之前,如果你本机上有需要保存的镜像,使用docker push将它们保存到Docker Hub或其他的镜像库当中。
下面是在Docker中使用overlayfs的步骤:
- 如果docker daemon正在运行,停止它;
- 检查你的内核版本和overlay模块的按装情况:
$ uname -r
3.19.0-21-generic
$ lsmod | grep overlay
overlay
使用overlaystorage driver启动docker daemon:
$ docker daemon --storage-driver=overlay &
此外,你可以在/etc/default/docker文件中通过配置DOCKER_OPTS使overlay作为Docker的默认storage driver。
overlayfs在Docker中的性能表现
总体上,overlay要比aufs和device mapper快一点,在某些场景下甚至比btrfs快。下面是对overlay性能影响较大的几个方面:
- 页缓存(page caching):overlayfs支持页缓存的共享,这意味着多个使用同一文件的容器可以共享同一页缓存,这使得overlayfs具有很高的内存使用效率;
- copy-up操作:overlay的拷贝操作工作在文件层面上,也就是对文件的第一次修改需要复制整个文件,这回带来一些性能开销,在修改大文件时尤其明显。
但overlay的拷贝操作比aufs还是快一点,因为aufs有很多层,而overlay只有两层,所以overlay在文件的搜索方面相对于aufs具有优势。 - i节点限制:使用overlay作为storage driver会消耗大量的i节点,随着镜像和容器数量的增长这种消耗尤其显著,这在一定程度上限制了overlay的使用。