Docker实战-镜像和容器的存储结构
一、镜像、容器和存储驱动的关系
前面也已经讲过说,镜像是程序和文件的集合,容器是镜像的运行实例。
Docker为了节约存储空间共享数据会对镜像和容器进行分层,不同镜像可以共享相同数据,并且在镜像上为容器分配一个RW层来加快容器的启动顺序
1. 镜像层和容器层
每个镜像都由多个镜像层组成,从下往上以栈的方式组合在一起形成容器的根文件系统,Docker的存储驱动用于管理这些镜像层,对外提供单一的文件系统
当容器启动时,Docker就会创建thin类型的可读写容器层,使用预分配存储空间,也就是开始时并不分配存储空间,当需要新建文件或修改文件时,才从存储池中分配一部分存储空间
每个容器运行时都有自己的容器层,保存容器运行相关数据(所有文件变化数据),因为镜像层是只读的,所以多个容器可以共享同一个镜像
删除容器时,Docker Daemon会删除容器层,保留镜像层
2. 镜像存储方式
为了区别镜像层,Docker为每个镜像层都计算了UUID,在Docker 1.10之前根据镜像层中的数据产生一个随机码作为UUID;Docker 1.10版本中则是根据镜像层中的数据使用加密哈希算法生成UUID,这种方式避免了UUID冲突并且也保证了在pull、push、load等操作中的镜像数据完整性,而容器层仍然是采用随机数方式生成UUID
在Docker 1.10版本前,镜像之间不能共享镜像层,而在Docker 1.10版本中,只要镜像层的数据相同就可以在不同镜像之间共享。当Docker升级到1.10版本后,宿主机上可能存在以前下载的镜像仍使用旧的存储方式,当Docker 1.10版本的Docker Daemon第一次启动时会自动迁移镜像,使用加密哈希算法重新计算每个镜像层的UUID,当重新计算UUID会花费很长时间并且迁移过程中Docker Daemon不能响应其他Docker命令,在实际中并不推荐
可以通过Docker 官方的镜像升级工具完成这项工作,这样升级Docker后,Docker Daemon看可以立即响应Docker命令
过程描述:
下载迁移工具的镜像 docker pull docker/v1.10-migrator
启动镜像 docker run –rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator
提示:在启动镜像时,需要把宿主机存放镜像的目录挂载到容器中即/var/lib/docker
3. 写时复制策略(Copy On Write)
Docker中的存储驱动用于管理镜像层和容器层,不同的存储驱动采用不同的算法和管理方式,在管理中使用的两大技术是栈层式管理和写时复制
写时复制采用了共享和复制技术,针对相同的数据系统只保留一份,所有操作都访问这一份数据。当有操作需要修改或添加数据时,操作系统会把这部分数据复制到新的地方再进行修改或添加,而其他操作仍然访问原数据区数据,通过这项技术节约了镜像的存储空间,加快了系统启动时间
(1)使用共享技术减少镜像存储空间
所有镜像层和容器层都保存在宿主机的文件系统/var/lib/docker/中,由存储驱动进行管理
在拉取ubutnu的时候可以看到有如下输出,表明该版本Ubuntu由五个镜像层组成,下载镜像时实际上就是下载了这五个镜像层,这些镜像层都在宿主机的文件系统中,每层都有独立的目录,在Docker 1.10之前的版本中,目录的名字和镜像的UUID相同,而Docker 1.10后则采用了新的存储方式
可以看到目录名和下载镜像的UUID并不相同
尽管存储方式不尽相同,但在所有版本的Docker中都可以共享镜像层。在下载镜像时,Docker Daemon会检查景象中的镜像层与宿主机文件系统中的镜像层进行对比,如果存在则不下载,只下载不存在的镜像层
通过Dockfile创建了一个Changed-ubuntu镜像,通过docker history可以查看镜像的镜像层及镜像大小,可以看到新建的镜像层占用空间为0B,也就是说changed-ubuntu镜像只会占用额外的一丢丢空间而不需要为其他的五个镜像层分配存储空间
由此可以看出,Docker通过共享技术,非常节约存储空间
(2)使用复制技术加快容器启动时间
容器中所有的写操作都是发生在容器层,而下面的镜像层都是只读的。当容器修改文件时,Docker通过存储驱动,发起一个写时复制操作
如果容器中存在许多修改文件或新建文件相关操作会使得容器占用更多存储空间(容器修改时都会将原有内容复制到容器层,不同存储驱动复制的基本单位不同),当写操作数量较多时,最好使用挂载卷(挂载卷的读写操作都会绕过存储驱动)
但这种复制的系统开销仅发生一次,之后的操作都是在容器层中完成,不会造成额外开销
当启动容器时,Docker Daemon只需要为容器新建一个可写的数据层,而不用复制所有镜像层,从而使得容器启动时间减少
4. 数据卷和存储驱动
Docker使用数据卷(宿主机的文件或目录)来保证数据持久性,在启动容器时,会把数据卷挂载到容器中,所有数据卷读写操作直接工作在宿主机的文件系统,不受存储驱动管理
删除容器时,数据卷的数据保存在宿主机中,不会删除;所有不在数据卷的数据都会被删除
存储驱动管理所有Docker Daemon生成的容器,每种存储驱动都是基于Linux文件系统或卷管理工具,通过命令docker info
可以查看当前使用的存储驱动
本机Docker Daemon使用的存储驱动为Overlay,后端文件系统为extfs,即Overlay存储驱动工作在extfs文件系统上
在启动Docker Daemon时可以通过–storage-driver=<驱动名>来设置存储驱动,常见的文件驱动有AUFS、Devicemapper、Btrfs、ZFS、Overlay … …
在选择存储驱动的时候,需要考虑稳定性和成熟性,不同Linux发行版本安装Docker时会根据操作系统的不同选择对应的存储驱动,所有默认的存储驱动对于当前机器是最稳定的
二、AUFS存储驱动
AUFS是一种Union FS,它将不同的目录合并为一个目录做成一个虚拟文件系统
AUFS是Docker最早使用的存储驱动,非常稳定,但是很多Linux发行版本都不支持AUFS
1. AUFS中的镜像
AUFS使用联合挂载技术,通过栈的方式将目录组装起来挂载在一个单一的挂载点对外提供服务,其中每个目录都叫一个分支
在Docker中,AUFS的每个目录都对应了镜像中的一层,联合挂载点对外提供一个统一的访问路径
2. AUFS中的容器读写
AUFS工作在文件层,也就是说即使文件只有一丢丢改动,AUFS的写时复制也会复制整个文件,当文件特别大的时候或目录层级很深时,对容器性能影响显著;在复制文件前,如果文件放在底层的镜像层中,那么搜索文件也会花费大量时间,文件的复制只会进行一次
3. AUFS中删除文件
镜像是只读的,无法删除其中文件,AUFS在删除一个文件时会在容器中生成一个空白文件覆盖镜像层中的对应文件,实现假删除
4. 配置AUFS
可以在安装了AUFS的Linux系统中使用AUFS存储驱动,而本机是CentOS,并不支持AUFS,所以第一步就挂掉!这里只是写出操作步骤
grep aufs /proc/filesystem 查看本机是否支持AUFS,如果不支持则什么也不出现
通过docker daemon –storage-driver=aufs & 命令行启动Docker Daemon或者在Docker Daemon配置文件中配置
最后使用docker info查看AUFS是否配置成功
5. 镜像存储方式
当使用AUFS作为存储驱动时,镜像层和容器层都保存在/var/lib/docker/aufs/diff/目录中;/var/lib/docker/aufs/layers/目录中则保存了镜像层的元数据,用来描述镜像层如何叠加在一起,在该目录中,每个镜像层和容器层都有一个描述文件,记录在该层下所有镜像层的名字
举个栗子,某个镜像有ABC三个镜像层,A在最上层,则/var/lib/docker/aufs/layers/A文件的内容为B C
;同理/var/lib/docker/aufs/layers/B文件内容为C
;而C即最下层镜像层的原数据文件为空
6. 容器存储方式
容器运行时,容器中的文件系统被挂载在/var/lib/docker/aufs/mnt/<容器ID>,也就是说不同的容器有不同的挂载点
所有容器(包括停止的容器)在/var/lib/docker/containers/中都有对应的子目录,容器的元数据和配置文件的保存位置在对应的子目录中,包括日志文件xxx-json.log
当容器停止时,容器目录仍然存在,当容器被删除时,容器目录才会被删除(/var/lib/docker/aufs/diff中的容器层也是如此,因为CentOS没法弄AUFS,所以只能演示/var/lib/docker/containers/目录)
7. AUFS性能
在PaaS应用场景下,AUFS是种很好的选择。AUFS能够在容期间共享镜像,加速容器启动,节约存储空间;但是大量写操作时性能较差(复制文件)
三、Devicemapper存储驱动
因为在RedHat中不能使用AUFS,而Docker流行速度又十分快速,所以Redhat和Docker Inc联合开发了适用于Redhat的存储驱动Devicemapper
Devicemapper是Linux 2.6内核提供的一套逻辑卷管理框架,Docker中的Devicemapper存储驱动就是基于该框架实现,使用了按需分配和快照功能管理镜像和容器
1. Devicemapper中的镜像
Devicemapper将容器和镜像存储在虚拟设备上,对块设备进行操作而不是整个文件
Devicemapper创建镜像的流程
创建一个thin pool(可以创建在块设备也可以是sparse)
创建一个基础设备(按需分配的设备),并在该设备上创建文件系统
创建镜像,会在基础设备上做快照(每创建一个镜像层就会做一次快照,使用写时复制技术,当内容变化时才会在thin pool中分配存储空间保存数据)
在Devicemapper中,每个镜像层都是在前一个镜像层上做了一个快照,第一个镜像层是在基础设备(Devicemapper设备,而不是Docker镜像层)上做了一个快照,容器则是镜像的一个快照
thin pool的块设备配置在Docker实战-Docker Daemon中有简单说明
2. Devicemapper中的读写操作
容器层只是镜像层的一个快照,没有保存数据,而是保存了数据块映射表,指向镜像层中真正的数据块。
当应用程序发起读请求后,Devicemapper通过数据块映射表找到数据在哪个镜像层的哪个数据块上后将该数据块的内容复制到容器的内存区中,然后再将需要的数据放回给应用程序
Devicemapper作为存储驱动时的写操作有两种情况,一种是写入新数据(通过按需分配技术实现);另一种是更新已有数据(写时复制技术实现)。Devicemapper是管理数据块的,也就是说这两种技术都是工作在数据块,更新数据时只复制变化的数据块而不是整个文件
Devicemapepr中,每个数据块的大小为64KB。当应用程序发起写新数据请求后,Devicemapper通过按需分配技术,在容器层分配一块或多个数据块给应用程序,应用程序把数据写入新分配的数据块
当应用程序发起更新数据请求后,Devicemapper定位到需要修改的数据块,在容器层中分配新的存储区,使用写时复制技术将修改的数据块复制到新分配的存储区供应用程序更新
Devicemapper使用的按需分配和写时复制技术对容器中应用程序而言是透明的,不需要了解Devicemapper是如何管理数据块
3. Devicemapper存储方式
Devicemapper工作在数据块层面,在宿主机上很难发现镜像层和容器层的区别
宿主机/var/lib/docker/devicemapper/mnt保存了镜像层和容器层的挂载点;/var/lib/docker/devicemapper/metadata保存了镜像层和容器层的配置信息
4. Devicemapper的性能
Devicemapper使用了按需分配技术,并且使用的数据块大小为64K,就算应用程序请求数据小于64K,也会分配一个数据块大小,当容器中发生了大量小数据写操作时,会影响容器性能
当需要在大文件中更新小数据时,Devicemapper比AUFS效果好,但是执行大量小数据写操作时,Devicemapper的性能就不如AUFS
四、Overlay存储驱动
OverlayFS是一种联合文件系统,类似AUFS,更有优势但目前还不太成熟
1. Overlay中的镜像
OverlayFS在Linux主机上用到两个目录,一个在下层保存镜像层(lowerdir),一个在上层保存容器层(upperdir),再使用联合挂载技术将这两个目录组合起来对外提供一个文件系统(merged),容器层中的文件会覆盖镜像层中的文件
运行容器时,存储驱动将所有镜像层对应目录组合起来再添加上容器层,其中镜像中最上面一层在lowerdir中,容器层在upperdir中
Overlay存储驱动只能使用两层,多层镜像不能再OverlayFS中映射为多个层,每个镜像层在/var/lib/docker/overlay都有对应的目录,使用硬链接与底层关联数据
启动容器可以看到容器层同样保存在/var/lib/docker/overlay/目录下,进入容器层的目录可以看到文件lower-id,merged,upper,work
其中lower-id中包含镜像顶层ID(顶层镜像保存在lowerdir中)
容器层保存在upper目录中,运行过程中修改的数据都保存在该目录中;merged目录是容器的挂载点,合并了lowerdir和upperdir;work目录是供OverlayFS使用
可以使用mount命令查看挂载关系
2. Overlay2中的镜像
在使用Overlay存储驱动时,只使用lowerdir目录保存镜像,有需要的时候则在该目录中使用硬链接保存多层镜像。而Overlay2原生支持多个lowerdir,最多可以到128层
Overlay2中,镜像和容器都存储在/var/lib/docker/overlay2目录下。最底层的镜像层包含了link文件和diff目录,其中link文件保存了短标识符名称(用于解决mount参数中长字符串超过页大小限制问题),diff目录包含了镜像层的内容;倒数第二个层包含了link,lower,diff,merged,work,容器层的组成结构也类似
其中lower文件保存镜像层的组成信息,文件中的镜像层使用:分隔,放在上面的镜像层排在前面,也就是说加入镜像有ABC三个镜像层,其中A为最顶层,则A文件的lower文件信息为B:C
3. Overlay中的读写操作
当应用程序读取文件时,若目标文件在容器层中,直接从容器层读取文件;如果目标文件不在容器层,就会从镜像层中读取文件
OverlayFS文件系统工作在文件层而不是数据块层。当容器中第一次修改文件时,Overlay/Overlay2存储驱动会从镜像层复制文件到容器层修改,OverlayFS只有两层,在很深的目录树中搜索文件时,Overlay比AUFS速度更快
当需要在容器中删除文件时,Overlay存储驱动会在容器层新建一个without文件/opaque目录用于隐藏镜像层中的目标文件/目录,而不会真正删除镜像层中的文件或目录
4. 配置Overlay/Overlay2
若要使用Overlay存储驱动需要宿主机Linux内核为3.18版本;而要使用Overlay2存储驱动则需要Linux内核为4.0或以上版本
过程描述:
service docker stop/systemctl stop docker.service 停止Docker Daemon
uname -r 检查Linux内核版本
lsmod | grep overlay 加载Overlay模块
docker daemon –storage-driver=overlay & 配置存储驱动或在Docker Daemon配置文件中配置
docker info 检查Overlay是否生效
使用Overlay/Overlay2存储驱动后,Docker会自动创建Overlay挂载点,并在其中创建lowerdir、upperdir、merged、workdir
5. Overlay的性能
总体来讲,Overlay/Overlay2存储驱动很快,支持共享页缓存;只是当文件特别大时会影响写操作性能,但OverlayFS的复制操作比AUFS快,因为AUFS有很多层,目录树很深时会产生很大开销;同样当需要大量读写时,最好将数据保存在数据卷中,所有读写操作都会绕过存储驱动,不会增加额外开销