Jenkins构建缓存策略:加速CI/CD管道的有效方法

通过掌握构建缓存策略加速Jenkins CI/CD管道。本指南详细介绍了跨构建重用依赖项、编译器输出和Docker层的实用方法。学习如何利用工作空间保留、Docker构建选项和共享缓存技术来最小化冗余任务,显著加快集成和部署流程。

Jenkins构建缓存策略:加速CI/CD管道的有效方法

Jenkins构建缓存并非单一功能,而是一系列关于在构建之间安全重用内容的决策。良好的缓存能节省依赖下载、Docker层、编译工作和包管理器元数据。糟糕的缓存则会隐藏失败的构建、填满磁盘,或导致管道在一个代理上通过而在另一个上失败。

首先查看作业日志中最慢的重复步骤。如果每次构建都花两分钟下载Maven工件,就缓存Maven。如果Docker每次都重建相同的基础层,就修复Docker缓存。如果测试因每次运行都从头编译而缓慢,在发明Jenkins级归档前,先使用构建工具自身的缓存。

在有益时保留工作空间,在有害时清理

最简单的缓存是持久代理上现有的Jenkins工作空间。如果同一作业在同一节点上运行,可以重用上次构建留下的文件。

这对Maven、Gradle、npm、pnpm、Cargo和Go等工具有帮助。但当生成的文件、旧的测试报告或过时的构建输出留在工作空间中时,也可能导致奇怪的失败。

常见的折衷方案是只清理源代码树,并在其外部保留专用的缓存目录:

pipeline {
  agent { label 'linux-build' }
  environment {
    MAVEN_OPTS = '-Dmaven.repo.local=/var/cache/jenkins/maven'
    npm_config_cache = '/var/cache/jenkins/npm'
  }
  stages {
    stage('Checkout') {
      steps {
        deleteDir()
        checkout scm
      }
    }
    stage('Build') {
      steps {
        sh 'mvn -B test'
      }
    }
  }
}

这保持了工作空间的可重现性,同时仍能重用已下载的依赖项。确保缓存目录权限与运行代理的用户匹配。

按工具规则缓存依赖项

依赖缓存最好由包管理器控制。除非有充分理由,否则不要跨无关代理归档和恢复node_modules。通常更安全的是缓存包管理器的下载存储。

对于npm:

environment {
  npm_config_cache = "${WORKSPACE}/.npm-cache"
}
steps {
  sh 'npm ci'
}

对于pnpm:

environment {
  PNPM_STORE_PATH = "${WORKSPACE}/.pnpm-store"
}
steps {
  sh 'pnpm install --frozen-lockfile'
}

对于Maven,使用稳定的本地仓库路径:

sh 'mvn -B -Dmaven.repo.local=/var/cache/jenkins/m2 test'

对于Gradle,保持GRADLE_USER_HOME稳定,并在适当时在项目中启用Gradle构建缓存:

environment {
  GRADLE_USER_HOME = '/var/cache/jenkins/gradle'
}
steps {
  sh './gradlew test --build-cache'
}

锁文件是你的安全护栏。如果package-lock.jsonpnpm-lock.yamlpom.xmlbuild.gradle发生变化,工具应获取正确的新依赖项。如果缓存持续提供旧或损坏的包,删除它并让工具重建。

Docker层缓存

Docker缓存取决于Docker守护进程存储层的位置。在长期运行的VM代理上,正常的docker build可以自动重用层。在临时Kubernetes代理上,下一次构建通常落在没有层历史的新Pod上。

对于持久Docker代理,将变化缓慢的Dockerfile行放在前面:

FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test

如果在npm ci之前复制整个仓库,每次源代码更改都会使依赖层失效。

对于临时代理,使用BuildKit注册表缓存:

docker buildx build \
  --cache-from type=registry,ref=registry.example.com/my-app:buildcache \
  --cache-to type=registry,ref=registry.example.com/my-app:buildcache,mode=max \
  -t registry.example.com/my-app:${GIT_COMMIT} \
  --push .

该缓存通过注册表共享,而非本地守护进程。这通常比尝试将可写的Docker层目录挂载到多个Pod中更适合基于Kubernetes的Jenkins代理。

归档工件,而非缓存,除非你确实需要

archiveArtifacts适用于你想保留的构建输出:JAR、测试报告、覆盖率文件、生成的包。它不是一个好的通用缓存存储。大型依赖归档会拖慢控制器,增加存储压力,并使清理变得困难。

如果需要跨代理缓存,优先选择真正的外部缓存位置:工件仓库、对象存储桶、包代理或注册表缓存。对于Java构建,Nexus或Artifactory等仓库管理器通常比在Jenkins代理之间复制.m2效果更好。对于Docker,注册表支持的BuildKit缓存比打包层目录更可预测。

使缓存键可见

缓存应有有效的理由。好的缓存键包括操作系统、架构、主要语言版本、包管理器版本和锁文件哈希。

例如,Node缓存键可能基于:

linux-amd64-node22-npm10-sha256(package-lock.json)

你不需要花哨的缓存插件来应用这个想法。甚至目录名也可以携带键:

sh '''
LOCK_HASH=$(sha256sum package-lock.json | awk '{print $1}')
export npm_config_cache="/var/cache/jenkins/npm/node22-${LOCK_HASH}"
npm ci
'''

这避免了在不兼容的工具版本之间重用依赖缓存。同时使清理不再神秘,因为旧键易于识别。

注意失败模式

缓存有一些常见的失败模式:

  • 构建仅在一个代理上通过,因为该代理的工作空间中有隐藏文件。
  • 磁盘填满,因为旧缓存从未被清理。
  • Docker镜像从头重建,因为Dockerfile过早复制了易变文件。
  • 当多个构建同时写入时,共享缓存损坏。
  • 恢复大型缓存比从附近的包代理下载依赖项花费更长时间。

构建日志应显示缓存是否被使用。Maven会显示何时下载依赖项。Docker会显示CACHED或使用BuildKit输出时的缓存未命中。Gradle有构建扫描和控制台输出显示缓存行为。如果缓存不可见,在其路径、大小和键周围添加日志。

实用的推出计划

选择一个管道和一个瓶颈。仅对该步骤添加缓存。运行相同提交两次并比较日志。然后更改依赖锁文件后运行,确认缓存正确失效。最后添加清理。

对于持久代理,清理可以是cron作业或Jenkins维护作业:

find /var/cache/jenkins/npm -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
find /var/cache/jenkins/m2 -type f -name '*.lastUpdated' -delete
docker system prune -af --filter 'until=168h'

根据你的环境调整这些命令。不要在未检查同一Docker守护进程或文件系统还有谁使用的情况下,在共享主机上运行广泛的清理命令。

最好的Jenkins构建缓存策略是平淡无奇的:缓存昂贵的重复工作,让构建工具验证正确性,保持缓存路径明确,并在代理磁盘成为下一个瓶颈之前删除旧数据。

使缓存与代理模型匹配

持久VM代理和一次性云代理需要不同的缓存设计。在持久VM上,本地目录便宜且快速。/var/cache/jenkins/m2中的Maven缓存或守护进程上的Docker层缓存可以存活数周。风险是磁盘增长和状态过时。

在一次性代理上,本地缓存在每次构建后消失。你仍然可以缓存,但缓存必须存在于其他地方:对象存储、包代理、容器注册表或持久卷。Kubernetes中的持久卷可以工作,但共享可写缓存需要小心。两个构建同时写入同一缓存可能导致锁争用或根据工具不同而损坏部分下载。

对于许多团队,最好的第一笔投资不是Jenkins插件,而是附近的依赖代理:

  • 通过Nexus或Artifactory的Maven或Gradle。
  • 通过私有注册表或代理的npm。
  • 通过注册表镜像的Docker。
  • 通过包镜像或内部索引的Python。

这能改善每个代理,而无需在每个作业开始时恢复大型tarball。

知道何时不缓存

不要缓存密钥、生成的凭据、包含令牌的部署清单,或混合构建输出与环境特定配置的目录。不要缓存测试数据库,除非测试套件明确设计用于快照恢复。不要跨信任和不可信作业共享可变缓存。

当恢复步骤大于它所节省的工作时,也要对缓存持怀疑态度。如果包管理器可以从本地代理在45秒内干净安装,那么一个需要90秒下载和解压的2GB归档就没有帮助。

测量冷构建和热构建:

冷构建:无本地缓存
热构建:相同提交,相同缓存键
依赖变更构建:锁文件已更改
源代码变更构建:仅源代码更改

这四次运行告诉你缓存是否帮助了实际工作流,还是仅使一次人为重建看起来不错。

将缓存清理作为设计的一部分

每个缓存在上线前都需要一个过期策略。在静态代理上,在高峰构建时间之外安排清理。在共享注册表或对象存储上,使用生命周期策略。在Jenkins中,将缓存大小作为运营指标跟踪,而非事后考虑。

在事故期间删除缓存是正常的。好的管道在删除缓存后应变慢,而非损坏。如果删除缓存导致构建损坏,说明缓存隐藏了缺失的依赖声明或环境假设。