您的位置 首页 java

网易工程师实战:Docker存储驱动心路历程

如果对我的文章感兴趣。希望阅读完可以得到你的一个【三连】,这将是对我最大的鼓励和支持。

什么是docker存储?

简单来讲,就是镜像是以怎么样的方式存在于宿主机上的。这种方式对docker极为重要。因为从docker的思想上:

首先 ,有镜像这个概念(可以理解为一个打包好的文件系统);

其次 ,类似于版本控制的管理方式,不能每次修改都搞个包出来吧。这样很多重复个浪费,使用会很笨重;

最后 ,关于镜像启动出来的容器,容器的修改类似于镜像基础上的增量,怎么高效的维护每次的diff也是个问题,这决定了你起来的容器的存在形态。

理解镜像、容器、存储驱动

  • 镜像和分层

docker镜像是多个只读文件系统的分层堆叠,每层代表了在上层原有基础上的部分差异。

下图将docker镜像保存为tar包,然后查看了ubuntu镜像的分层情况,里边记录了各个层级内容及对应关系。

 root@hzboa-kse1:~# docker history ubuntu //history命令可以查看镜像的具体提交信息。
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
c5f1cf30c96b        3 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
<missing>           3 weeks ago         /bin/sh -c sed -i 's/^#s*(deb.*universe)$/   1.895 kB
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B
<missing>           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /u   701 B
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:ffc85cfdb5e66a5b4f   120.7 MB

root@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
    └── file2
root@hzboa-kse1:/tmp/temp# mount -t aufs -o br=/tmp/temp/layer1=ro:/tmp/temp/layer2=rw none /tmp/temp/aufs
mount: 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
    └── file2

root@hzboa-kse1:/tmp/temp/aufs# echo 1 >> file1
-bash: file1: Read-only file system
root@hzboa-kse1:/tmp/temp/aufs# echo 2 >> file2
root@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/aufs
mount: 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的呢?

网易工程师实战:Docker存储驱动心路历程

多容器共享一个镜像的场景

镜像内容均作为只读层被使用,每个容器需要写的时候,会再挂载一个可写层出来。

  • Device Mapper

devicemapper把所有的镜像和容器都存储在自己的虚拟设备中。

首先 ,devicemapper存储驱动会创建一个存储池; 然后 ,从池中创建出一个逻辑卷(块设备),每个新的镜像或者可写层都是基于该控设备的快照,数据被写入时才真正的消耗空间。

容器layer则是在镜像的基础上创建的快照,这样一层层下去,达到了cow的效果。

网易工程师实战:Docker存储驱动心路历程

devicemapper读写请求是如何的呢?

网易工程师实战:Docker存储驱动心路历程

读取一个块的过程

  • 应用程序通过一个地址请求容器内容
    因为容器仅仅是个镜像的快照(并没有数据),所以这里会得到一个指针,到镜像存储栈中去寻找数据
  • 存储根据指针找到镜像的对应地址
  • devicemapper从镜像拷贝对应数据到内存中对应容器中
  • 返回数据到请求程序

写操作(分为写入新数据和修改已有数据)的过程牵扯到数据的变化,这里简单介绍下过程:

①写入新数据过程

  • 应用程序请求写入数据
  • 按照最小单元(64K)分配快照,不够则分配多个连续快照
  • 写入数据到快照

②修改已有数据过程

  • 应用程序修改文件请求
  • 定位需要更新的镜像块
  • 给容器快照分配一个空块,并拷贝数据到新的块
  • 修改后的数据写入到新的块

那么devicemapper的性能如何呢?

按需分配的性能影响

  • Overlay

Overlay是一个现代化的联合文件系统,类似于Aufs,在其基础上有一个更简单的设计,自3.18加入linux内核。目前对于这项新技术,生产环境中,还是建议谨慎使用。

overlay主要在于联合挂载,原理和Aufs类似,多了一个merge操作,最终暴露单一的点给上层。

网易工程师实战:Docker存储驱动心路历程

如上图所示,镜像和容器同样是分层的概念,镜像在这里称为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”)的文件不会被删除。在容器中的特殊文件会覆盖它。

如果对我的文章感兴趣,希望可以得到您的一个点赞和关注,这将是对我最大的鼓励和支持,感谢~另外,私聊我【Java】可以收获本文章更细节的资料以及 互联网大厂Java面经 超全资料 ~

参考链接:

文章来源:智云一二三科技

文章标题:网易工程师实战:Docker存储驱动心路历程

文章地址:https://www.zhihuclub.com/178827.shtml

关于作者: 智云科技

热门文章

网站地图