
重复构建反复拉依赖、编译,拖慢研发效率。本文介绍腾讯云 CNB 构建缓存:通过 volumes 声明缓存目录,copy-on-write 避免并发冲突,相同依赖后续构建可大幅跳过重复下载,附配置示例。
在持续集成和日常开发中,团队常常会遇到这样的现象:代码改动并不多,但每次触发构建,流水线都要重新拉取依赖、重新编译,耗时和上一次几乎没有差别。
问题出在"重复劳动"上。以常见的前端项目为例,一次构建往往包含 npm install 安装依赖、编译打包、运行测试等步骤。其中安装依赖和编译产物(如 node_modules、编译缓存)本质上是可复用的——只要依赖版本没变,结果就是一样的。可如果每次构建都在全新的干净环境里从头跑一遍,这些本可省略的步骤就会一次次消耗构建时长和计算资源。
对于大型项目,这种浪费尤为明显。依赖体积动辄数百 MB 甚至上 GB,全量编译可能需要几十分钟。当一天要触发几十次构建时,累计浪费的时间相当可观。因此,能否把上一次构建的中间产物留存下来、供下一次构建直接复用,成为加速重复构建的关键。这正是构建缓存要解决的问题。
腾讯云 CNB 基于 Docker 生态构建,对环境、缓存和插件做了抽象,通过声明式语法让开发者高效构建软件。缓存的声明方式非常直观:在 .cnb.yml 配置文件中,通过 volumes 字段声明需要缓存的目录。
volumes 中每一项指定一个容器内的目录路径,构建结束后该目录的内容会被保留下来,作为下一次构建的缓存。常见的缓存目录包括:
/root/.npmnode_modules/root/.gradle、Maven 的 ~/.m2)一个最基础的声明示例如下:
main:
push:
- docker:
image: node:20
volumes:
- /root/.npm:copy-on-write
stages:
- node -v
- npm install
- npm test这段配置的含义是:在 node:20 镜像中运行构建,把容器内的 /root/.npm 目录声明为缓存。第一次构建时,npm install 会正常下载依赖,构建结束后 npm 缓存被保留;当同一分支再次触发构建,CNB 会把上次的缓存挂载回来,npm install 命中的部分无需重新下载,从而跳过重复的网络拉取。
声明缓存只需要在配置里加几行 volumes,不需要额外搭建缓存服务器,也不需要维护缓存的清理逻辑,这是 CNB 缓存机制在易用性上的一个特点。
缓存带来了一个新问题:如果多个构建同时运行,它们都去读写同一份缓存目录,会不会互相覆盖、导致缓存损坏?
CNB 的解决思路是 copy-on-write(写时复制,简称 CoW)。当缓存被声明为 copy-on-write 模式时,多个构建可以共享同一份只读的缓存基线;只有当某个构建需要修改缓存目录中的数据时,才会把要修改的那部分数据复制成私有副本,在自己的副本上写入,不会影响其他构建正在使用的基线。构建成功结束后,变更再合并回主缓存。
这种机制让并发构建无需担心缓存读写冲突。CNB 的 volumes 支持多种缓存类型,可按场景选择:
类型 | 简写 | 适用场景 |
|---|---|---|
copy-on-write | cow | 默认类型,并发构建场景,变更在流水线成功后合并 |
copy-on-write-read-only | - | PR 场景,只读主分支缓存 |
read-write | w | 串行构建场景,读写但需自行处理并发冲突 |
read-only | ro | 只读访问,写操作抛出异常 |
data | - | 临时数据卷,容器间共享数据,流水线结束自动清理 |
可以看到,copy-on-write 是默认且推荐的类型,尤其适合分支并行开发、多个 PR 同时触发构建的场景。需要说明的是,这种基于母机存放缓存、通过 volume 挂载进容器的实现,缓存作用范围在同一构建节点内有效。当需要跨节点共享缓存时,CNB 还提供了 docker:cache 内置任务,可把缓存构建为 Docker 镜像,供不同节点拉取复用。
缓存机制带来的提速效果,可以从两个层面看。
首先是依赖拉取的复用。相同依赖的后续构建,绝大部分依赖下载步骤被直接跳过。对于依赖体积大的项目,这意味着原本几分钟的依赖安装,在命中缓存后可以缩短到十几秒。
其次是代码准备的提速。CNB 基于 OverlayFS 实现了 git-clone-yyds 秒级克隆能力,可在数秒内完成代码准备,轻松支持 100GB 以上的超大仓库。以 AOSP(安卓开源项目)为例,一个约 125GB 的代码库,克隆时间从约 20 分钟降至 10 秒内;而在定制开发中对 AOSP 做全量编译,通过 CoW 缓存复用编译产物后,编译耗时从约 46 分钟压缩到约 1 分半。
除了节点内的 volumes 缓存,前面提到的 docker:cache 可以把缓存做成 Docker 镜像推送到制品库,实现跨节点共享。一个典型用法如下:
main:
push:
- docker:
image: node:20
stages:
- name: build cache image
type: docker:cache
options:
dockerfile: cache.dockerfile
by:
- package.json
- package-lock.json
versionBy:
- package-lock.json
exports:
name: DOCKER_CACHE_IMAGE_NAME
- name: use cache
image: $DOCKER_CACHE_IMAGE_NAME
commands:
- cp -r "$NODE_PATH" ./node_modules
- name: build with cache
script:
- npm run build这样即使构建被调度到不同节点,也能拉取到同一份缓存镜像,避免每个节点都重新生成缓存。节点内缓存配合跨节点缓存,可以覆盖从单机高频构建到分布式调度的多种场景。
上面示例中,docker:cache 通过 dockerfile 指定用于构建缓存镜像的 Dockerfile,用 by 声明构建依赖的文件列表,用 versionBy 控制版本(对应文件内容变化才重建),构建出的缓存镜像名通过 exports 导出为环境变量,供后续 use cache 阶段拉取复用。
结合上面的机制,下面给出一个更完整的 Node.js 项目缓存配置示例:
main:
push:
- docker:
image: node:20
volumes:
- /root/.npm:copy-on-write
- node_modules:copy-on-write
runner:
cpus: 8
stages:
- node -v
- npm ci --frozen-lockfile
- npm run lint
- npm run test
- npm run build这里同时缓存了 npm 的包缓存目录 /root/.npm 和项目依赖目录 node_modules,前者加速依赖下载,后者让 npm ci 在依赖未变化时直接复用已安装的模块。
在实际配置时,可以遵循几条经验:
/root/.npm、~/.m2、/root/.gradle),它们的复用价值高、体积也较大。docker:cache 构建缓存镜像。关于成本,CNB 社区版采用免费额度加超额按量计费的方式,每月提供 160 核时的云原生构建 CPU 免费额度,缓存能力本身不单独计费,只需为构建实际消耗的计算资源付费。合理使用缓存把重复步骤跳过,本身也是降低构建资源消耗、控制成本的有效手段。
构建缓存解决的是重复构建中"每次都从头来过"的浪费。腾讯云 CNB 通过 .cnb.yml 中的 volumes 声明缓存目录,用 copy-on-write 写时复制模式让并发构建共享缓存而不冲突,并支持用 docker:cache 做跨节点共享,让相同依赖的后续构建大幅跳过重复的拉取与编译,把依赖准备和代码准备的时间从分钟级压到秒级。
如果你正在被重复构建的耗时问题困扰,不妨在自己的流水线里加上几行 volumes 缓存配置,体验一下腾讯云 CNB的构建提速效果。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。