docker 挂载只读文件夹_网易工程师实战:Docker存储驱动心路历程

本文探讨了Docker的存储机制,包括镜像是如何以分层结构存在,以及容器如何在镜像基础上创建可写层。重点讲解了Aufs和Device Mapper两种存储驱动的工作原理,解释了它们如何实现只读和读写分离,以及如何有效地管理容器和镜像的分层。此外,还介绍了Docker如何利用这些驱动实现高效的空间利用和容器共享。
摘要由CSDN通过智能技术生成

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

什么是docker存储?

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

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

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

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

基于以上问题,docker的存储经历了几个过程的发展。

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

  • 镜像和分层

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

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

878cb5de645c7f779c838c067411e2ef.png
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的呢?

77e7567b1c44c3b66b3ca855b825b70e.png

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

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

  • Device Mapper

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

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

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

f0afec65f01203a22b7d325589340f3e.png

devicemapper读写请求是如何的呢?

c9c7e0358d6b5261c07bdf8bd4b5f5fe.png

读取一个块的过程

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

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

①写入新数据过程

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

②修改已有数据过程

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

那么devicemapper的性能如何呢?

按需分配的性能影响

  • Overlay

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

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

e19f7d24113580fe0a50b4b1fc70f82d.png

如上图所示,镜像和容器同样是分层的概念,镜像在这里称为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面经超全资料~

参考链接:

https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/

http://www.infoq.com/cn/articles/analysis-of-docker-file-system-aufs-and-devicemapper

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
docker-compose 搭建的lamp+redis 代码 附上docker-compose # 标准配置文件应该包含 version、services、networks 三大部分, # 其中最关键的就是 services 和 networks 两个部分,下面先来看 services 的书写规则 # 指定版本号 version: '2' services: # 在 services 标签下的第二级标签是 console,这个名字是用户自己自定义,它就是服务名称。 console: # 为自定义的容器指定一个名称,而不是使用默认的名称 container_name: console # 容器内置名称 hostname: console # 指明路径 build: # context 指定绝对路径或者相对路径 context: ./images/console # dockerfile 指定 Dockerfile 的文件名称 dockerfile: Dockerfile # volumes_from 从其它容器或者服务挂载数据卷, # 可选的参数是 :ro 或 :rw,前者表示容器只读,后者表示容器对数据卷是可读可写的(默认情况为可读可写的)。 volumes_from: - php # 挂载一个目录或者一个已存在的数据卷容器,可以直接使用 HOST:CONTAINER 这样的格式 # ,或者使用 HOST:CONTAINER:ro 这样的格式,后者对于容器来说,数据卷是只读的,这样可以有效保护宿主机的文件系统 volumes: # 使用绝对路径挂载数据卷 - /root/.ssh/:/root/.ssh/ # 类似于使用 docker run 的效果 我也不知道 不写因为console并不是直接启动导致镜像不会产生 tty: true # web,这个名字是用户自己自定义,它就是服务名称。 web: # 为自定义的容器指定一个名称,而不是使用默认的名称 container_name: web # 容器内置名称 hostname: web # 指明路径 build: # context 指定绝对路径或者相对路径 context: ./images/nginx # dockerfile 指定 Dockerfile 的文件名称 dockerfile: Dockerfile # 映射端口 ports: - '80:80' # 此选项解决了启动顺序的问题 这个的意思是必须在php启动以后才能启动 # 注意的是,默认情况下使用 docker-compose up web 这样的方式启动 web 服务时, # 也会启动 php 服务,因为在配置文件中定义了依赖关系 depends_on: - php # volumes_from 从其它容器或者服务挂载数据卷, volumes_from: - php volumes: # 已经存在的命名的数据卷 - nginx-log:/var/log/nginx # 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器 - ./images/nginx/sites-enabled:/etc/nginx/sites-enabled - ./images/nginx/cert:/etc/nginx/cert # 加入指定网络 networks: default: # 同一网络上的其他容器可以使用服务器名称或别名来连接到其他服务的容器 aliases: - web.sunchanghao.top - mid.sunchanghao.top - sevice.sunchanghao.top - admin.sunchanghao.top # php,这个名字是用户自己自定义,它就是服务名称。 php: # 为自定义的容器指定一个名称,而不是使用默认的名称 container_name: php # 容器内置名称 hostname: php # 服务除了可以基于指定的镜像,还可以基于一份 Dockerfile, # 在使用 up 启动之时执行构建任务,这个构建标签就是 build,它可以指定 Dockerfile # 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器 build: # context 选项可以是 Dockerfile 的文件路径,也可以是到链接到 git 仓库的 url # 当提供的值是相对路径时,它被解析为相对于撰写文件的路径,此目录也是发送到 Docker 守护进程的 context context: ./images/php # 使用此 dockerfile 文件来构建,必须指定构建路径 dockerfile: Dockerfile # 挂载一个目录或者一个已存在的数据卷容器, volumes: # 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。 - ./app:/mnt/app # db,这个名字是用户自己自定义,它就是服务名称。 db: # 为自定义的容器指定一个名称,而不是使用默认的名称 container_name: db # 容器内置名称 hostname: db # 从指定的镜像中启动容器,可以是存储仓库、标签以及镜像 ID image: mysql:5.7 environment: MYSQL_USER: 'sch' MYSQL_PASS: '1111' MYSQL_ROOT_PASSWORD: 'root' volumes: - db:/var/lib/mysql ports: - '3306:3306' redis: # 为自定义的容器指定一个名称,而不是使用默认的名称 container_name: redis # 容器内置名称 hostname: redis # image 则是指定服务的镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。 image: redis:3.2.7 # 设置端口号 ports: - '6379:6379' # 挂载一个目录或者一个已存在的数据卷容器 volumes: # 已经存在的命名的数据卷。 - redis:/data # node volumes: nginx-log: # 设置volume的驱动,默认是local. driver: local db: driver: local redis: driver: local
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值