晚上八点,小林推开实验室的门,发现老周正盯着流水线页面发呆。
「周哥,看什么呢?」
「你昨天的流水线。」老周指了指屏幕,「lint 跑 40 秒,test 跑 1 分 20 秒。build 要是好了,至少 3 分钟。」
「那怎么了?」
「你每次推代码,都等 5 分钟。」
「一天推 10 次,就是 50 分钟。一年呢?」
小林拿出手机算了算。
「按 250 个工作日算……大概 208 个小时。」

顺便说一句:50 分钟/天 × 365 天 = 304 小时,× 250 个工作日 = 208 小时。
两个数都对,但口径不同。报出来的账,得经得起追问。
「208 小时,差不多 26 个工作日。」老周说。
「而且 Runner 跑着占 CPU、内存、网络,这些在云上都是按分钟计费的。」
「所以今天教你两个东西:cache 和 artifacts。一个省时间,一个省力气。」

一、这俩到底差在哪
老周在白板上画了两栏:
- cache(缓存):跨流水线复用,存依赖和中间产物,加速后续构建
- artifacts(制品):同一流水线内传递,存构建结果和测试报告,可下载
「简单说 ——」
「cache 是『上次留下的东西,这次接着用』;artifacts 是『这次做出来的东西,给别人用』。」
小林问:「pip 依赖算哪个?」
「cache。因为下次构建还要用。」
「那测试报告呢?」
「artifacts。因为它是这次做出来的,给人看的。」

老周给了个口诀:
「『下次还要用的』是 cache,『这次做出来的』是 artifacts。」
二、配个缓存,先快起来
test-api:
stage: test
image: python:3.13-slim
cache:
key: api-deps
paths:
- .pip-cache/
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
script:
- pip install -r api/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
推一次,再跑 —— 从 30 秒降到 8 秒。
「快了。」小林说。
「但先别高兴。」老周说,「你这个写法里,藏着一个小矛盾。」
他指了指原来的 script:
pip install --no-cache-dir -r api/requirements.txt
「--no-cache-dir 和 PIP_CACHE_DIR 是打架的。」
「一个说『禁用缓存目录』,一个说『缓存放这儿』。两个都写,pip 听前者的:一个字节都不写。」
「那为什么刚才变快了?」
「因为 GitLab 那一层缓存仍然生效,但 pip 不会再往里写新东西 —— 也就是说,你今天快,下周依赖更新时它会突然不快。」
「把 --no-cache-dir 去掉。」

这条坑不只在 GitLab CI 里。任何地方配 pip 缓存,都容易同时留下这两个参数。
看到「缓存配了但没效果」,先搜 --no-cache-dir。
三、让缓存跟着依赖自动失效
「那如果依赖变了呢?」
「问得好。」老周说,「requirements.txt 变了、缓存还是旧的,可能装成旧版本的包,你却以为装的是新的。」
「让 key 跟着文件走。」
cache:
key:
files:
- api/requirements.txt
「requirements.txt 一变,key 就变,旧缓存自动失效。」
四、artifacts:把测试报告留下来
test-scripts:
stage: test
image: bats/bats:latest
script:
- bats --formatter tap tests/ | tee test-results.tap
artifacts:
name: "bats-results-$CI_COMMIT_SHORT_SHA"
paths:
- test-results.tap
expire_in: 1 week
「tee 同时输出到屏幕和文件,expire_in 一周后自动删。」老周说。
「在哪下载?」
「流水线页面,任务右侧有 Browse 和 Download。」
下载打开长这样:
1..6
ok 1 disk-check 正常运行时退出码为 0 或 2
ok 2 disk-check 阈值无效时退出码为 1
...
「这就是制品。你可以把它接到监控、报表、通知。」
五、制品还能在阶段之间传
「build 构建了镜像,deploy 要部署 —— 怎么让 deploy 知道该部署哪个版本?」
「写进文件,当制品传下去。」
build-image:
script:
- echo "$DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA" > image-tag.txt
artifacts:
paths:
- image-tag.txt
deploy-api:
needs:
- build-image
script:
- IMAGE_TAG=$(cat image-tag.txt)
「needs让 deploy 等 build 完成,并自动下载它的制品。」
但 needs 有个副作用要注意:它会打破「必须等上一个 stage 全绿」的默认规则。
写了 needs: [build-image],它就只等 build-image,不再等 test 阶段。
能省时间,也可能让「测试没过就部署」成为现实。所以 needs 里写谁,要沿着真实依赖链写。
六、别忘了清理
「缓存和制品不能无限存。」
Admin Area → Settings → CI/CD:
- Default artifacts expiration:默认 30 天,改成 7 天
- Keep the latest artifacts for all jobs:保留最新制品
Runner 本地也有缓存:
[runners.cache]
Type = "local"
Path = "/cache"
Shared = true
「教学环境本地缓存够用。生产环境可以用 S3、MinIO 做分布式缓存 —— 否则每个 Runner 各存一份,命中率很低。」
「那缓存坏了呢?」
「清掉。GitLab 界面 CI/CD → Pipelines → Clear runner caches。」
七、优化之后
lint passed
test passed ← 从 1 分 20 秒降到 50 秒
build failed (allowed) ← Registry 还没部署
「test 快了 30 秒。」小林说。
「这就是缓存的威力。」
小林收拾东西,走到门口回头问:
「周哥,省下来的时间干什么?」
「干更重要的事。」老周说,「比如设计更好的流水线,或者早点下班。」
「那省下来的钱呢?」
「买更好的服务器。」老周笑了,「或者给领导看:自动化之后资源利用率提高了,可以申请更多预算。」
下一篇:《自动构建镜像》—— 你会把配置改对,但改到另一台机器上。