了解docker 构建和存储镜像与容器使用镜像方式,是高效使用存储驱动程序的前提,有助于选择应用程序最佳的存储方式,避免性能问题。存储驱动允许你在容器的可写层中创建数据,但容器删除后,数据不会持久化存储,同时容器内文件读写效率低于宿主设备的文件读写效率。
一.镜像和层
镜像是由一系列层,采用叠加方式构成的,上层包含了下层镜像连接信息,每一层代表镜像Dockerfile的一条指令(shell, bash 等,支持 && ),除最外层外,其他每层都是只读的。例如包含如下几条指令的dockerfile:
共四条指令,每一条创建一个镜像层,
FROM: 表示从 已存在的镜像 ubuntu:18.04 创建一个层。
COPY: 表示从当前目录拷贝文件到镜像内,与ADD指令功能相同
RUN: 运行指令,使用make 创建应用程序
CMD: 指定容器启动时,运行程序
每相邻两次之间只有一组指令运行结果差别,每层只保存于下层不同的数据,在创建一个容器时,将在顶层之上,加一个读写层,通常称之为容器层。所有针对容器的操作,例如创建,删除,修改文件等 都是在这个薄容器层上改变(这个薄的读写层的设计另外一个目的是实现镜像开箱即用的目的)。层之间的关系视图如下所示:
二.容器和层
容器和镜像的主要区别是在于顶层的可写层。对容器添加新数据或修改现有数据的所有操作都存储在这个可写层中。当容器删除,可写层也被删除,所有下层保持不变。
由于每个容器拥有各自的容器层,所有容器操作都保存在容器层,所以,多个容器可以共享相同底层映像,同时又拥有自己的数据状态。
注意:如果需要多个容器共同访问宿主的同一份已存在数据,请将宿主数据目录挂载到容器内部。
Docker使用存储驱动程序来管理图像层和可写容器层的内容。每个存储驱动程序处理实现的方式不同,但所有驱动程序都使用如下策略:
- 可堆叠的映像层
- 复制-写(CoW)策略
三.容器磁盘空间
获取运行容器近似磁盘空间,使用指令 docker ps -s
可发现 两列属性 size virtual size,
size: 容器的可写层在磁盘上的所有存储空间大小
virtual size: 容器调用的镜像只读层大小 + 可写层大小
由于,不同容器从相同的镜像启动,或者共享部分的只读层,因此,不能仅仅通过统计virtual size 估计容器使用的存储空间。如果两个容器100%共享一个镜像(即从同一镜像生成容器),统计过程中则需要减去一个镜像空间大小(virtual size - size).
此外,仅仅参考 vitrual size 和 size ,也无法准备统计容器使用空间大小,还需要考虑其他的一些空间开销:
- 如果启动了日志程序,容器运行过程中产生大量的日志,未即使清除
- 容器使用数据卷和绑定挂载。
- 磁盘存储容器的配置文件,这部分通常很少
- 内存交换空间,(如果启用 swapp)
- 容器维护程序存储空间,如果使用了 检查/恢复机制
四.复制-写(CoW)策略
Copy-on-write策略 是一种共享和复制文件,追求最大化效率的策略。如果一个镜像中的某个低层中文件或者目录,在一个高层(包含读写层)中被读取访问,通常直接使用。如果另外一个层需要修改该底层文件或目录,在第一次时,直接将对应的文件或目录拷贝到本层,然后修改,后续每次修改,直接在拷贝的目录上修改,这保证了每层文件最小化存在。Docker使用Copy-on-write策略保证镜像与容器的最小化空间开销的策略具体体校在如下:
4.1 镜像层间直接共享,确保镜像最小化
当使用 docker pull 从仓库中拉取镜像时,或者利用一个本地未存储的镜像创建容器时,每层数据将被单独地下载,存储在docker本地存储空间,
linux主机上一般在/var/lib/docker/下。很多情况下,/var/lib/docker所在的磁盘空间有限,可指定docker根目录。具体修改服务启动参数:/usr/lib/systemd/system/docker.service
在 ExecStart= 后加 --graph=想要设置的目录
然后,systemctl daemon-reload && systemctl restart docker
接着了解层之间的文件共享,每层内容,保存在docker存储区域内的各自目录下(也即docker根目录下的各自目录内)。一般在 /var/lib/docker/<storage-driver> 如果 存储驱动为 overlay2 则 ls /var/lib/docker/overlay2 即可查看
我本地的docker 主目录为 /home/hadoop/docker
这里的每个子目录存储了每层镜像数据。进入到内部:
注意:存储驱动程序不同,层存储的内容也不同,如果docker 存储驱动采用的 overlay2,则如上图格式. 目录结构描述如下:
子目录 | 存储内容 |
diff | 记录本次信息 |
link | 本层链接内容, 内容具体的链接 为 本层同一目录下 l 的下, 其实指向本层的diff目录 |
lower | 下层信息, 使用:符号来分割不同的底层,并且顺序是从高层到底层。 |
work | 当前工作目录 |
merged | 如果为容器层,则存在该目录。可写层目录信息 |
由上可见,镜像层直接共享,减少镜像空间,是采用链接方式,上层保存下层数据连接,叠加方式,每层仅保存于下层区别的数据,结构上减少每层空间开销,
4.2 容器高效操作:写时复制
每当我们启动一个容器,将在镜像的顶层之上,添加一个薄的容器层,所有对容器文件系统的操作将保存在该层,没有修改的文件,将不会被拷贝到该层,因此,容器层越小越好,有利于减少空间开销。
当一个容器内一个已存在的文件被修改时,根据存储驱动程序的不同,写时复制操作也不同,针对aufs, overlay,overlay2存储驱动,写时复制操作遵循如下步骤:
- 从最近的顶层开始,向下逐层寻找需要修改的文件,找到后,添加到缓冲,以便完成后续操作
- 在第一次操作该修改文件时,执行拷贝操作,将文件拷贝到可读写层。
- 后续针对该文件的一系列操作,在读写层上完成,下层内的该文件将不可见。
其他的存储驱动,类似Btrfs, ZFS 写时复制操作处理过程不同,这里不做过多介绍。
在容器内写大量数据,比不写数需要消耗更多的空间,因为,更多的写操作,要求更多可读写层空间。因此,针对write-heavy 应用程序,应当使用数据卷方式,挂载到容器,数据卷一方面独立于容器,实现高效的I/O,另一方面,可被多个容器共同挂载,不仅容器间可数据共享,而且不增加容器的可读写层空间。
写时复制,会导致明显的性能开销,尤其针对大文件,多层次的文件拷贝表现更突出,但好在所有文件只是拷贝一次,性能可控。
写时复制不仅减少了空间开销,同时,减少了容器启动时间,当创建容器时,系统只需要创建一个薄的可写容器层,这区别于虚拟机工作机制,无需生成底层映像堆栈的完整副本,使得每个容器对应一个虚拟的磁盘空间,极大减少时间,空间开销。