使用 Docker 层缓存快速构建
我发现 Docker 是我工作中最令人生畏和困惑的事情之一。希望我能以一种易于理解且不那么令人生畏的方式分享我学到的一些东西。_
什么是 Docker?
Docker 是一个用于在[(几乎)虚拟机](https://en.wikipedia.org/wiki/Docker_(software)中“容器化”软件的应用程序。 Docker 容器可用于:
-
将您的应用程序代码与计算机的其余部分隔离开来。
-
允许您控制应用程序可以访问哪些资源(例如网络)。
-
在虚拟操作系统上运行您的代码,让您可以在不同的计算机上运行它,并且更有信心它会以相同的方式运行(例如在开发和生产期间)。
工作原理
以下是 Docker 容器的制作方法:
-
您编写一个 Dockerfile 并将其添加到您的源代码旁边。
-
将 Dockerfile 提供给 Docker build 命令。
-
Docker 通过运行 Dockerfile 中的指令来构建容器的 image。
-
将该图像提供给 Docker create 命令。
-
Docker 从镜像创建一个容器。
所以首先,Docker 会根据你的 Dockerfile 中的说明构建一个镜像。
这可能是这样的:
FROM node:12.14.1-slim
USER node
COPY package.json /home/node/
WORKDIR /home/node
RUN npm install
RUN npm run build
进入全屏模式 退出全屏模式
Dockerfile 中的每一行都是一个指令用于 Docker 应该如何构建镜像。
例如,copy 指令将文件从 Docker 上下文(例如您的源代码)复制到容器中。 (容器有自己的文件系统!)
Docker 一个接一个地执行您的指令,然后创建结果的快照并将其存储为_image_。
然后,您可以从映像_create_ 一个容器,start 容器,并在其中执行_execute_ 命令。
下面是我如何使用上面的 Dockerfile 构建和运行我的 Node.js 应用程序:
docker build --tag my_app .
docker run my_app npm start
进入全屏模式 退出全屏模式
(Docker run 就像是 create、start 和 execute 在一个命令中的组合)。
但是等等,这并不是正在发生的事情的全貌。让我们谈谈层。
图层如何工作
Docker 不只是在构建映像结束时创建快照。事实上,Docker 在构建过程中根据每条指令的结果创建一个快照。
这些快照称为_layers_,每一层都记录了由于执行指令而发生的变化。
因为一个层只存储自运行上一条指令以来发生的变化,所以每一层都依赖于前一层(也就是它的_parent_层)。没有父层的层是无效的,因为它仅在其父层的上下文中才有意义。
因此,一个层包括:
-
一条指令
-
教学期间更改内容的记录
-
唯一标识
-
其父ID
(旁注:Docker 层看起来很像 Git 提交!)
Docker 将构建期间的所有层存储在输出的映像中。
当您要求 Docker 从映像创建容器时,Docker 会重播每一层的更改,一个在另一个之上(这就是它们被称为层的原因)。
层缓存的工作原理
当您运行 docker build 时,您可以通过添加--cache-from命令行参数为 Docker 提供一个图像以用作其层缓存。
在运行每条指令之前,Docker 会检查它的缓存中是否有一个层与它要运行的指令匹配,如果找到一个,它将使用该层而不是构建一个新层。不错,Docker!
重要的是,Docker 还会检查前一层的 ID 是否与缓存层的父 ID 匹配。如果上一层不匹配,则缓存层不能使用。
这样做的效果是,一旦 Docker 无法在其层缓存中找到一条指令的匹配项,则必须重新构建该指令的层和所有子层——使一个缓存层失效实际上会使所有的缓存层失效。它的孩子也是如此。
为了演示这在实践中是如何工作的,让我们回到我们的示例 Dockerfile,它看起来像这样:
FROM node:12.14.1-slim
USER node
COPY package.json /home/node/
WORKDIR /home/node
RUN npm install
RUN npm run build
进入全屏模式 退出全屏模式
让我们运行 docker build,并像这样添加--cache-from参数:
docker build --tag “my_app” --cache-from=”my_app” .
进入全屏模式 退出全屏模式
这是我们的日志输出:
Step 1/6 : FROM node:12.14.1-slim
---> 918c7b4d1cc5
Step 2/6 : USER node
---> Using cache
---> d897eea3d14a
Step 3/6 : COPY package.json /home/node/
---> Using cache
---> 6211fc2535b1
Step 4/6 : WORKDIR /home/node
---> Using cache
---> 8b7fbfbc367f
Step 5/6 : RUN npm install
---> Using cache
---> 530d5f1f8e6d
Step 6/6 : RUN npm run build
---> Using cache
---> ae5267476d3d
Successfully built ae5267476d3d
Successfully tagged my_app
进入全屏模式 退出全屏模式
在每条指令之后,Docker 都会记录 Using cache,然后是它正在缓存的层的 ID。
现在,如果我将我的 Dockerfile 更改为RUN npm install --only=prod而不是RUN npm install并重新运行构建,这就是我们得到的:
Step 1/6 : FROM node:12.14.1-slim
---> 918c7b4d1cc5
Step 2/6 : USER node
---> Using cache
---> d897eea3d14a
Step 3/6 : COPY package.json /home/node/
---> Using cache
---> 6211fc2535b1
Step 4/6 : WORKDIR /home/node
---> Using cache
---> 8b7fbfbc367f
Step 5/6 : RUN npm install --only=prod
---> Running in a168555b7db9
Removing intermediate container a168555b7db9
---> 44ab41899c52
Step 6/6 : RUN npm run build
---> Running in 1230a3e778aa
Removing intermediate container 1230a3e778aa
---> 8d3dca829a17
Successfully built 8d3dca829a17
Successfully tagged my_app
进入全屏模式 退出全屏模式
您无法从日志输出中看出,但此构建运行速度比上一个慢很多!让我们找出原因。
这里的前几行和以前一样,但是现在当我们看到我们改变的指令时,而不是:
Step 5/6 : RUN npm install
---> Using cache
---> 530d5f1f8e6d
进入全屏模式 退出全屏模式
现在我们得到:
Step 5/6 : RUN npm install --only=prod
---> Running in a168555b7db9
Removing intermediate container a168555b7db9
---> 44ab41899c52
进入全屏模式 退出全屏模式
啊哈,所以这一步没有使用缓存,而是执行了指令,因为指令已更改!这当然是一件好事,因为我们改变了指令,所以我们希望 Docker 做一些不同的事情。如果 Docker 使用缓存层,就会得到错误的结果。
但这里的关键是图层的 ID 也发生了变化(从530d5f1f8e6d变为44ab41899c52)。这意味着从现在开始,当 Docker 在其层缓存中查找时,它不会找到匹配的层,因为层的 ID 都与以前不同 - 请记住,它根据父层的 ID 缓存层层以及指令。
必须按照我们更改的说明重新构建所有层。您可以在日志输出中看到这正是发生的事情。一旦缓存失效,就不再使用缓存行。
所以我们的构建需要更长的时间是因为我们必须重新运行npm install和npm run build步骤。
这里值得注意的是,这不是 Docker 的问题,而是一个重要的特性!npm run build的结果可能会根据我们事先运行npm install还是npm install --only=prod而改变。更改 Dockerfile 中的npm install行意味着我们几乎肯定希望 Docker 也重新运行我们的npm run build。这是_缓存失效_按预期工作。
当然,如果我们再次重新运行此命令而不进行任何更改,我们现在将获得一个完全缓存的构建,因为我们每次都覆盖相同的图像标签。
为什么这很重要
在编写 Dockerfile 时,了解 Docker 层缓存的工作原理非常重要。
让我们以这个 Dockerfile 片段为例:
COPY src /home/node/src
COPY package.json /home/node/
WORKDIR /home/node
RUN npm install
RUN npm run build
进入全屏模式 退出全屏模式
在这里,我们将COPY src指令放置在npm install指令之前。这意味着每次我们对src目录中的文件进行更改时,都必须再次执行npm install,因为对 src 文件的更改将使COPY src行期间的层缓存无效。 (注意:在COPY期间,Docker 会将文件更改视为指令已更改,这将使缓存无效)。
对于我的 Node.js 应用程序,我相信npm install的结果不会受到我的src文件夹中的内容的影响,所以我希望能够对src进行更改而不必每次等待npm install时间。
所以让我们改变 Dockerfile 的顺序,将COPY src移到npm install之后:
COPY package.json /home/node/
WORKDIR /home/node
RUN npm install
COPY src /home/node/src
RUN npm run build
进入全屏模式 退出全屏模式
现在,如果我更改src的内容但我没有触及package.json,Docker 可以直接从缓存中获取前几层,它只需要重新运行npm run build。这正是我们想要的,因为npm install的输出不会根据src的内容而改变,而是npm run build的输出_does_。
当我不更改package.json时,这将加快我的构建速度,因为这意味着 Docker 不会重新运行npm install。🎉
总之......
重要的是要了解 Dockerfile 中指令之间的依赖关系,以最大限度地利用 Docker 层缓存的好处。尝试将相关指令组合在一起。
您还可以考虑哪些内容您经常更改,哪些内容您不更改,并尝试将您认为最常更改的内容放在 Dockerfile 的底部。
❤
更多推荐

所有评论(0)