每个作业都在传一遍产物,流水线慢的根子可能就在这
每个作业都在传一遍产物,流水线慢的根子可能就在这
先明确一件事:作业产物(artifacts)是作业跑完后归档出来的文件或目录,比如编译产物、测试报告、构建出的镜像清单。它的默认行为常常被忽略——后面阶段的作业会把前面阶段所有作业的产物都下载一遍。官方文档写得很直白:默认情况下,后续作业会获取早期阶段作业的所有产物的副本。一个十几步的流水线,很容易变成几十次无意义的重复下载。
为什么流水线越优化越慢
很多团队优化流水线的顺序是反的:先换更大的 Runner,再加缓存,最后才想起看作业日志里那几十秒的「Downloading artifacts」。
三段真实代价:
时间:每个作业开跑都要先解压一遍产物归档,这一步在慢存储或跨可用区时能占到作业时长的三成。
存储:产物不是免费的,单次流水线产物动辄几百 MB,加上每个引用上保留的历史流水线,份额涨得比代码快得多。
正确性:把产物当发布存储用,是官方明确劝阻的做法——保留「最近成功作业产物」的设置会忽略
expire_in,而最新产物的 URL 每次新流水线都会指向不同的文件,下游按 URL 取包迟早会取错版本。
换句话说,产物传递链没梳理清楚,前面所有优化都在给一条漏水的管子加压。
三步把产物传递链收窄
第一步:先找出谁在无脑下载
打开任意一次流水线的作业日志,搜索 Downloading artifacts。凡是明明只用到一两个文件的作业,却在下载整个归档,就是可优化点。判断标准很简单:这个作业的脚本里到底引用了哪些路径。
第二步:用 dependencies 或 needs:artifacts 精准指定来源
dependencies 和 needs:artifacts 都会改变默认行为——一旦使用,产物只从你点名的作业里取,不再全量拉取。
stages: [build, test, package]
build-app:
stage: build
script: ["./build.sh"]
artifacts:
paths:
- dist/
expire_in: 3 days
unit-test:
stage: test
needs:
- job: build-app
artifacts: true # 只用 build-app 的产物
script: ["./run-tests.sh"]
lint:
stage: test
dependencies: [] # 这个作业根本不需要产物,直接关掉
script: ["./lint.sh"]dependencies: [] 是最容易被忽视的一行。把不需要产物的作业全部显式关掉,往往能直接砍掉三成以上的下载时间。
第三步:把归档缩小,把过期时间定清楚
只列后续作业真正需要的路径,用 artifacts:exclude 剔除中间文件,再用 expire_in 兜底:
build-app:
stage: build
script: ["./build.sh"]
artifacts:
paths:
- dist/
exclude:
- "dist/**/*.map"
- "dist/tmp/**/*"
expire_in: 3 days如果完全没写 expire_in,用的是实例级的默认产物过期时间;写了之后,保留时长就按你的配置走。另外单次产物归档有大小上限,默认每 100 MB,实例、群组、项目都能单独调,改小一点反而能逼着团队把归档做干净。
产物不该承担发布职责
这是一个比性能更贵的问题。产物的生命周期跟着流水线走,发布资产的生命周期跟着版本走,两者混在一起,早晚出事。
官方文档专门提醒过:不要将作业产物用作发布存储。原因很实在——开启「保留最近成功作业的产物」后,这批产物会忽略 expire_in,而某个分支上「最新产物」的 URL 每次流水线都会指向不同的文件。下游如果有人拿这个 URL 去拉取,拿到的可能是三次流水线之前的包。
正确分工是这样的:
需求 | 该用什么 | 不该用什么 |
|---|---|---|
阶段间传编译结果 | 作业产物 + | 制品库 |
测试报告、覆盖率、SBOM |
| 制品库(报告类产物会被流水线直接读取,不能挪走) |
交付给下游的包 | 软件包仓库(通用包、Maven、npm 等) | 作业产物 |
对外发布的版本 | 发布(Release)+ 软件包仓库 | 分支上「最新产物」的 URL |
软件包仓库的容量量级和产物不是一个水平:通用软件包单个文件上限可以到 5 GB,而作业产物归档默认每个 100 MB。把该进制品库的东西挪过去,产物的存储曲线会立刻平下来。
改完之后怎么衡量
不要只凭感觉说「快了一点」。改之前先记三个数,改完对比:
指标 | 观察方式 | 合理预期 |
|---|---|---|
单次流水线「Downloading artifacts」总耗时 | 作业日志累加 | 明显下降,通常能砍掉一半以上 |
产物存储占用 | 项目设置里的用量视图 | 一周内趋于平稳,不再随流水线数量线性上涨 |
流水线端到端时长 | 流水线详情页 | 有改善,但幅度小于第一项——它受最慢作业限制 |
三个容易踩到的坑
坑一:exclude 不是递归的。 写 dist/tmp/ 并不会排除这个目录下的所有文件,必须显式匹配,比如 dist/tmp/**/*。这一点官方文档专门做过提示,也是最常见的配置错误。
坑二:expire_in 不等于「到点就删」。 每个引用上最近完成的流水线、最近成功的流水线的产物默认会被保留,任何 expire_in 都对它们不生效。所以在主分支上跑一次就指望产物自动消失,不会发生。
坑三:跨项目取产物要写全三要素。 用 needs:project 时,项目路径、作业名、ref 必须一起给,ref 写错就会拿到另一条分支上的旧产物,排查时极难看出问题。
常见问题
dependencies 和 needs 该用哪个? 项目里已经在用 needs 做 DAG 并行,就统一用 needs:artifacts,别两套混用;纯阶段模型的老项目用 dependencies 改动更小。
产物默认什么时候上传? 默认只在作业成功时上传(on_success)。失败也要留产物(比如测试报告、崩溃日志),显式写 artifacts:when: on_failure 或 always。
产物能给外部人下载吗? 可以限制,artifacts:access 控制谁能下载,敏感项目的产物别默认对全项目成员开放。
动手清单
在流水线日志里搜
Downloading artifacts,列出下载量最大的三个作业;给不需要产物的作业加
dependencies: [];给需要的作业改用
needs:artifacts点名来源;收敛
paths、补exclude、统一expire_in;把产物大小上限下调一档,观察一周存储曲线。
需要对照官方语义细节,可以查 作业产物文档 与 CI/CD YAML 参考。想直接在自己的流水线上看一遍效果,可以在极狐GitLab 上开一个项目的免费试用,把上面几段配置贴进去跑两轮对比作业时长。
一句话收尾:流水线的性能问题,一半出在算力上,另一半出在「不必要的数据搬运」上。先把后者清干净,再去谈扩容。

