利用缓存提升构建速度

# 介绍

众所周知,缓存是性能优化一个重要手段。在 CI 领域,利用好缓存能极大提升流水线构建速度!

下面以前端 NodeJS 为例,介绍两种有效的利用缓存提升构建速度的方法。

# no cache

我们先准备一份 package.json ,里面有这些模块:

{
  "dependencies": {
    "angular": "^1.8.3",
    "ant-design-vue": "^3.2.15",
    "axios": "^1.3.4",
    "bootstrap": "^5.2.3",
    "chokidar": "^3.5.3",
    "eslint": "^8.36.0",
    "express": "^4.18.2",
    "husky": "^8.0.3",
    "jest": "^29.5.0",
    "jquery": "^3.6.4",
    "koa": "^2.14.1",
    "markdownlint": "^0.27.0",
    "nodemon": "^2.0.21",
    "nwjs": "^1.4.4",
    "react": "^18.2.0",
    "ts-jest": "^29.0.5",
    "typescript": "^4.9.5",
    "vue": "^3.2.47",
    "vuepress": "^1.9.9",
    "webpack": "^5.76.1"
  }
}

npm install 后看 node_modules 文件夹大小约 326 MB。

在流水线上执行 npm install,流水线配置文件:

master:
  push:
    - stages:
        - name: install
          script: npm install

执行效果:

no-cache

由于没有 cache,会从网络下载资源,耗时约 42s。

# volume cache

云原生构建 利用 Docker 的 volumes (opens new window) 功能,可在配置文件中通过声明 pipeline.docker.volumes, 将构建机上目录 mount 到容器中。构建任务可将下载好的依赖放入构建机缓存,供后续流水线使用。

Node 流水线配置文件:

master:
  push:
    - docker:
        volumes:
          - node_modules:copy-on-write
      stages:
        - name: install
          script: npm install

命中缓存后执行效果:

fit volume cache

可以看到前面截图中 added 1973 packages from 1072 contributors 字样消失了,无需从网络下载资源,耗时降为 11.5s。

volumes 的缺点是,缓存只在当前构建机有效。云原生构建 会根据项目构建并发情况,动态在3~8台构建机上轮转,后续流水线分配了其他无缓存的构建机,仍会重新从网络下载资源。

Maven 流水线配置文件:

master:
  push:
    - docker:
        #可以去 dockerhub 上 https://hub.docker.com/_/maven 找到您需要 maven 和 jdk 版本
        image: maven:3.8.6-openjdk-8
        volumes:
          - /root/.m2:cow
      stages:
        - name: build
          script: mvn clean package

Gradle 流水线配置文件:

master:
  push:
    - docker:
        #可以去 dockerhub 上 https://hub.docker.com/_/gradle 找到您需要 gradle 和 jdk 版本
        image: gradle:6.8-jdk8
        volumes:
          - /root/.gradle:copy-on-write
      stages:
        - name: build
          script: ./gradlew bootJar

# docker cache

云原生构建 还提供了一种 cache 方式:在一个镜像中 npm install 好依赖,然后缓存这个镜像在当前构建机,并推送到远端镜像源。

后续流水线使用时,若构建机有镜像缓存,则直接使用。若构建机无镜像缓存,则会从远端镜像源拉取下来。

docker:cache 内置任务使用示例:

master:
  push:
    - 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

cache.dockerfile 示例:

# 选择一个 Base 镜像
FROM node:16

# 设置工作目录
WORKDIR /space

# 将 by 中的文件列表 COPY 过来
COPY . .

# 根据 COPY 过来的文件进行依赖的安装
RUN npm ci

# 设置好需要的环境变量
ENV NODE_PATH=/space/node_modules

无镜像,需要构建并推送镜像的效果:

build cache

可以看到耗时比直接 npm install 长,有 1.3m。

构建机无镜像,远端有镜像,从远端拉取的效果:

pull cache

耗时降到约 16.3s。

构建机有镜像,直接使用的效果:

pull cache

耗时降到 2.2s,效果非常明显!

# 对比

# 缓存

  • volumes:缓存在构建机,效果好
  • docker:cache:缓存在构建机和远端,效果好

# 复杂度

  • volumes:配置简单,清晰易懂
  • docker:cache:配置复杂,涉及配置文件、dockerfile,有一定理解、使用成本

# 跨流水线

  • volumes:同一个构建机中,可跨流水线共享缓存,不可跨构建机
  • docker:cache:流水线执行过程中,流水线独享。缓存镜像构建完成、推送到远端后,可跨流水线、跨构建机使用

# 缓存更新

  • volumes:可控制读写权限,适用更多场景
  • docker:cache:重新构建缓存镜像并推送到远端,其他构建机需重新拉取。