使用缓存管理来优化构建
您可能会发现自己一遍又一遍地重建相同的 Docker 镜像。 无论是用于软件的下一个版本,还是在本地开发过程中。 由于构建镜像是一项常见任务,因此 Docker 提供了多种加快构建速度的工具。
提高构建速度的最重要功能是 Docker 的构建缓存。
构建缓存是如何工作的?
了解 Docker 的构建缓存有助于您编写更好的 Dockerfile,从而加快构建速度。
看一下下面的示例,它显示了一个用 C 编写的程序的简单 Dockerfile。
# syntax=docker/dockerfile:1 FROM ubuntu:latest RUN apt-get update && apt-get install -y build-essentials COPY main.c Makefile /src/ WORKDIR /src/ RUN make build
此 Dockerfile 中的每条指令都会(大致)转换为最终镜像中的一层。 您可以将镜像层视为一个堆栈,每个层在其之前的层之上添加更多内容:
每当层发生变化时,就需要重新构建该层。 例如,假设您在 main.c 文件中对程序进行了更改。
进行此更改后,必须再次运行COPY命令才能使这些更改出现在镜像中。
换句话说,Docker 将使该层的缓存失效。
如果某个图层发生更改,则该图层之后的所有其他图层也会受到影响。
当使用COPY命令的层失效时,后面的所有层也需要重新运行:
简而言之,这就是 Docker 构建缓存。 一旦层发生变化,所有下游层也需要重建。 即使他们不会以不同的方式构建任何东西,他们仍然需要重新运行。
假设您的 Dockerfile 中有RUN apt-get update && apt-get upload -y步骤,用于将基于 Debian 的镜像中的所有软件包升级到最新版本。
这并不意味着您构建的镜像始终是最新的。 一周后在同一主机上重建镜像仍将获得与以前相同的软件包。
强制重建的唯一方法是确保其之前的层已更改,或者使用 docker builder prune 清除构建缓存。
如何有效地使用缓存?
现在您已经了解了缓存的工作原理,您可以开始利用缓存来发挥自己的优势。 虽然缓存将自动在您运行的任何 docker 构建上工作,但您通常可以重构 Dockerfile 以获得更好的性能。 这些优化可以为您的构建节省宝贵的几秒钟(甚至几分钟)。
排列你的层
将 Dockerfile 中的命令按逻辑顺序排列是一个很好的起点。 由于更改会导致后续步骤的重建,因此请尝试使花费时间过长的步骤出现在 Dockerfile 的开头附近。 经常更改的步骤应出现在 Dockerfile 的末尾附近,以避免触发未更改的层的重建。
考虑以下示例。 从当前目录中的源文件运行 JavaScript 构建的 Dockerfile 片段:
# syntax=docker/dockerfile:1 FROM node WORKDIR /app COPY . . # Copy over all files in the current directory RUN npm install # Install dependencies RUN npm build # Run build
这个 Dockerfile 效率相当低。 每次构建 Docker 镜像时,更新任何文件都会导致重新安装所有依赖项,即使依赖项自上次以来没有更改!
相反,COPY命令可以分为两部分。
首先,复制包管理文件(在本例中为package.json和yarn.lock)。 然后,安装依赖项。
最后,复制项目源代码,该源代码会经常更改。
# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY package.json yarn.lock . # Copy package management files
RUN npm install # Install dependencies
COPY . . # Copy over project files
RUN npm build # Run build
通过在 Dockerfile 的早期层中安装依赖关系,当项目文件发生变化时,就没有必要重新构建这些层。
保持层小
加快镜像构建速度的最佳方法之一就是在构建中放入更少的内容。 零件越少,缓存就越小,但也意味着可能过时和需要重建的东西应该更少。
首先,这里有一些提示和技巧:
不要包含不必要的文件
仔细考虑您添加到镜像中的该是哪些文件。
运行COPY . /src等命令。 会将整个构建上下文COPY到镜像中。
如果您的当前目录中有日志、包管理器 artifacts,甚至以前的构建结果,这些也将被复制。
这可能会使您的镜像比需要的更大,特别是因为这些文件通常没有用处。
通过明确说明要复制的文件或目录,避免将不必要的文件添加到构建中。
例如,您可能只想将Makefile和src目录添加到镜像文件系统。
在这种情况下,请考虑将其添加到您的 Dockerfile 中:
COPY ./src ./Makefile /src
与此相反:
COPY . /src
您还可以创建 .dockerignore 文件,并使用它来指定要从构建上下文中排除的文件和目录。
聪明的使用您的包管理器
大多数 Docker 镜像构建都涉及使用包管理器来帮助将软件安装到镜像中。
Debian 有apt,Alpine 有apk,Python 有pip,NodeJS 有npm,等等。
安装软件包时,要考虑全面。 确保只安装您需要的软件包。 如果您不打算使用它们,请不要安装它们。 请记住,对于您的本地开发环境和生产环境来说,这可能是不同的列表。 您可以使用多阶段构建来有效地拆分它们。
使用 RUN 专用缓存
RUN命令支持专用缓存,当您在 runs 时需要更细粒度的缓存时,可以使用该缓存。
例如,在安装软件包时,您并不总是需要每次都从互联网上获取所有软件包。 您只需要已更改的内容。
要解决这个问题,可以使用RUN --mount type=cache。例如,对于基于 Debian 的镜像,您可以使用以下内容:
RUN \
--mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y git
使用带有--mount标志的显式缓存可以在构建之间保留target目录的内容。
当这一层需要重建时,它将使用/var/cache/apt中的apt缓存。
最小化层的数量
保持较小的层数是一个很好的开始,合乎逻辑的下一步是减少所拥有的层数。 更少的层意味着当 Dockerfile 中的某些内容发生更改时,您需要重建的内容更少,因此您的构建将更快完成。
以下部分概述了一些可用于将层数保持在最低限度的技巧。
使用适当的基础镜像
Docker 为几乎所有常见的开发场景提供了超过 170 个预构建的 官方镜像。 例如,如果您正在构建 Java Web 服务器,请使用专用镜像,例如 eclipse-temur。 即使没有您可能想要的官方镜像,Docker 也会提供来自 经过验证的发布商 和 开源合作伙伴 的镜像, 可以为您提供帮助。 Docker 社区也经常生成第三方镜像来使用。
使用官方镜像可以节省您的时间,并确保您在默认情况下保持最新状态和安全。
使用多阶段构建
多阶段构建可让您将 Dockerfile 分成多个不同的阶段。 每个阶段都会完成构建过程中的一个步骤,您可以桥接不同的阶段以在最后创建最终镜像。 Docker 构建器将计算出各个阶段之间的依赖关系,并使用最有效的策略来运行它们。这甚至允许您同时运行多个构建。
多阶段构建使用两个或多个FROM命令。以下示例说明了构建一个简单的 Web 服务器,该服务器从 Git 中的 docs 目录提供 HTML:
# syntax=docker/dockerfile:1
# stage 1
FROM alpine as git
RUN apk add git
# stage 2
FROM git as fetch
WORKDIR /repo
RUN git clone https://github.com/your/repository.git .
# stage 3
FROM nginx as site
COPY --from=fetch /repo/docs/ /usr/share/nginx/html
此构建有 3 个阶段:git、fetch和site。
在此示例中,git是fetch阶段的基础。 它使用COPY --from标志将数据从docs/目录复制到 Nginx 服务器目录中。
每个阶段只有几条指令,并且在可能的情况下,Docker 将并行运行这些阶段。
只有site阶段中的指令才会作为最终镜像中的图层。
整个git历史记录不会嵌入到最终结果中,这有助于保持镜像的小和安全。
尽可能将命令组合在一起
大多数 Dockerfile 命令,特别是RUN命令,通常可以组合在一起。 例如,不要像这样使用RUN:
RUN echo "the first command"
RUN echo "the second command"
可以在单个RUN中运行这两个命令,这意味着它们将共享相同的缓存!
这可以通过使用&& shell 运算符运行一个又一个命令来实现:
RUN echo "the first command" && echo "the second command"
# or to split to multiple lines
RUN echo "the first command" && \
echo "the second command"
另一个允许您以简洁的方式简化和连接命令的 shell 功能是 heredoc。
它使您能够创建具有良好可读性的多行脚本:
RUN <<EOF
set -e
echo "the first command"
echo "the second command"
EOF
set -e命令在任何命令失败后立即退出,而不是继续。