项目规模一上去,测试套件的运行时间就成了整个团队的噩梦。之前我们把所有的单元测试和集成测试一股脑塞进一个 CI 任务里跑,随着测试用例累积到 3000 多个,CI 阶段的耗时直接飙到了半小时以上。每次提个紧急 Hotfix 想要合并,光是干等 CI 绿就需要怀疑人生,严重阻碍了日常发布。为了干掉这个卡脖子的性能瓶颈,我们决定对 GitLab CI 的管道进行大刀阔斧的重构,利用 parallel:matrix 语法配合测试工具的 Sharding(分片)机制,将测试分发给 4 个 Runner 同时跑,把时间直接缩短了四分之三。
很多人在尝试优化测试时,最容易想到的办法是在单台机器上开启多线程(比如 Jest 的 --maxWorkers)。但在 CI 容器这种 CPU 和内存资源本就非常受限的环境下,强行开多个 worker 往往会导致内存直接爆掉(OOM),或者 CPU 争抢剧烈反而导致运行速度比单线程更慢。真正有效的方式是把任务分摊到不同的物理 Runner 节点上。
现代测试框架(比如 Vitest、Playwright、Jest)大多原生支持 --shard=index/total 参数。只要我们告诉它“总共有 4 个分片,当前是第 2 个”,它就会自动计算哈希并运行该分片对应的测试子集,完全不需要我们手工去写复杂的 shell 脚本来切割文件列表。
以下是我们在 .gitlab-ci.yml 中实现分片并行测试的完整配置:
stages:
- install
- test
- deploy
default:
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store/
install-deps:
stage: install
image: node:18-alpine
script:
- corepack enable
- pnpm config set store-dir .pnpm-store
- pnpm install --frozen-lockfile
artifacts:
paths:
- node_modules/
expire_in: 1 hour
run-tests-parallel:
stage: test
image: node:18-alpine
dependencies:
- install-deps
parallel:
matrix:
# 定义分片矩阵,这里启动 4 个并发 Runner 任务
- TEST_SHARD_INDEX: [1, 2, 3, 4]
TEST_SHARD_TOTAL: [4]
variables:
NODE_ENV: test
# 针对集成测试,为每个分片动态指定隔离的数据库服务
DATABASE_URL: "postgresql://test_user:test_pass@postgres-db-${TEST_SHARD_INDEX}:5432/test_db_${TEST_SHARD_INDEX}"
services:
# 动态为每个 Runner 绑定独立的 PostgreSQL 容器,避免多任务并发写入冲突
- name: postgres:15-alpine
alias: postgres-db-${TEST_SHARD_INDEX}
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off"]
script:
- corepack enable
- pnpm config set store-dir .pnpm-store
# 使用 Vitest 原生的分片能力进行测试分流
- pnpm exec vitest run --shard=${TEST_SHARD_INDEX}/${TEST_SHARD_TOTAL}
artifacts:
when: always
reports:
junit:
- junit-report-${TEST_SHARD_INDEX}.xml
paths:
- coverage/
expire_in: 7 days
在这套配置中,有几个非常关键的实践细节:
- 测试数据库隔离:集成测试最忌讳并发写入同一个数据库实例。我们利用 GitLab CI 的
services别名功能,动态拼接${TEST_SHARD_INDEX}。每个并行任务都在自己独立的 Postgres 容器中读写,天然避免了脏数据污染和死锁大坑。 - 临时关闭 Postgres 刷盘:我们在
services启动命令中传入了-c fsync=off,这虽然在生产环境下极度危险,但在测试环境能极大提高数据库写入性能,毕竟 CI 跑完容器就扔了,根本不需要数据持久化安全。 - 合并测试报告:每个分片运行完毕后会生成对应的 JUnit 报告,GitLab 会自动收集并合并这些报告,在 Merge Request 页面统一展示失败的用例,体验非常丝滑。
重构之后,整个测试阶段在 CI 上的运行时间成功被压到了 7 分钟以内。这对于快速迭代的敏捷团队来说,开发心智负担得到了前所未有的释放。
大家在做 CI/CD 优化时,除了利用矩阵分片,还有没有尝试过基于测试用例的历史耗时做动态平衡负载均衡?对于那种执行时间超长的“害群之马”测试用例,有什么好的调优手段?