当我们pull
一个Docker Image
时,就会看到如下的输出,那么pull
的时候,控制台输出的这些信息是什么意思呢?想说明什么呢?
[vagrant@nexus3 ~]$ docker pull mongo
Using default tag: latest
latest: Pulling from library/mongo
23884877105a: Pull complete
bc38caa0f5b9: Pull complete
2910811b6c42: Already exists
36505266dcc6: Pull complete
a4d269900d94: Pull complete
5e2526abb80a: Pull complete
d3eece1f39ec: Pull complete
358ed78d3204: Pull complete
1a878b8604ae: Pull complete
978c572f0440: Pull complete
35a600ffcf6a: Downloading 11.89MB/129.1MB
fa9f812cdfe6: Waiting
7a8109e27110: Download complete
仔细观察pull
的过程会发现,在pull
开始的时候列出了很多个hash
值,这些值分别处于不同的状态,有些已经存在Already exists
,有些刚完成pull complete
,有些还在等待waiting
,而在所有的值都处于pull complet
的时候,image
就被成功pull
下来了,难道这些东西全部合在一起就是一个Image
?
镜像的组成
在官方文档上有这么一段话。
A Docker image is built up from a series of layers. Each layer represents an instruction in the image’s Dockerfile. Each layer except the very last one is read-only.
一个image是由一系列的 层(layer) 组成的,每一个 层(layer) 都对应 image 的 Dockerfile 中的一条指令,并且除了最后一个 层(layer) 外,其他每一个 层(layer) 都是只读的。
然后官方文档还提供了一个Dockerfile
的示例:
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
这个Dockerfile
包含了四个命令,每个命令创建一个 layer 。
FROM
语句首先根据ubuntu:18.04
的镜像创建 层(只读)COPY
命令从 Docker 客户端的执行目录复制文件,这条指令在步骤1的基础上添加了一个新的层 (只读)RUN
命令使用make
构建应用程序,同时而也在步骤2上又添加一个层(只读)- 最后一层,使用
CMD
指令,指定在容器中运行什么命令,同时在步骤3上添加又添加一个层
所有这些层,按照顺序堆叠在一起就构成了镜像。
当创建容器时,就是将镜像做为基础层,在基础层之上添加新的可写层。该层通常称为“容器层”。对运行中的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。下图显示了基于Ubuntu 18.04映像的容器。
通过上的分析,可以知道pull
的时候输出的那些类hash
值代表的就是一个个的 image layer
。
容器和层
通过上一节的讨论,可以知道,容器和镜像之间的主要区别在与容器有一个可读写的顶层。在容器中添加数据和修改数据的操作都在这个可读写的层。当删除容器或者容器崩溃时,可写层也会被删除,而容器的基础镜像是不会变化的。
因为每个容器都有其自己的可读写容器层,并且所有更改都存储在该容器层中,多个容器可以共享对同一个基础镜像的访问,只是具有自己的数据状态。下图显示了共享Ubuntu 18.04
镜像的容器。
镜像大小
有一个比较奇怪的现象,在hub.docker.com
中,mysql的镜像的大小标示的是150.33MB
可是在pull
到本地后,通过docker image ls
查看,会发现mysql
镜像的大小居然是是448MB
,这是为什么呢?
这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。
如果仔细观察pull
的过程的话,会发现Download complete
的状态后有一个Extracting
的过程,这个过程就是在从压缩中提取下载的镜像层。
类似mysql
这样的 docker 镜像说大不大,说小不小,但是一旦镜像的总数上来之后,岂不是对本地磁盘造成很大的存储压力?平均每个镜像四五百MB, 100 个镜像岂不是需要准备 四五十G
的存储空间?
事实上,Docker 在镜像复用方面设计得非常出色,大大节省镜像占用的磁盘空间。Docker 镜像的复用主要体现在多个不同的 Docker 镜像可以共享相同的镜像层。通过docker image ls
显示的镜像大小是各个镜像层加起来的大小,而在实际的物理存储上,相同的镜像层只会存储一份。
上面的截图中是本地的所有镜像列表,实际计算了下,大概是3.6GB
。下面通过docker system df
查看所有镜像的实际占用的存储空间。
docker system df
#TYPE TOTAL ACTIVE SIZE
#Images 10 0 3.007GB
而实际上,镜像复用带来的好处不只是存储空间方面的。在本文的开始,就提到pull
的时候会有一个Already exists
状态,出现这个状态的原因就是,将要pull
的这个镜像所依赖的层已经被其他的镜像下载的本地了,已经存在无需再从仓库下载了。
中间层镜像
默认的 docker image ls
列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a
参数。
$ docker image ls -a
这样会看到很多无标签的镜像,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
镜像的删除
该部分内容来自 https://yeasy.gitbook.io/docker_practice/image/rm
如果要删除本地的镜像,可以使用 docker image rm
命令。
$ docker image rm php
# Untagged: php:latest
# Untagged: php@sha256:7c9c0f661ffa956f181c7fded3f06fae21326f8a4cfcd99aa547d87d80e14f06
# Deleted: sha256:e97d32e924d01f000e5bb7371c2207e0e649005930c9c34922adb46bdda06d4b
# Deleted: sha256:e77fc89254ff05def4f34014c3e875db91659b0a5f02cc4b198da38c13f01e4a
# Deleted: sha256:e29014815b2355dc217820f131001f5a80e91068c16546fd02b980c77de213af
# Deleted: sha256:a875d1ab755d4a1fdcb5ae7addf8e7793156c960d4f6762a1dcab21d2d49c806
# Deleted: sha256:c1f69a2ed619964b1b5212c1ad001da55454cc6846eec0d7c931d7376bc438fd
# Deleted: sha256:c96ce9a3e1719c7ce625713cf173dac3f762b980f584fb54cfe40a5d71d8b2d2
# Deleted: sha256:e034a8beee6faaa0afb038a841ac0f0b230e2d9604ae6d5f2dfdefb956b7801a
# Deleted: sha256:891a2717569c40be7e08bbafe0ef074a0f6dbb90f45429cf016e17eb55523dcf
# Deleted: sha256:8072103e605f90bb9a6b6c5cbd47e569908516c37e434b9b4285185e9e907630
如果观察删除命令的输出信息,你会注意到删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。
当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。这里需要说明的是镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 Untagged
的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete
行为就不会发生。所以并非所有的 docker image rm
都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。
镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 docker pull
看到的层数不一样的原因。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
总结
- 容器运行在镜像定义的系统之上
- 镜像是由一个或者多个镜像层组成的
- Dockerfile 用来定义一个镜像,Dockerfile 中的每一个命令都添加了一个层
<完结>