极狐GitLab CI/CD 进阶:用 needs 构建 DAG 流水线
极狐GitLab CI/CD 进阶:用 needs 构建 DAG 流水线
在很多团队眼里,CI/CD 流水线被默认为"按阶段顺序执行"——build 跑完才能跑 test,test 全部通过才能 deploy。这种阶段性流水线(staged pipeline)最符合直觉,但也最容易在并行作业上浪费整条流水线的关键路径。
举一个最常见的例子:一个 monorepo 同时维护前端(App A)和后端服务(App B),两者的构建、测试、部署相互独立。阶段性流水线会让 B 的构建拖住 A 的测试与部署——即使 A 早就准备好了,也要等 B 的 build 完成后才能进入下一阶段。这种等待并不是因为依赖关系,而是因为阶段边界。
极狐GitLab CI/CD 提供的 needs 关键字正是为这种场景设计的:让作业在自己的真实依赖完成后立即启动,不再受阶段顺序的牵绊。
01 默认阶段流水线为什么会卡
阶段(stages)是传统流水线的骨架。在 .gitlab-ci.yml 中这样声明:
stages:
- build
- test
- deployRunner 在调度作业时遵循一条硬规则:同一阶段的所有作业必须完成,下一阶段的任何作业才能启动。这条规则在"作业之间确实有依赖"时是合理的保护,但在"作业只是恰好被分到同一个阶段"时,它就会把整条流水线的关键路径拉长到最慢的那个作业。
直观地看:
A 的构建耗时 2 分钟,B 的构建耗时 10 分钟。
A 的测试只要 1 分钟,B 的测试要 5 分钟。
阶段性流水线下,A 必须等 B 的 build 完成才能开始 test,总耗时 ≈ 2 + 10 + 1 + 5 = 18 分钟。
但 A 的真实链路只需要 2 + 1 = 3 分钟。
这 15 分钟的差距不是 A 自己的问题,是流水线结构强加的等待成本。
02 needs:让作业按真实依赖启动
needs 是 CI/CD YAML 的一个关键字,用来显式声明一个作业依赖哪些其他作业。被声明的依赖完成后,这个作业立即启动,不再等待整个阶段结束。
一个最简示例:
build_app_A:
stage: build
script: echo "正在构建 A..."
build_app_B:
stage: build
script: echo "正在构建 B..."
test_app_A:
stage: test
needs: ["build_app_A"]
script: echo "正在测试 A..."
test_app_B:
stage: test
needs: ["build_app_B"]
script: echo "正在测试 B..."
deploy_app_A:
stage: deploy
needs: ["test_app_A"]
script: echo "正在部署 A..."
deploy_app_B:
stage: deploy
needs: ["test_app_B"]
script: echo "正在部署 B..."在这个例子里:
test_app_A在build_app_A成功后立刻启动,不用等build_app_B。deploy_app_A在test_app_A完成后立刻启动,不用等 B 链路。两条链路并行推进,A 链路的总耗时只取决于 A 自己的作业。
作业之间的真实依赖被画成一张有向无环图(DAG, Directed Acyclic Graph),这就是 DAG 流水线这个名字的由来。极狐GitLab 在流水线详情页提供"任务依赖关系"视图(Pipeline graph → 任务依赖关系),可以直观看到这张图。
03 四种典型依赖模式
needs 真正强大的地方,是它能表达多种依赖模式,而不只是"单线串联"。下面四种模式是日常工程里最常见的。
扇出(Fan-out)
一个作业完成后,并行启动多个后续作业:
stages:
- build
- test
build:
stage: build
script: echo "正在构建..."
test_unit:
stage: test
needs: ["build"]
script: echo "单元测试..."
test_integration:
stage: test
needs: ["build"]
script: echo "集成测试..."
test_performance:
stage: test
needs: ["build"]
script: echo "性能测试..."扇入(Fan-in)
多个并行作业汇入到一个后续作业:
deploy:
stage: deploy
needs: ["test_frontend", "test_backend"]
script: echo "正在部署..."菱形依赖(Diamond)
扇出和扇入的组合:
deploy:
stage: deploy
needs:
- "test_unit"
- "test_integration"
- "test_performance"
script: echo "正在部署..."立即启动(needs: [])
一些作业(lint、YAML 校验、Secret 检测等)不依赖任何构建产物,可以让它们在流水线创建时就立刻跑:
lint_yaml:
stage: test
needs: []
script: echo "正在对 YAML 进行 lint 检查..."
lint_code:
stage: test
needs: []
script: echo "正在对代码进行 lint 检查..."needs: [] 让作业不依赖任何早期作业或阶段直接开始,常用于那些只需要源代码、不需要构建产物的检查任务。
04 关键路径优化:让流水线真正快起来
判断一个流水线是否高效,关键看它的关键路径——即从第一个作业启动到最后一个作业结束的最长依赖链。needs 的全部意义,就是把这条路径缩到最短。
几个实战经验:
优先识别相互独立的作业。 多个服务、多个平台(Linux / Windows / macOS)、多个 Node 版本之间天然没有依赖,应分别写
needs,而不是塞到同一个阶段里相互等待。把可立即启动的作业挪到
needs: []。 代码静态检查、YAML 校验、Secret 检测等可以在构建前就完成,它们一旦晚开始,就意味着开发者多等一轮反馈。可选用
optional: true处理条件作业。 当某个needs指向的作业可能因rules而不存在时,给它加optional: true可以避免流水线创建失败:
deploy:
stage: deploy
needs:
- job: "test"
- job: "test_optional"
optional: true
script: echo "正在部署..."无阶段流水线(stageless pipeline)。 如果想完全脱离阶段概念,可以省略
stages和stage,让needs成为唯一的编排手段:
compile:
script: echo "正在编译..."
unit_tests:
needs: ["compile"]
script: echo "正在运行单元测试..."
integration_tests:
needs: ["compile"]
script: echo "正在运行集成测试..."
package:
needs: ["unit_tests", "integration_tests"]
script: echo "正在打包..."这种方式下,所有未声明 stage 的作业都会落在默认的 test 阶段下,但执行顺序完全由 needs 决定。
05 流水线之间:父子流水线 vs 多项目流水线
needs 解决的是同一条流水线内部的依赖。当需要把工作分发到另一条流水线时,就要用到 下游流水线(downstream pipelines)。极狐GitLab 提供两种下游流水线,理解它们的差别对架构选型很关键。
父子流水线(Parent-child pipelines)
由 trigger: include 在同一个项目里触发,配置文件通常来自 local、template、project 或 artifact:
trigger_job:
trigger:
include:
- local: path/to/child-pipeline.yml特性:
与父流水线共享项目、引用、提交 SHA。
$CI_PIPELINE_SOURCE为parent_pipeline。嵌套最多两级(子流水线不能再触发孙流水线,除非突破默认限制)。
子流水线详情只在父流水线详情页可见。
多项目流水线(Multi-project pipelines)
由 trigger: project 在不同项目里触发:
staging:
stage: deploy
trigger:
project: my/deployment
branch: stable-11-2特性:
触发用户必须在下游项目有启动流水线的权限。
$CI_PIPELINE_SOURCE为pipeline。没有嵌套限制,因为它们是各自独立的流水线。
在下游项目的流水线列表中可见。
怎么选
同一仓库内的子任务(多语言构建矩阵、按服务拆分的 CI 流程、动态生成的配置)→ 父子流水线 +
artifact触发。跨仓库的部署、跨团队的流水线编排、跨产品的端到端验证 → 多项目流水线。
如果父流水线需要"等子流水线结束才算成功",触发作业应该加上 strategy: mirror,让触发作业的状态镜像下游流水线状态,而不是依赖默认的 depend 行为。
06 避坑:循环依赖与孤儿依赖
needs 的 DAG 特性意味着作业之间必须有严格的偏序关系,不能形成环。一旦出现循环依赖,流水线图会直接拒绝渲染:
job_a:
needs: ["job_b"]
job_b:
needs: ["job_a"]这种写法在极狐GitLab 中会立即报错,提示作业之间存在循环依赖。
更隐蔽的问题是"孤儿依赖"——一个作业通过 needs 引用了一个因为 rules 条件不满足而没有被加入流水线的作业。极狐GitLab 会给出明确的报错信息:
'unit_tests' 作业依赖于 'compile' 作业,但 'compile' 不存在于流水线中。
解决方法有两种:
给依赖加
optional: true,让缺失依赖被忽略。调整
rules配置,让被依赖作业和依赖作业总是同时存在或同时不存在。
写在最后
流水线优化的核心从来不是"加机器"或"切更快的 Runner",而是让作业之间的依赖图与真实业务依赖一致。needs 让极狐GitLab CI/CD 的流水线可以从僵化的阶段模型,转向贴合真实依赖关系的 DAG 模型——独立作业并行、条件依赖可选、父子流水线分层编排。
当下一次有人问"为什么我的流水线 30 分钟才跑完"时,先打开任务依赖关系视图看关键路径,再判断是阶段顺序问题、作业并行度问题,还是上下游拆分问题。多数时候,答案都在那张 DAG 图里。

