为了有效地利用存储驱动程序,了解Docker如何构建和存储镜像以及如何使用这些镜像非常重要。
镜像分层
Docker镜像是由一系列层来构成的,每层代表Dockerfile中的一条指令,依下面Dockerfile为例:
| 1 | FROM ubuntu:18.04 | 
该Dockerfile包含四个命令,每个命令都会新创建一个层。FROM语句会从ubuntu:18.04镜像创建一个层。COPY指令会从Docker客户端的当前目录下添加一些文件。RUN指令使用了make指令来构建。最后CMD是☞在容器中运行什么命令。
而对于Docker来说,创建新容器时,每一层都会彼此堆叠,可以在基础层的基础上添加新的可写容器层。对容器的所做的所有更改都将写入到该可写容器层中。下图显示了基于Ubuntu 15.04 的容器。对于Image layers都是Read Only的。

容器和层
对于容器和镜像(container和image)的主要区别就是顶部的可写层(the top writable layer),在容器中添加数据或者修改现有数据的所有读写操作都会存储在此可写层中。删除容器后,可写层也会被删除,而基础镜像则保持不变。
每个容器都会有自己的可写层,所有的改变都存储在该容器层中。多个容器可以共享对同一基础镜像的访问,但可以拥有自己的数据状态。

如果需要对完全相同的数据的访问权限,需要将该数据存储在
Docker volume中并且装入到容器中。
Copy-on-write策略
写时复制是一种共享和复制文件的策略,可以最大程度地提高效率。
当我们使用dockerpull`指令时会从仓库拉下一个镜像例如下面的例子
| 1 | $ docker pull ubuntu:18.04 | 
所有的这些层都会在Docker主机本地存储区域内存储,可以通过以下指令来列出
| 1 | $ ls /var/lib/docker/overlay2 | 
依下面的Dockerfile为例,创建一个名为acme/my-base-image:1.0的镜像。
| 1 | FROM ubuntu:18.04 | 
第二个Dockerfile依第一个Dockerfile为例,但是有用一个增加的层。
| 1 | FROM acme/my-base-image:1.0 | 
第二个镜像包含第一个镜像的所有层,再加上带有CMD指令的新层,以及一个可读写的容器层。因为Docker已经拥有第一个镜像的所有层,所以不需要再次将其pull下来。
构建这两个Dockerfile镜像,可以通过docker history和docker image ls指令来验证共享层的加密ID是否相同。
- 创建一个新文件夹cow-test/,并切换到该目录下。
- 在cow-test/目录下,创建一个名为hello.sh的文件内容如下:
| 1 | #!/bin/sh | 
然后设置可执行
| 1 | chmod +x hello.sh | 
- 将上面第一个镜像复制为Dockerfile.base。
- 将上面第二个镜像复制为Dockerfile
- 在cow-test/目录下,构建第一个镜像
| 1 | $ docker build -t acme/my-base-image:1.0 -f Dockerfile.base . | 
- 构建第二个镜像
| 1 | $ docker build -t acme/my-final-image:1.0 -f Dockerfile . | 
- 查看镜像的大小。
| 1 | $ docker image ls | 
- 查看每个镜像中层的大小
| 1 | $ docker history bd09118bcef6 | 
可以看到除了第二个镜像的顶层以外,所有层都是相同的,其他层在这两个镜像中共享,并且只会在/var/lib/docker目录中存储一次,实际上新层并不会占用任何空间,因为它只是运行命令。
磁盘上容器的大小
可以使用docker ps -s指令来查看容器的大小。
- size表示每个容器的可写层使用的大小。
- virtual size表示容器使用的用于只读镜像的数据的数据量加上容器可写层的大小。多个容器可以共享一些或者所有的只读镜像数据。
因此每个正在运行的容器所使用磁盘总空间大小是virtual size和size的某种组合。如果多个容器从相同的基础镜像开始,所以这些容器在磁盘上的总大小为所有容器的大小size(SUM of all container size)加上一个镜像的大小(virtual size-size)
本文简单介绍了docker镜像分层。
- 镜像如果共享了相同的层只会在/var/lib/docker下存储一个,通过这个可以大大减少容器编译、推送的时长。