4、Stage与Job设计:Stage顺序执行与并行执行,Job的依赖关系(needs),Job的失败处理(allow_failure)

好,咱们接着聊。前面我们把Pipeline的骨架搭起来了,现在要往里面填肉了——也就是Stage和Job的具体设计。这部分我踩过的坑最多,也最有发言权。你想想看,嵌入式编译动不动就半小时一小时,如果Stage和Job设计得不好,那浪费的可不只是时间,还有开发者的耐心。

4.1 Stage顺序执行与并行执行:别让流水线变成单行道

Stage在GitLab CI里默认是顺序执行的。什么意思呢?就是上一个Stage里的所有Job都跑完了,下一个Stage才开始。这很符合直觉——你总得先编译通过,才能去做单元测试吧?

但这里有个常见的误区:Stage是顺序的,但同一个Stage里的Job是并行的。我见过不少新手把一堆Job塞到同一个Stage里,以为它们会乖乖排队跑,结果发现同时跑起来把服务器CPU干到100%。

核心原则:

  • Stage之间:顺序执行,前一个Stage全部成功后,后一个Stage才开始
  • Stage内部:Job并行执行,所有Job同时启动(除非你限制了资源或并发数)

举个例子,我在一个车载项目里是这样设计的:

stages:
  - build          # 编译阶段
  - unit_test      # 单元测试
  - static_analysis # 静态分析
  - package        # 打包
  - deploy         # 部署

这里build和unit_test是顺序的,但unit_test和static_analysis其实可以并行——它们互不依赖。不过我当时为了保险,还是把它们分成了两个Stage。后来想想,其实可以合并成一个Stage,让测试和分析同时跑,能省不少时间。

我的建议:把没有依赖关系的任务放在同一个Stage里,利用并行加速。比如代码检查、单元测试、覆盖率统计这些,完全可以同时进行。

4.2 Job的依赖关系(needs):打破Stage的枷锁

Stage顺序执行有个问题:太死板了。有时候你希望某个Job能跳过Stage的限制,直接依赖前面某个Stage里的特定Job。这时候就需要needs关键字了。

needs允许你定义Job之间的依赖关系,而不是依赖整个Stage。说白了,就是让Job可以「跳级」。

我记得有一次做固件升级包,编译完Bootloader和App之后,需要把它们打包在一起。但Bootloader编译快,App编译慢。如果按Stage走,必须等App编译完才能进打包Stage。用了needs之后,Bootloader一编译完就可以开始打包它的部分,不用干等App。

build_bootloader:
  stage: build
  script:
    - make bootloader

build_app:
  stage: build
  script:
    - make app

package_bootloader:
  stage: package
  needs: ["build_bootloader"]
  script:
    - ./package_bootloader.sh

package_app:
  stage: package
  needs: ["build_app"]
  script:
    - ./package_app.sh

你看,package_bootloader只依赖build_bootloader,不用等build_app跑完。这样流水线的整体时间就缩短了。

注意:使用needs时,被依赖的Job必须和当前Job在同一个Pipeline里。而且,如果被依赖的Job失败了,当前Job也会被标记为失败——除非你配合allow_failure使用。

还有一个坑:needs不能跨Pipeline使用。我曾经想在一个Merge Request的Pipeline里依赖另一个Pipeline的Job结果,结果发现根本不行。嗯,这个限制要记住。

4.3 Job的失败处理(allow_failure):给流水线一点容错空间

嵌入式开发里,有些Job失败了其实不影响大局。比如代码风格检查,失败了顶多代码丑点,编译还是能过的。但默认情况下,任何一个Job失败,整个Pipeline就红了。这很烦人。

allow_failure就是用来解决这个问题的。它允许你指定某些Job即使失败了,也不会阻塞后续Stage的执行。

lint_check:
  stage: static_analysis
  script:
    - cppcheck --error-exitcode=1 .
  allow_failure: true

这样配置之后,lint_check失败了,Pipeline会显示黄色警告,但后面的打包和部署照常进行。

我个人习惯把allow_failure用在以下几类Job上:

  • 代码风格检查:重要但不致命
  • 覆盖率统计:失败了不影响功能
  • 文档生成:文档没生成,代码还是能用的
  • 性能测试:性能不达标,但功能没问题

但要注意,allow_failure不是万能的。编译失败、单元测试失败这些核心步骤,绝对不能允许失败。我曾经在一个项目里把单元测试也设成了allow_failure: true,结果测试挂了没人发现,代码带着bug上线了。从那以后,我对allow_failure的使用就谨慎多了。

最佳实践:

Job类型 allow_failure 原因
编译 false 编译不过,后面全白搭
单元测试 false 测试失败说明代码有问题
代码风格检查 true 风格问题可以后续修复
覆盖率统计 true 覆盖率低不影响功能
集成测试 false 集成失败必须修复

4.4 组合使用:needs + allow_failure 的威力

这两个功能组合起来用,效果更佳。比如你有一个「代码分析」Job,它失败了不影响编译,但编译Job又需要它的结果来做一些决策。这时候可以这样:

code_analysis:
  stage: analysis
  script:
    - ./run_analysis.sh
  allow_failure: true

build:
  stage: build
  needs: ["code_analysis"]
  script:
    - make
  rules:
    - if: $CI_JOB_STATUS == "success"
      when: always
    - when: never

这样配置后,code_analysis失败了,build不会执行——因为needs默认要求被依赖的Job成功。但如果你希望即使分析失败也继续编译,可以在build里加上needs: ["code_analysis"]的同时,把code_analysisallow_failure设为true,然后在buildrules里处理。

嗯,这里有点绕。说白了就是:allow_failure让Job失败不阻塞Pipeline,但needs仍然会检查被依赖Job的状态。如果你想让依赖关系也「宽容」一点,需要额外配置。

避坑指南:我曾经在needsallow_failure的组合上翻过车。当时以为allow_failure: true的Job失败后,依赖它的Job会自动跳过,结果发现它还是会被标记为失败。后来查文档才知道,needs默认是「硬依赖」——被依赖的Job必须成功。要改成「软依赖」,得用needs: { job: "xxx", optional: true }

4.5 嵌入式场景下的实战建议

最后,结合嵌入式开发的特点,我给出几个实战建议:

  • 编译Job尽量并行:如果项目有多个模块(Bootloader、Kernel、App),可以拆成多个并行Job,用needs控制依赖关系。我见过一个项目,把编译时间从40分钟降到了15分钟。
  • 硬件测试Job要谨慎使用allow_failure:嵌入式经常有硬件相关的测试,比如Flash读写测试、外设通信测试。这些测试失败了,说明硬件可能有问题,不建议允许失败。
  • 利用Stage顺序做「门禁」:比如在编译Stage之后加一个「冒烟测试」Stage,只有冒烟测试通过了,才允许进行后续的集成测试和部署。这样能尽早发现问题。
  • 不要滥用needsneeds虽然灵活,但用多了会让Pipeline的依赖关系变得复杂,难以维护。我一般只在「确实需要跳过Stage」的场景下才用。

好了,Stage和Job的设计就聊到这里。说白了,就是要在「顺序执行」和「并行执行」之间找到平衡,用needs打破僵局,用allow_failure保留容错空间。下一章我们聊聊更高级的话题——Pipeline的触发条件和变量传递。