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_analysis的allow_failure设为true,然后在build的rules里处理。
嗯,这里有点绕。说白了就是:allow_failure让Job失败不阻塞Pipeline,但needs仍然会检查被依赖Job的状态。如果你想让依赖关系也「宽容」一点,需要额外配置。
避坑指南:我曾经在needs和allow_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,只有冒烟测试通过了,才允许进行后续的集成测试和部署。这样能尽早发现问题。
- 不要滥用needs:
needs虽然灵活,但用多了会让Pipeline的依赖关系变得复杂,难以维护。我一般只在「确实需要跳过Stage」的场景下才用。
好了,Stage和Job的设计就聊到这里。说白了,就是要在「顺序执行」和「并行执行」之间找到平衡,用needs打破僵局,用allow_failure保留容错空间。下一章我们聊聊更高级的话题——Pipeline的触发条件和变量传递。