8.2.3 最佳实践
在结束本章之前,介绍一些最佳实践,当然本节无意罗列所有的最佳实践。
1.利用构建缓存
Docker的构建过程利用了缓存机制。观察缓存效果的一个方法,就是在一个干净的Docker主机上构建一个新的镜像,然后再重复同样的构建。第一次构建会拉取基础镜像,并构建镜像层,构建过程需要花费一定时间;第二次构建几乎能够立即完成。这就是因为第一次构建的内容(如镜像层)能够被缓存下来,并被后续的构建过程复用。
docker image build
命令会从顶层开始解析Dockerfile中的指令并逐行执行。而对每一条指令,Docker都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中(Cache Hit),并且会使用这个镜像层;如果没有,则是缓存未命中(Cache Miss),Docker会基于该指令构建新的镜像层。缓存命中能够显著加快构建过程。
下面通过实例演示其效果。
示例用的Dockerfile如下。
FROM alpine
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
第一条指令告诉Docker使用 alpine:latest
作为基础镜像。如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令;如果镜像不存在,则会从Docker Hub(docker.io)拉取。
下一条指令( RUN apk...
)对镜像执行一条命令。此时,Docker会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层。在此例中,Docker会检查缓存中是否存在一个基于 alpine:latest
镜像且执行了 RUN apk add --update nodejs nodejs-npm
指令构建得到的镜像层。
如果找到该镜像层,Docker会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。此处“设置缓存无效”作用于本次构建的后续部分。也就是说Dockerfile中接下来的指令将全部执行而不会再尝试查找构建缓存。
假设Docker已经在缓存中找到了该指令对应的镜像层(缓存命中),并且假设这个镜像层的ID是 AAA
。
下一条指令会复制一些代码到镜像中( COPY . /src
)。因为上一条指令命中了缓存,Docker会继续查找是否有一个缓存的镜像层也是基于 AAA
层并执行了 COPY . /src
命令。如果有,Docker会链接到这个缓存的镜像层并继续执行后续指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效。
假设Docker已经有一个对应该指令的缓存镜像层(缓存命中),并且假设这个镜像层的ID是 BBB
。
那么Docker将继续执行Dockerfile中剩余的指令。
理解以下几点很重要。
首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时须特别注意这一点,尽量将易于发生变化的指令置于Dockerfile文件的后方执行。这意味着缓存未命中的情况将直到构建的后期才会出现——从而构建过程能够尽量从缓存中获益。
通过对 docker image build
命令加入 --nocache=true
参数可以强制忽略对缓存的使用。
还有一点也很重要,那就是 COPY
和 ADD
指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。例如,有可能Dockerfile中的 COPY . /src
指令没有发生变化,但是被复制的目录中的内容已经发生变化了。
为了应对这一问题,Docker会计算每一个被复制文件的Checksum值,并与缓存镜像层中同一文件的checksum进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。
2.合并镜像
合并镜像并非一个最佳实践,因为这种方式利弊参半。
总体来说,Docker会遵循正常的方式构建镜像,但之后会增加一个额外的步骤,将所有的内容合并到一个镜像层中。
当镜像中层数太多时,合并是一个不错的优化方式。例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。
缺点是,合并的镜像将无法共享镜像层。这会导致存储空间的低效利用,而且push和pull操作的镜像体积更大。
执行 docker image build
命令时,可以通过增加 --squash
参数来创建一个合并的镜像。
图8.8阐释了合并镜像层带来的存储空间低效利用的问题。两个镜像的内容是完全一样的,区别在于是否进行了合并。在使用 docker image push
命令发送镜像到Docker Hub时,合并的镜像需要发送全部字节,而不合并的镜像只需要发送不同的镜像层即可。
3.使用no-install-recommends
在构建Linux镜像时,若使用的是APT包管理器,则应该在执行 apt-get install
命令时增加 no-install-recommends
参数。这能够确保APT仅安装核心依赖( Depends
中定义)包,而不是推荐和建议的包。这样能够显著减少不必要包的下载数量。
4.不要安装MSI包(Windows)
在构建Windows镜像时,尽量避免使用MSI包管理器。因其对空间的利用率不高,会大幅增加镜像的体积。