4. 任务拆分策略:如何将大任务拆分为小任务

你有没有碰到过这种情况——CI流水线跑一次要40分钟,最后失败在某一步。你气得想摔键盘,但又不得不等下一轮。我懂,我也经历过。

说实话,很多CI问题根源就一个:任务太大了。一个大任务就像一个黑盒子,你根本不知道里面哪个环节出了问题。最好的应对就是想办法把它拆碎。

4.1 拆分原则:标准是什么

我刚开始做CI拆分时,也走过弯路——看见一个大阶段就一顿乱切,结果反而跑得更慢。后来总结出三条核心标准:

单一职责、独立可运行、早反馈

单一职责:每个任务只干一件事。编译就是编译,测试就是测试,别混在一起。我记得有一次,有个同事把编译和代码检查放在同一个job里,结果编译挂了,检查的结果根本看不到。你想想看,这不就是浪费吗?

独立可运行:拆分出来的每个小任务,都应该能单独跑。不需要依赖另一个任务的中间产物。比如编译任务完了,会生成一个可部署的包,后面的测试任务只需要这个包就行。这样即使测试挂了,编译结果也不会丢失。

早反馈:越快暴露问题越好。我会把lint, 单元测试这些耗时少的任务放在前面。一旦出错,马上就能发现,不用等后面的构建跑完。

原则 坏例子 好例子
单一职责 一个job里又编译又部署 编译、部署各自独立job
独立可运行 测试job依赖上一个job的临时文件 测试job使用制品仓库里的构建产物
早反馈 单元测试放在集成测试之后 先跑lint再跑单元测试最后跑集成

「我曾经因为没注意验收标准,把一个"大任务"拆成了10个小任务,结果每个任务都要重复安装依赖,总耗时反而增加了。说白了,拆分不是越细越好,恰到好处才是关键。」

4.2 常见的三种拆解模式

实战中我主要用三种拆解模式。大家可以根据项目类型选:

模式一:按阶段拆分

  • 编译阶段:只做代码编译、打包
  • 静态检查阶段:lint、sonar、安全扫描
  • 测试阶段:单元测试→集成测试→端到端测试

这种模式最通用,适合大多数Web后端项目。我个人习惯把安全扫描放到静态检查阶段最后,因为它的执行时间不太稳定。

模式二:按代码模块拆分

  • 商品服务单独的编译和测试
  • 订单服务单独的编译和测试
  • 用户服务单独的编译和测试

这种模式适合微服务架构。记得我在一个电商项目里就是这样做的。每个服务独立跑,任何改动只会触发对应服务的CI。效果很明显,平均流水线耗时从原来的30分钟降到了8分钟。

模式三:按功能边界拆分

  • 基础库编译
  • 业务逻辑编译
  • 前端构建
  • 后端构建

嗯,这种适合前后端分离或依赖公共库的项目。我会把公共库的编译单独放在最前面,然后同时启动前后端构建。注意,这里有个小技巧——前后端构建可以并行,别串行跑浪费时间。

4.3 实战拆解:从流水线到Job

光说理论你们可能有点懵。我直接拿一个典型的微服务项目举例,看看具体的流水线配置文件长什么样。


# .gitlab-ci.yml 示例片段
stages:
  - lint
  - build
  - test
  - deploy

# 1. 代码风格检查——最快
lint-job:
  stage: lint
  script: npm run lint
  only:
    - merge_requests

# 2. 编译构建
build-api:
  stage: build
  script:
    - cd services/api
    - mvn clean package

build-web:
  stage: build
  script:
    - cd services/web
    - npm run build

# 3. 测试阶段——分两步
unit-test:
  stage: test
  dependencies:
    - build-api
    - build-web
  script: npm run test:unit

integration-test:
  stage: test
  dependencies:
    - build-api
  script: npm run test:integration

看到没?编译阶段里,build-api和build-web可以并行跑。而测试阶段也拆分成了unit-test和integration-test,这样如果单元测试挂了,后面的集成测试就不用跑了。

但我要提醒一点:不要所有任务都并行。我之前有个项目,把20个微服务的测试全部并行跑,结果CI Runner直接爆了。并行任务数要根据CI Agent的资源来定,别贪多。

个人经验:对于Java项目,建议把编译和单元测试写在一个job里;因为Maven/Gradle的增量编译需要上下文,拆开反而会每次全量编译,得不偿失。说白了,拆分的粒度要看构建工具的特性。

4.4 特殊任务的处理技巧

有些任务天生比较大,不好拆。比如数据库迁移、数据清洗。我的做法是加一个「手工确认」环节。

举例:

  1. 先把迁移脚本拆分出来单独做测试
  2. 后续的集成测试等需要跑迁移的,用一个变量控制:NEED_MIGRATE: true/false
  3. 最后生产环境的迁移走一个单独的job,需要手工点一下

嗯,那有些跨团队的任务呢?例如集成测试需要两个团队的服务都部署好。这时候我会使用「分阶段触发」机制。第一个阶段,团队A的服务部署;第二阶段,团队B的服务部署。每个阶段跑完后通知另一个团队,再触发下一阶段。

注意:这种跨团队的大任务,不要强行塞到一个CI Pipeline里。我的经验是——各自维护CI,然后在之间通过API传递信息。强行合并只会让流水线越来越慢,最后谁都不想管。

4.5 使用工具辅助拆分

手动拆分配置文件很麻烦,尤其项目大了之后。我推荐大家用JOB模板或配置生成器来自动化这个事。

拿GitLab CI举例:


# 模板定义
.job_template: &job_definition
  script:
    - echo "Running job"
  stage: test

# 使用模板自动生成多个job
unit-test-api:
  <<: *job_definition
  variables:
    SERVICE: api

unit-test-web:
  <<: *job_definition
  variables:
    SERVICE: web

这样你改一次模板,所有job都跟着更新。我的团队现在就是这么做的——我们通过一个ci-template.yml文件定义了20多种标准拆法,新人来了直接套用,不用再从头设计。

还有一种思路是依赖图自动生成。对于特别复杂的依赖,我会用make或task这类工具,把job的依赖关系写到一个文件里,然后根据文件自动生成Pipeline配置。你想想看,如果你有30个微服务,每个服务有固定的lint→build→test→deploy,手动写根本写不过来。

我的建议:不要试图一次写完所有拆分。先写核心的拆分,然后观察流水线的瓶颈在哪里,再逐步调整。我记得我们团队刚开始只拆了4个job,后来不停迭代到了15个,效果越来越好。

总结一句话:好拆分是改出来的,不是设计出来的。先跑起来再优化,比纸上谈兵强一百倍。
任务拆分决策流程 CI流水线任务 能否独立运行? 能 按阶段拆分 否 按模块拆分 能否并行执行? 能否并行执行? 并行执行 并行执行 串行执行 串行执行 提示:拆分完成后,建议运行一次完整Pipeline,观察每个阶段耗时,再决定是否调整并行策略

避坑指南:刚拆分完的首个Pipeline,建议手动触发一次,别直接推代码。我有一回没留意新配置的分支策略,结果新拆的job根本没跑,白白浪费了两天调试时间。

好了,该讲的核心就这些。拆分这件事,说白了就是规模与效率的平衡。太粗了,反馈慢;太细了,管理成本高。我个人的经验是,先往粗里拆,跑通了再慢慢细化。千万不要追求一步到位,迭代才是正解。