作者:Whilconn
原文:https://juejin.cn/post/7135756687134162980
1. 背景
近接手的BI
项目在Jenkins
的构建机上构建耗时比较久,日常构建耗时都在 20min 以上,即使改动一行代码也要构建这么久。构建耗时截图如下:
构建耗时较长导致日常测试和正式发版都会浪费很多时间等待,对研发流程影响较大(主要是我忍不了)。因此需要对构建速度进行优化。
2. 优化思路分析
要优化项目的构建速度,得先了解构建流程:
开发人员推送代码到 Gitlab
,触发Gitlab
服务器的Push Events
Push Events
被触发后,会调用提前配置好的Jenkins webhooks
Jenkins webhooks
被调用后,会执行对应项目的构建任务构建任务开始后先拉取项目源码到构建机,再使用 docker build
构建镜像docker
构建镜像分为两个阶段,先使用npm scripts
构建前端项目,然后把构建产物拷贝到nginx
基础镜像
在这个流程中,可以优化的环节只有构建docker
镜像这一步,其他环节的耗时基本可以忽略不计。而在不大改项目的情况下能起到明显提速效果的方案是:缓存策略。构建docker
镜像时可以用到的缓存包括两类:docker层缓存
和应用层缓存
。
docker层缓存
是指docker build
所产生的可重用镜像层,只要Dockerfile
中的命令及相关的源文件未改变,就能直接使用这些镜像缓存。这种缓存策略在代码不改变的情况下效果很好,构建耗时甚至可以控制在 10 秒内。而对于日常开发情况下,代码频繁变化,如果应用本身构建时间又很长,则需要使用应用层缓存
。(上一篇文章《docker build 缓存失效分析》中有 docker 层缓存相关介绍,也可以看看官方文档[1]、中文文档[2],本文不再赘述)
应用层缓存
是指应用构建所产生的中间产物,这些中间产物主要是node_modules
目录中的物理文件,其中包括npm install
下载的依赖包和npm run build
产生的.cache
目录文件。而docker build
每次都会初始化全新的环境用于构建,新环境中不存在node_modules
目录,因此每次都是重新写入而无法复用,得想办法复用该目录下的文件;另外npm run build
需要开启缓存功能,才会输出缓存文件到node_modules/.cache
目录。
综上,优化思路主要是两点:1、开启应用层构建缓存(如webpack cache
);2、持久化node_modules
目录,确保每次npm install
和npm run build
都能复用该目录下的文件。
3. 开启应用层构建缓存
项目使用的技术是React
,构建主要依靠react-scripts@4.0.3
,底层实际调用的是webpack@4.44.2
,应用构建缓存主要来自webpack
。webpack
需要手工开启缓存功能(官方文档传送门[3]),配置cache
属性为true
即可。
实际操作只有 1 步, 找到webpack.config.js
设置cache:true
,代码如下:
module.exports = {
//...
cache: true
};
本地npm run build
构建,无缓存的情况下,耗时 13min 左右。
启用缓存后在本地进行二次构建,有缓存的情况下,无论是否修改源码构建耗时均为 4min 左右,比优化前的 13min 有明显提升。 构建耗时截图如下:
实际上,webpack@4
的缓存只在watch
和development
模式下生效,在上述构建测试中其实不起作用。 实测删除wepack
中的cache:true
配置,或者配置为cache:false
,二次构建时间也是 4min 左右。
之所以构建速度提升了那么多,是因为react-scripts
的webpack
配置[4]中开启了babel-loader
和eslint-webpack-plugin
的缓存功能,另外terser-webpack-plugin
配置[5]也默认开启了缓存功能。从缓存目录node_modules/.cache
中也能看到它们的缓存文件。
所以,这一步其实啥也不用做,如果想进一步提速可以升级到webpack@5
。
4. 持久化node_modules
目录
想在docker build
环境中持久化node_modules
需要使用到BuildKit
的mount
功能,该功能有几个前置条件:
docker
版本必须高于 18.09BuildKit
需要手工启用[6],可在docker build
命令前添加环境变量DOCKER_BUILDKIT=1
启用如果前两个条件不满足,则需要具备 Jenkins
和构建机的读写权限,以调整构建环境参数修改 Dockerfile
,使用RUN --mount=type=cache
运行npm install
和npm run build
指令(--mount=type=cache
说明文档传送门[7])
开启
BuildKit
还有其他特性[8],比如输出日志更友好,基本每一步都会输出耗时,就这一条,值了!
实际操作分为 2 步:1、修改Jenkins
配置,在docker build
命令前加上环境变量。修改后镜像构建命令长这样:
DOCKER_BUILDKIT=1 docker build .
2、修改Dockerfile
,将RUN npm install
和RUN npm run build
指令改为RUN --mount=type=cache npm xxx
。修改后Dockerfile
长这样:
FROM node:alpine as builder
WORKDIR /app
COPY package.json /app/
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
--mount=type=cache,target=/root/.npm,id=npm_cache \
npm i --registry=https://registry.npm.taobao.org
COPY src /app/src
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
npm run build
文档说由于
BuildKit
为实验特性,需要在Dockerfile
文件开头加上如下代码:# syntax = docker/dockerfile:experimental
。在Docker 20.10
环境下,加了上述代码反而构建报错,原因是加载外网资源失败,删除后构建成功。这不就是玄学吗?🤡
5. 优化结果
在配置好缓存策略后,模拟日常开发修改项目代码触发自动构建流程,构建耗时从 20min+下降到 4min+,总体耗时减少 80%。整个优化过程修改了Jenkins
的一行配置,另外在Dockerfile
中添加了3行代码,改动很少但效果很不错。