Docker BuildKit 实战:使用缓存优化依赖管理加速构建

Docker BuildKit 实战:使用缓存优化依赖管理加速构建

什么是 Docker BuildKit

Docker BuildKit 是 Docker 的下一代构建引擎,它提供了更高效、更灵活的容器镜像构建能力。BuildKit 于 2018 年引入,从 Docker 18.09 版本开始集成到 Docker 引擎中,并在 Docker 23.0 版本后成为默认的构建系统。

BuildKit 的主要特点

  1. 并行构建:能够并行执行独立的构建步骤,大幅提高构建效率
  2. 高级缓存机制:更智能的缓存系统,支持内容寻址存储
  3. 挂载功能:支持在构建过程中挂载文件系统,如缓存、密钥等
  4. 跨平台构建:可以在一个平台上构建用于其他平台的镜像
  5. 更安全的特权降级:更好的安全隔离

如何启用 BuildKit

从 Docker Engine v23.0 开始,BuildKit 已经开始作为 Docker 的默认构建引擎来使用。

可以通过设置环境变量 DOCKER_BUILDKIT= 0 来禁用。

如果你使用的 Docker 是 v23.0 之前的版本,可以通过环境变量启用:

# 临时启用
export DOCKER_BUILDKIT=1

# 在 .zshrc 中永久启用
echo 'export DOCKER_BUILDKIT=1' >> ~/.zshrc

或在 Docker 配置文件中启用(/etc/docker/daemon.json 或 macOS 上的 Docker Desktop 设置):

{
  "features": {
    "buildkit": true
  }
}

修改后记得重启 Docker Engine。

示例

以一个 Java 项目为例,我们将使用 Docker BuildKit 的缓存功能来优化 Maven 依赖的下载和构建过程。让我们看下这个 Dockerfile

# Stage 1: Build the application
FROM maven:3.9-eclipse-temurin-17 AS build

# Set the working directory
WORKDIR /app

# Copy pom.xml 
COPY pom.xml .

# Copy source code
COPY src/ /app/src/

# Use dependency hash value as cache ID identifier for better cache management
# A new cache is automatically created when pom.xml changes
COPY pom.xml pom.xml.checksum
RUN md5sum pom.xml > pom.xml.checksum

# Build the application (with cache mount support)
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests

# Stage 2: Create the runtime image
FROM eclipse-temurin:17-jre-jammy

WORKDIR /app

# Copy the JAR file from the build stage
COPY --from=build /app/target/spring-boot-3-rest-api-sample-1.0-SNAPSHOT.jar app.jar

# Expose the port the app runs on
EXPOSE 8080

# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

在这个示例中,使用 RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests 替换了原来的 RUN mvn clean package -DskipTests。使用原来的命令,每次构建都会重新下载 Maven 依赖,导致构建速度慢且浪费网络流量。

修改之后,第一次构建同样会因为下载所有 Maven 依赖耗时较久,但后续的构建将会显著加快。有兴趣的可以使用 这个仓库 的代码进行测试。

BuildKit 缓存命令解析

在 Dockerfile 中使用 BuildKit 缓存的典型命令如下:

RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests

命令组成部分

  1. RUN - Dockerfile 的标准指令,用于执行命令
  2. --mount=type=cache,target=/root/.m2 - BuildKit 的缓存挂载参数
    • type=cache - 指定挂载类型为缓存
    • target=/root/.m2 - 指定挂载点为容器内的 Maven 仓库目录
  3. mvn clean package -DskipTests - 实际执行的 Maven 构建命令

除了这里使用的 cache 类型,BuildKit 还支持其他类型的挂载:

  • bind - 挂载宿主机目录
  • secret - 挂载机密文件
  • ssh - 挂载 SSH 密钥
  • tmpfs - 挂载临时文件系统

这里不展开介绍,更多信息可以参考 Dockerfile 文档

工作原理

BuildKit 缓存机制通过以下方式优化构建流程:

  1. 首次构建

    • Maven 下载所有依赖到 /root/.m2 目录
    • BuildKit 将这个目录作为缓存保存
  2. 后续构建

    • BuildKit 挂载先前缓存的目录
    • Maven 检测到依赖已存在,直接使用缓存版本
    • 显著减少构建时间和网络流量

除了支持 Java 项目,BuildKit 缓存也适用于其他语言和工具,如 Node.js、Golang、Python 等。

缓存生命周期

BuildKit 缓存默认没有固定的过期时间,会持续存在直到:

  • 手动执行 docker builder prune 命令清理
  • 系统磁盘空间不足时自动清理
  • Docker 守护进程重启(某些配置下)

高级用法

添加缓存 ID 方便管理

在使用 BuildKit 缓存时,可以为缓存指定 ID,以便于管理和清理:

RUN --mount=type=cache,target=/root/.m2,id=maven-deps-myproject mvn clean package

添加缓存统计信息

在构建过程中输出缓存统计信息,帮助了解缓存的使用情况:

RUN --mount=type=cache,target=/root/.m2 \
    echo "==== Maven Repository Cache Information ====" && \
    find /root/.m2 -name "*.jar" | wc -l | xargs echo "Number of cached JAR files:" && \
    du -sh /root/.m2 | xargs echo "Maven cache size:" && \
    echo "=======================================" && \
    mvn clean package -DskipTests

基于依赖变化的缓存管理

在某些情况下,可以基于依赖文件的哈希值来管理缓存。正如上面的示例中使用的,当 pom.xml 文件发生变化时,BuildKit 会自动创建新的缓存:

COPY pom.xml pom.xml.checksum
RUN md5sum pom.xml > pom.xml.checksum

RUN --mount=type=cache,target=/root/.m2 \
    mvn clean package -DskipTests

缓存管理命令

清理缓存:

# 清理所有未使用的构建缓存
docker builder prune

# 仅清理缓存挂载
docker builder prune --filter type=exec.cachemount

# 清理超过7天未使用的缓存
docker builder prune --filter "until=168h"

与传统方法对比

传统方法使用 Docker 层缓存和卷挂载来管理依赖,但存在一些局限性,如下表所示:

特性 BuildKit 缓存 传统 Docker 层缓存 卷挂载
配置复杂度
持久性
依赖变化时有效性
CI/CD 适用性
清理管理 简单 简单 复杂

验证缓存是否有效

在使用了缓存后,如何验证它是否正常工作呢?可以通过以下几种方式来检查:

  1. 比较构建时间

    time docker build -t myapp .  # 第一次构建
    time docker build -t myapp .  # 第二次构建应明显更快
    
  2. 观察 Maven 日志

    docker build --progress=plain -t myapp . | grep "Downloaded from"
    
    • 第一次构建会有多个 “Downloaded from” 日志
    • 后续构建应该很少或没有这种日志
  3. 查看缓存统计信息:在构建时添加缓存信息输出命令

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus