极狐GitLab

极狐GitLab CI/CD 进阶:用 needs 构建 DAG 流水线

极狐GitLab
2026年8月28日
9200
分享:

极狐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
  - deploy

Runner 在调度作业时遵循一条硬规则:同一阶段的所有作业必须完成,下一阶段的任何作业才能启动。这条规则在"作业之间确实有依赖"时是合理的保护,但在"作业只是恰好被分到同一个阶段"时,它就会把整条流水线的关键路径拉长到最慢的那个作业。

直观地看:

  • 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_Abuild_app_A 成功后立刻启动,不用等 build_app_B

  • deploy_app_Atest_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 的全部意义,就是把这条路径缩到最短。

几个实战经验:

  1. 优先识别相互独立的作业。 多个服务、多个平台(Linux / Windows / macOS)、多个 Node 版本之间天然没有依赖,应分别写 needs,而不是塞到同一个阶段里相互等待。

  2. 把可立即启动的作业挪到 needs: [] 代码静态检查、YAML 校验、Secret 检测等可以在构建前就完成,它们一旦晚开始,就意味着开发者多等一轮反馈。

  3. 可选用 optional: true 处理条件作业。 当某个 needs 指向的作业可能因 rules 而不存在时,给它加 optional: true 可以避免流水线创建失败:

deploy:
  stage: deploy
  needs:
    - job: "test"
    - job: "test_optional"
      optional: true
  script: echo "正在部署..."
  1. 无阶段流水线(stageless pipeline)。 如果想完全脱离阶段概念,可以省略 stagesstage,让 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同一个项目里触发,配置文件通常来自 localtemplateprojectartifact

trigger_job:
  trigger:
    include:
      - local: path/to/child-pipeline.yml

特性:

  • 与父流水线共享项目、引用、提交 SHA

  • $CI_PIPELINE_SOURCEparent_pipeline

  • 嵌套最多两级(子流水线不能再触发孙流水线,除非突破默认限制)。

  • 子流水线详情在父流水线详情页可见。

多项目流水线(Multi-project pipelines)

trigger: project不同项目里触发:

staging:
  stage: deploy
  trigger:
    project: my/deployment
    branch: stable-11-2

特性:

  • 触发用户必须在下游项目有启动流水线的权限。

  • $CI_PIPELINE_SOURCEpipeline

  • 没有嵌套限制,因为它们是各自独立的流水线。

  • 在下游项目的流水线列表中可见。

怎么选

  • 同一仓库内的子任务(多语言构建矩阵、按服务拆分的 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' 不存在于流水线中。

解决方法有两种:

  1. 给依赖加 optional: true,让缺失依赖被忽略。

  2. 调整 rules 配置,让被依赖作业和依赖作业总是同时存在或同时不存在

写在最后

流水线优化的核心从来不是"加机器"或"切更快的 Runner",而是让作业之间的依赖图与真实业务依赖一致needs 让极狐GitLab CI/CD 的流水线可以从僵化的阶段模型,转向贴合真实依赖关系的 DAG 模型——独立作业并行、条件依赖可选、父子流水线分层编排。

当下一次有人问"为什么我的流水线 30 分钟才跑完"时,先打开任务依赖关系视图看关键路径,再判断是阶段顺序问题、作业并行度问题,还是上下游拆分问题。多数时候,答案都在那张 DAG 图里。