使用缓存管理来优化构建


您可能会发现自己一遍又一遍地重建相同的 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 中的每条指令都会(大致)转换为最终镜像中的一层。 您可以将镜像层视为一个堆栈,每个层在其之前的层之上添加更多内容:

cache-stack

每当层发生变化时,就需要重新构建该层。 例如,假设您在 main.c 文件中对程序进行了更改。 进行此更改后,必须再次运行COPY命令才能使这些更改出现在镜像中。 换句话说,Docker 将使该层的缓存失效。

如果某个图层发生更改,则该图层之后的所有其他图层也会受到影响。 当使用COPY命令的层失效时,后面的所有层也需要重新运行:

cache-stack-invalidated

简而言之,这就是 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.jsonyarn.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,甚至以前的构建结果,这些也将被复制。 这可能会使您的镜像比需要的更大,特别是因为这些文件通常没有用处。

通过明确说明要复制的文件或目录,避免将不必要的文件添加到构建中。 例如,您可能只想将Makefilesrc目录添加到镜像文件系统。 在这种情况下,请考虑将其添加到您的 Dockerfile 中:

COPY ./src ./Makefile /src

与此相反:

COPY . /src

您还可以创建 .dockerignore 文件,并使用它来指定要从构建上下文中排除的文件和目录。

大多数 Docker 镜像构建都涉及使用包管理器来帮助将软件安装到镜像中。 Debian 有apt,Alpine 有apk,Python 有pip,NodeJS 有npm,等等。

安装软件包时,要考虑全面。 确保只安装您需要的软件包。 如果您不打算使用它们,请不要安装它们。 请记住,对于您的本地开发环境和生产环境来说,这可能是不同的列表。 您可以使用多阶段构建来有效地拆分它们。

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 个阶段:gitfetchsite。 在此示例中,gitfetch阶段的基础。 它使用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命令在任何命令失败后立即退出,而不是继续。