「Allen 谈 Docker 系列」docker build 的 cache 机制
「Allen 谈 Docker 系列」DaoCloud 正在启动 Docker 技术系列文章,每周都会为大家推送一期真材实料的精选 Docker 文章。主讲人为 DaoCloud 核心开发团队成员 Allen 孙宏亮,他是 InfoQ《Docker 源码分析》专栏作者,已出版《Docker 源码分析》一书。Allen 接触 Docker 近两年,爱钻研系统实现原理,及 Linux 操作系统。
Docker 的镜像技术一直是重中之重,《Allen 谈 Docker 系列》也已经有不少篇幅深入剖析 Docker 的镜像原理。从一开始,我们认识到 Docker 镜像的层级管理;而后我们开始窥探 Docker 镜像体积的含义;随后我们又深入解析了 Docker 镜像的包含的内容:镜像文件系统内容以及镜像 json 文件。随着循序渐进的深入分析,我们会发现对于 Docker 镜像的研究将无可避免地遇见一条 Docker 命令,那就是 “docker build”。
反复审视该命令,相信有两部分内容是 Docker 爱好者绝对不容错过的,那就是镜像 cache 机制和docker commit 原理,本文首先带大家了解并深入 Docker 镜像的 cache 机制。
docker build 简介
众所周知,一个 Dockerfile 唯一的定义了一个 Docker 镜像。如此依赖,Docker 必须提供一种方式,将 Dockerfile 转换为 Docker 镜像,采用的方式就是docker build 命令。以如下的 Dockerfile 为例:
FROM ubuntu:14.04RUN apt-get update
ADD run.sh /
VOLUME /data
CMD ["./run.sh"]
一般此 Dockerfile 的当前目录下,必须包含文件 run.sh。通过执行以下命令
docker build -t="my_new_image"即可将当前目录下的 Dockerfile 构建成一个名为my_new_image的镜像,镜像的默认 tag 为 latest。对于以上的docker build请求,Docker Daemon 新创建了 4 层镜像,除了 FROM 命令,其余的 RUN、ADD、VOLUME 以及 CMD 命令都会创建一层新的镜像。
镜像 cache 机制介绍
Dockerfile 可以通过docker build命令构建为一个新的镜像,Dockerfile 中每一条命令都会构建出一个新的镜像层。既然如此,构建成功后宿主机上的镜像层是否会不断增多,导致磁盘空间资源逐渐缩小?另外,一个 Dockerfile 如果构建多次,对于 Dockerfile 中的某一指定命令,是否会出现产生多个对应镜像层的情况呢?
镜像层的增多自然是毋庸置疑,然而并非每一次构建的每一条 Dockerfile 命令都会产生一个全新的镜像层。谈及原因,那我们必须谈谈 docker build 的 cache 机制。
docker build的 cache 机制 : Docker Daemnon 通过 Dockerfile 构建镜像时,当发现即将新构建出的镜像与已有的某镜像重复时,可以选择放弃构建新的镜像,而是选用已有的镜像作为构建结果,也就是采取本地已经 cache 的镜像作为结果。
反复阅读以上解释,细心的朋友肯定会有两点疑惑:
1. “即将构建出的镜像”属于仍未构建完成的镜像,通过何种方式来标识此镜像?
2. 涉及到镜像比较,重复时选择放弃构建,那镜像比较时重复的标准是什么?
cache 机制实现原理
Docker 镜像,由镜像层文件系统内容和镜像 json文件组成,而这两者都含有一个相同的镜像 ID 。还记得我们之前谈及的父镜像和子镜像的概念吗?此处也会大量运用镜像间的父子关系。
还是以上文中的 Dockerfile 为例,我们结合下图,着重分析命令FROM ubuntu:14.04和RUN apt-get update。
FROM ubuntu:14.04: FROM 命令是 Dockerfile 中唯一不可缺少的命令,它为最终构建出的镜像设定了一个基础镜像(base image)。docker build命令解析 Dockerfile 的 FROM 命令时,可以立即获悉在哪一个镜像基础上完成下一条命令RUN apt-get update的镜像构建。此时,Docker Daemon 获取 ubuntu:14.04 镜像的镜像 ID,并提取该镜像 json 文件中的内容,以备下一条命令构建时使用。
RUN apt-get update:RUN 命令是在上一层镜像(即 ubuntu:14.04 镜像)之上运行 apt-get update,所有对文件系统内容有更新的文件,都会保留于新构建的镜像层中,同时更新上一层镜像的 json 文件,更新镜像 json 文件的 Cmd 属性为"/bin/sh -c apt-get update"。注意:镜像 json 文件的 Cmd 属性与镜像 json 文件中 config 属性的 Cmd 属性,详见下图 RUN 命令所对应镜像(镜像 ID 为:0aaab7ef57ee)中两个 Cmd 的区别:
完成一条非 FROM 命令的构建,即产生一个新的镜像,新的镜像为其上一条命令产生镜像的子镜像。基于此以及以上的知识,我们可以提出这样的一个猜想:
“是否可以在构建 Dockerfile 某一命令前,就预知即将构建出新一层镜像的形态?”
围绕此问题,我们继续分析。未构建命令RUN apt-get update前,我们可以肯定的事实有以下几点:
- 镜像关系 :对于命令RUN apt-get update的构建,一定将会产生一个新镜像,新镜像的父镜像 ID 为 ubuntu:14.04 的镜像 ID,即 8251da35e7a7。
- 镜像 json 文件更新 :运行命令 apt-get update 后产生新镜像,新镜像 json 文件仅仅更新 ubuntu:14.04 镜像 json 文件的 Cmd 属性,其它如 config 属性均不会进行修改。
- 镜像层文件系统内容更新 :运行 apt-get update 后,对于容器可读写层的内容更新,全部将被打包进新镜像的镜像层文件系统内容。
基于这 3 个事实,我们再提出一个假设:如果在构建命令RUN apt-get update前,Docker Daemon 已经存在一个镜像满足以下两点:
- 此镜像的父镜像为 ubuntu:14.04
- 此镜像的 json 文件仅仅将 ubuntu:14.04 镜像 json 文件的 Cmd 属性更新为 apt-get update
那么是否可以认为:即将新构建的镜像与此镜像完全一致,不需要另行构建,只需复用此镜像即可?
如果你认可以上假设,那么 cache 机制的核心就接近浮出水面了: 遍历本地所有镜像,发现镜像与即将构建出的镜像一致时,将找到的镜像作为 cache 镜像,复用 cache 镜像作为构建结果。
cache 机制注意事项
可以说,cache 机制很大程度上做到了镜像的复用,降低存储空间的同时,还大大缩短了构建时间。然而,不得不说的是,想要用好 cache 机制,那就必须了解利用 cache 机制时的一些注意事项。
1. ADD 命令与 COPY 命令:Dockerfile 没有发生任何改变,但是命令ADD run.sh /中 Dockerfile 当前目录下的 run.sh 却发生了变化,从而将直接导致镜像层文件系统内容的更新,原则上不应该再使用 cache。那么,判断 ADD 命令或者 COPY 命令后紧接的文件是否发生变化,则成为是否延用 cache 的重要依据。Docker 采取的策略是:获取 Dockerfile 下内容(包括文件的部分 inode 信息),计算出一个唯一的 hash 值,若 hash 值未发生变化,则可以认为文件内容没有发生变化,可以使用 cache 机制;反之亦然。
2. RUN 命令存在外部依赖 :一旦 RUN 命令存在外部依赖,如RUN apt-get update,那么随着时间的推移,基于同一个基础镜像,一年的 apt-get update 和一年后的 apt-get update, 由于软件源软件的更新,从而导致产生的镜像理论上应该不同。如果继续使用 cache 机制,将存在不满足用户需求的情况。Docker 一开始的设计既考虑了外部依赖的问题,用户可以使用参数 --no-cache 确保获取最新的外部依赖,命令为docker build --no-cache -t="my_new_image" .3. 树状的镜像关系决定了,一次新镜像的成功构建将导致后续的 cache 机制全部失效:这一点很好理解,一旦产生一个新的镜像,同时意味着产生一个新的镜像 ID,而当前宿主机环境中肯定不会存在一个镜像,此镜像 ID 的父镜像 ID 是新产生镜像的ID。这也是为什么,书写 Dockerfile 时,应该将更多静态的安装、配置命令尽可能地放在 Dockerfile 的较前位置。
总结
docker build 的 cache 机制实现了镜像的复用,不仅节省了镜像的存储空间,也为镜像构建节省了大量的时间。 同时,如何命中 cache 镜像,也是衡量 Dockerfile 书写是否合理的重要标准之一。
欲知 Docker 镜像更精彩的内容,且听下回分解。下回内容预告:docker commit 的来龙去脉。