最近在开发的过程中,发现部署的过程非常非常的慢,可能一个跑完完整的任务理想情况下都需要10分钟左右,如果同时任务多一点,甚至到20分钟,非常影响工作效率;尽管可以在部署任务的时候摸鱼,当然这个时候可以切换去做其他任~但是长此下去不行鸭。
分析
ci/cd中有很多job,得分析这些job的过程,卡在哪一步,才能针对过程进行优化。通常来说,一个job的“主任务”,也就是任务的核心过程,例如通过webpack构建网站资源,连接到服务器,发送文件等;这部分处理本身“主任务”之外,可能还跟对应服务器硬件资源较大关联,暂时这些就不列在这次的优化方向。
那么除了这些主任务之外,还涉及一些通用的,例如cache
与artifacts
,这些也是比较影响job的速度,所以这次处理方向也是这个。
优化结果
提前说一下优化的结果:
优化前,job数量2-3个的时候,大概10-20分钟;job数量在5-6的时候,需要25分钟左右;
优化后,job数量2-3个的时候,大概2-3分钟;job数量在5-6个的时候,大概5分钟左右;
可以对比,大概下降的达到70%到90%左右,极大提高开发效率。
优化手段
更改cache
影响范围
这个其中一个原因是原有的cache
策略设置不好,设置了全局的cache
策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# .gitlab-ci.yml
cache:
key: xx
path:
- node_modules
# job1
job1:
# job2
job2:
# job3
job3: |
因为设置了全局的cache
,这个cache
的内容是node_modules
, 而且job之间并没有对策略(policy
)进行更改,所以每个job执行的时候,开始都拉取cache(pull cache
),然后提取node_modules
;然后开始执行“主任务”,执行完任务之后,再把node_modules
打包压缩为cache.zip
,再进行上传:
1 2 3 4 5 |
download cache.zip extract node_modules // ... tar node_modules upload cache.zip |
所以每个job都至少执行了“下载-上传”的过程;但是有些任务可能并不需要node_modules
;所以整个node_modules
连下载都没必要,通常下载是从docker对应的云盘进行下载;这些io也会很耗费时间,所以这个时候就需要看任务对cache
的使用情况;
- 若大部分job都是需要
node_modules
缓存,只有少部分job不需要,则可以仍然设置全局cache
,对部分job显式更改策略:
1 2 3 |
job1:
# 不需要cache
cache: {} |
- 若只有一部分的job使用
cache
,那么则不设置全局cache
,只对job进行设定cache
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
job1:
cache:
key: "node_modules"
path:
- node_modules/
job2:
script:
- echo "this is job2"
job3:
script:
- echo "this is job3"
job4:
cache:
key: "node_modules"
path:
- node_modules/ |
不同的job可以使用相同的缓存,通过key
值来确定,例如上面的设置node_modules
为cache
的id值,job1
与job4
都会使用并且更新同一份cache
。
减少cache的上传次数
通过上面第一点的优化,可以把不需要cache
的任务过滤掉;但是有些地方还是会有重复,例如上面例子的job1
与job4
,两个job都对cache
进行下载与更新,但是是否需要更新这么频繁呢?是否另外的任务是不需要对cahce
进行更新,只需要获取就ok了?
对于这种情况,可以设置一个任务需要“下载-上传”cache,而其他需要到相同的cache,只进行下载,那么就可以省了“上传”的步骤;可以通过设置policy: pull
配置该策略;policy
的值有三个:
pull
只下载push
只上传,不下载pull-push
下载-上传(不设置的默认值)
假设上面提到job1
总是比job4
要先,所以job4
的策略可以做更改,删除job4的 npm install
过程
1 2 3 4 5 6 |
job4:
cache:
key: "node_modules"
path:
- node_modules/
policy: pull |
而为了保证这种顺序,job1
我们可以设置为一个独立的stage
,在执行job4
的之前,必须执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
stage:
- first
- second
job1:
stage: first
cache:
key: "node_modules"
path:
- node_modules/
job2:
job3:
job4:
cache:
key: "node_modules"
path:
- node_modules/
policy: pull |
所以我们可以把stage: first
的过程定义为获取node_modules
,也就是npm install
的过程,作为其他依赖node_modules
最基础的一个job
。这就完了?还有优化方向吗?
通常来说,node_modules
的变化次数不会特别多,所以尽管把npm install
的过程抽离出来,但是每次提交都执行npm install
,还要把巨大的cache.zip
上传一遍;耗费的时间也是不少。所以我们可以设置当npm
包发生更改的时候再执行npm install
的job;也就是package.json
发生改变,所以job1可以更改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
job1:
stage: first
cache:
key: "node_modules"
path:
- node_modules/
rules:
- changes:
- package.json
- yarn.lock # 或者 package-lock.json 等
when: always
- if: $YOUR_CUSTOME_VARIABLE
when: always |
上面的changes
数组表示,如果package.json
或者yarn.lock
发生改变之后,再运行job1;细心的朋友可能发现rules
的数组还有一个规则,if: $YOUR_CUSTOME_VARIABLE
,这个规则作用是啥?
因为cache
是不稳定的,有可能被删除;这个时候package.json
并没有更新,job4
就不能够获取cache
,job4就不能跑起来(上面提到,job4去掉npm install
过程);而这个if: $YOUR_CUSTOME_VARIABLE
规则就是加入可以人为主动控制触发job1
,用于下载npm install
;而这个变量是存在于gitlab仓库的ci variable
,或者主动触发pipeline
的时候加入。这样子整个流程就能完整保证运行起来。
小结
实际上运用起来更改比较简单,但是效果确实非常明显,对于上面的方法,小结如下:
- 更改
cache
影响范围,不适用的选择跳过 - 减少
cache
的更新次数,通常node_modules
不需要多个任务更新,独立到一个任务即可 - 按需触发
cache
的更新策略,并有人工干预手段进行处理,保证程序可用性
番外篇
实际上,之前旧的规则每次耗费这么多的时间,除了cache
控制不好之外,也有另外一个非常重要的原因。从上面可以知道,在没优化cache
策略之前,每个job都有对cache进行pull-push
的过程;在push
之前,ci会显示打包涉及的文件数量:node_modules/: found ${xxx} matching files
,其中${xxx}
为需要压缩为cache.zip
的文件数量;通过观察发现,跑的pipeline越多,打包的文件数量越来越多;最终导致cache.zip
体积也越来越大;甚至已经达到了30w+
的文件数量,这是非常恐怖的数字。要知道,项目初始化安装完node_modules
的时候,文件数量才3w+
,为什么文件数量的数量级会直接加了一个数量级?
因为npm安装的文件也不会每次更改,已经有lock
的相关文件,所以剔除涉及的npm包问题;那么node_modules
除了npm包之外还有什么哪些文件?.bin
与.cache
。.bin
的数量也是不变的;所以只能落到.cache
文件夹变化了。
node_modules/.cache
文件夹内容通常是cache-loader
存储缓存文件的地方,通过统计开发机的该文件夹的文件数量,惊人的发现,居然也有将近20w
的文件。其中.cache/vue-loader
的最多,达到10几w,其次是.cache/babel-loader
。
那么为啥.cache
文件夹的数量,因为我们在开发或者部署过程中,更改文件之后,就会生成新的一些cache
,逐渐逐渐的,.cahce
文件就越来越多。而优化前的cache
规则,是全局的,而且每次都会push
,所以就每次跑完pipeline
都会把该次所更新的.cache
增加文件,最后就导致文件非常庞大.....
测试篇
cache-loader
的目标是为了加快构建速度,下面做了一个测试,大家可以观察一下:
# | cache | no-cache |
---|---|---|
first run dev-serve | 40s | 38s |
second run dev-serve | 20s | 29s |
third run dev-serve | 20s | 28s |
add line | 5s | 5s |
trigger eslint error | 3s | 3.1s |
stop & restart dev-serve | 21s | 29s |
stop & update element & restart dev-serve | 25s | 30s |
first build | 62s | 60s |
second build | 23s | 30s |
update element & build | 30s | 33s |
测试顺序从上到下,cache
为使用cache-loader
,no-cache
没有使用,测试流程解析如下:
first run dev-serve
首次启动本地开发,通常是npm run serve
second run dev-serve
文件没有更改,再次启动本地开发third run dev-serve
与上一步一致add line
添加一行代码,添加的这行代码一致trigger eslint error
,这里也是添加一行代码,但是这行代码会触发eslint
的报错,例如执行一个不存在的函数stop && restart dev-serve
,关掉本地开发的serve
,然后再次启动stop && update element & restart dev-serve
,关掉serve
,然后更新部分元素,两次更新一致;然后再次启动serve
first build
,第一次执行build
任务,生产环境构建second build
,没有更改文件,再次执行build
任务update element & build
,更改部分元素,再次执行构建
从上面可以发现,cache-loader
没有什么优势;只在两次构建文件没变化的时候,有一定的提升;但是这种情况比较少,所以是否利用也是值得商榷。当然也有可能测试不够严谨,大家也可以测试过程。
End.
参考文章: