如果对我的文章感兴趣。希望阅读完可以得到你的一个【三连】,这将是对我最大的鼓励和支持。
什么是docker存储?
简单来讲,就是镜像是以怎么样的方式存在于宿主机上的。这种方式对docker极为重要。因为从docker的思想上:
首先,有镜像这个概念(可以理解为一个打包好的文件系统);
其次,类似于版本控制的管理方式,不能每次修改都搞个包出来吧。这样很多重复个浪费,使用会很笨重;
最后,关于镜像启动出来的容器,容器的修改类似于镜像基础上的增量,怎么高效的维护每次的diff也是个问题,这决定了你起来的容器的存在形态。
基于以上问题,docker的存储经历了几个过程的发展。
理解镜像、容器、存储驱动
- 镜像和分层
docker镜像是多个只读文件系统的分层堆叠,每层代表了在上层原有基础上的部分差异。
下图将docker镜像保存为tar包,然后查看了ubuntu镜像的分层情况,里边记录了各个层级内容及对应关系。
root@hzboa-kse1:~# docker history ubuntu //history命令可以查看镜像的具体提交信息。IMAGE CREATED CREATED BY SIZE COMMENTc5f1cf30c96b 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B 3 weeks ago /bin/sh -c sed -i 's/^#s*(deb.*universe)$/ 1.895 kB 3 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B 3 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /u 701 B 3 weeks ago /bin/sh -c #(nop) ADD file:ffc85cfdb5e66a5b4f 120.7 MBroot@hzboa-kse1:~/docker# cat repositories //这里记录了镜像最上层的layer信息,剩下的可以在各layer中找到parent层。{"ubuntu":{"latest":"13eba6c5df21e17aadf8b4ad60839a6bc1746e24a712f9a4c77c7aa525fc28ac"}}
docker存储驱动负责做的事情是什么呢?就是站在上帝视角对这些分层的统一管理。
看了上边的分层结构,那么问题来了,docker为什么要搞这么复杂?创建容器是个什么流程?这些layer怎么用?优势在哪里?
接下来剖析,为了解决上边的疑问,先从容器创建说起。镜像即模板,一般来说是只读的,当创建一个容器后,会在指定镜像上创建一个新的、可写的layer出来,可以在这层做新增文件、对已有文件修改等操作,不会更改原有镜像层。
- 容器和分层
以上讨论了镜像,接下来谈谈容器。
容器被认为是运行中的景象,其本质区别主要在于顶端的可写层。
所谓docker 存储驱动负责启用和管理镜像层和可写层,这里是用的两个关键技术是stackable image layers和 copy-on-write 。
Docker 1.10引入了一个目录可寻址的存储模式,以前依靠随机生成的UUID哈希和UUID关联对应。之后官方也提供了镜像及容器迁移的工具。
并且目前使用docker history查看镜像分层信息看到的miss也是由于这种新的模式导致的,但是使用上没有任何影响,原因是记录层级关系放在了一个文本文件中,并不是在每一层的内容中。而目前文本文件中只有一个,所以其他为Miss状态。
- 插件式存储驱动设计、几大存储驱动的介绍 、Aufs
这是Docker最开始使用的存储驱动,相对来说更稳定一些,社区等开发者资源都比较丰富,比较适合一下应用场景:
- 对容器启动时间有要求
- 存储资源和内存的高利用率
但是aufs并没有被放进内核,所以在使用时候,需要手动加载需要的模块,docker为应对兼容性问题,在之后也推出了devicemapper的存储驱动。
Aufs是一个联合文件系统,顾名思义就是在文件系统中再进行挂载并覆盖已有内容,之前说过镜像是一种分层结构,Aufs拿到镜像后,会把镜像作为readonly层先挂载,AUFS 支持为每一个成员目录(类似Git的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限。
重点在于,写操作是在read-only上的一种增量操作,不影响read-only目录。当挂载目录的时候要严格按照各目录之间的这种增量关系,将被增量操作的目录优先于在它基础上增量操作的目录挂载,待所有目录挂载结束了,继续挂载一个read-write目录,如此便形成了一种层次结构。
Aufs是唯一一个可以实现容器间共享运行库的Driver,所以在高密度的PAAS平台及跑成千上万个拥有相同代码或运行库是时候,非常适合。
下边简单演示Aufs的使用,可以帮助我们更好的理解镜像和容器。
root@hzboa-kse1:/tmp/temp# tree.├── aufs├── layer1│ └── file1└── layer2 └── file2root@hzboa-kse1:/tmp/temp# mount -t aufs -o br=/tmp/temp/layer1=ro:/tmp/temp/layer2=rw none /tmp/temp/aufsmount: warning: /tmp/temp/aufs seems to be mounted read-only.- -o 指定mount传递给文件系统的参数- br 指定需要挂载的文件夹,这里包括dir1和dir2- ro/rw 指定文件的权限只读和可读写- none 这里没有设备,用none表示root@hzboa-kse1:/tmp/temp# tree.├── aufs│ ├── file1│ └── file2├── layer1│ └── file1└── layer2 └── file2root@hzboa-kse1:/tmp/temp/aufs# echo 1 >> file1-bash: file1: Read-only file systemroot@hzboa-kse1:/tmp/temp/aufs# echo 2 >> file2root@hzboa-kse1:/tmp/temp/aufs#
如果layer1和layer2有相同文件怎么办呢?将layer1文件重命名为file2,file内容为他们的绝对路径。
root@hzboa-kse1:/tmp/temp# mount -t aufs -o br=/tmp/temp/layer1=ro:/tmp/temp/layer2=rw none /tmp/temp/aufsmount: warning: /tmp/temp/aufs seems to be mounted read-only.root@hzboa-kse1:/tmp/temp# cat aufs/file2/root/temp/layer1
由此可见,在挂载的过程中,mount命令按照命令行中给出的文件夹顺序挂载。若出现有同名文件的情况,则以先挂载的为主,其他的不再挂载。这也说明了的Docker镜像为什么采用增量的方式:完全是利用Aufs的特性达到节约空间的目的。
Dcoker是怎么用Aufs的呢?
多容器共享一个镜像的场景
镜像内容均作为只读层被使用,每个容器需要写的时候,会再挂载一个可写层出来。
- Device Mapper
devicemapper把所有的镜像和容器都存储在自己的虚拟设备中。
首先,devicemapper存储驱动会创建一个存储池;然后,从池中创建出一个逻辑卷(块设备),每个新的镜像或者可写层都是基于该控设备的快照,数据被写入时才真正的消耗空间。
容器layer则是在镜像的基础上创建的快照,这样一层层下去,达到了cow的效果。
devicemapper读写请求是如何的呢?
读取一个块的过程
- 应用程序通过一个地址请求容器内容
因为容器仅仅是个镜像的快照(并没有数据),所以这里会得到一个指针,到镜像存储栈中去寻找数据 - 存储根据指针找到镜像的对应地址
- devicemapper从镜像拷贝对应数据到内存中对应容器中
- 返回数据到请求程序
写操作(分为写入新数据和修改已有数据)的过程牵扯到数据的变化,这里简单介绍下过程:
①写入新数据过程
- 应用程序请求写入数据
- 按照最小单元(64K)分配快照,不够则分配多个连续快照
- 写入数据到快照
②修改已有数据过程
- 应用程序修改文件请求
- 定位需要更新的镜像块
- 给容器快照分配一个空块,并拷贝数据到新的块
- 修改后的数据写入到新的块
那么devicemapper的性能如何呢?
按需分配的性能影响
- Overlay
Overlay是一个现代化的联合文件系统,类似于Aufs,在其基础上有一个更简单的设计,自3.18加入linux内核。目前对于这项新技术,生产环境中,还是建议谨慎使用。
overlay主要在于联合挂载,原理和Aufs类似,多了一个merge操作,最终暴露单一的点给上层。
如上图所示,镜像和容器同样是分层的概念,镜像在这里称为lower dir,容器称为upper dir。
正常情况,不同层通过mount体现在container mount层。
当各层之间有同名文件,即冲突的时候,upper dir会成为显性,lower dir变为隐形。显性层的文件会覆盖隐性层,这个动作就是merged操作,最终提供容器层只有一个版本。
读的话主要分以下几种情况:
- 不存在于容器层:文件不存在于容器层,则会按照upper dir、lower dir顺序向下寻找,这样可以减少一些性能上的开销,不必每次都读到镜像层。
- 存在于容器层:直接从容器中读出即可。
- 存在于容器层和图像层:如果文件存在于容器和镜像层,容器层的版本会被读取。这是因为,在容器层(“upperdir”)文件覆盖掉了镜像层(“lowerdir”)中的同名隐性文件。
写情况同样分为以下几种:
- 首次写入一个文件:容器中首次修改一个现有文件,该文件不存在于容器(“upperdir”)。overlay执行一个copy_up操作将文件从镜像(“lowerdir”)复制到容器(“upperdir”)。容器然后将更改写入该文件在容器层中的新副本。
然而,OverlayFS工作在文件级而不是块级。这意味着所有OverlayFS复制了操作复制整个文件,即使文件很大,修改很小。这对Docker写入性能产生显著影响。然而,两件事情值得注意:该copy_up操作仅发生第一次修改已有的文件。后续写入操作的是已复制到容器中的副本。OverlayFS仅适用于两层。这意味着,性能应该比AUFS镜像中很多层时搜索文件时的延迟更好。 - 删除文件和目录:当删除容器中的一个文件时,会在容器中创建一个特殊文件。镜像层(“lowerdir”)的文件不会被删除。在容器中的特殊文件会覆盖它。
如果对我的文章感兴趣,希望可以得到您的一个点赞和关注,这将是对我最大的鼓励和支持。
参考链接:
https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/