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 特殊任务的处理技巧
有些任务天生比较大,不好拆。比如数据库迁移、数据清洗。我的做法是加一个「手工确认」环节。
举例:
- 先把迁移脚本拆分出来单独做测试
- 后续的集成测试等需要跑迁移的,用一个变量控制:
NEED_MIGRATE: true/false - 最后生产环境的迁移走一个单独的job,需要手工点一下
嗯,那有些跨团队的任务呢?例如集成测试需要两个团队的服务都部署好。这时候我会使用「分阶段触发」机制。第一个阶段,团队A的服务部署;第二阶段,团队B的服务部署。每个阶段跑完后通知另一个团队,再触发下一阶段。
注意:这种跨团队的大任务,不要强行塞到一个CI Pipeline里。我的经验是——各自维护CI,然后在
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个,效果越来越好。
避坑指南:刚拆分完的首个Pipeline,建议手动触发一次,别直接推代码。我有一回没留意新配置的分支策略,结果新拆的job根本没跑,白白浪费了两天调试时间。
好了,该讲的核心就这些。拆分这件事,说白了就是规模与效率的平衡。太粗了,反馈慢;太细了,管理成本高。我个人的经验是,先往粗里拆,跑通了再慢慢细化。千万不要追求一步到位,迭代才是正解。