GitLab CI/CD 使用 resource_group 限制并发执行

背景

在 CI/CD 流水线中,某些 job 会对共享资源(如对象存储、数据库、文件系统)执行写操作。如果多个 pipeline 实例同时触发此类 job,可能导致:

  • 数据竞争:两个 job 同时写入同一路径,产生不一致或损坏的数据
  • 事务冲突:如 ossutil sync --delete 并发执行时,后完成的 job 可能覆盖先完成的 job 的变更

实际场景

我们有一个制品同步项目,包含两个 job:

  • sync-rpm:同步 RPM 包到 OSS 的 RPM 仓库目录
  • sync-deb:同步 DEB 包到 OSS 的 DEB 仓库目录

需求如下:

场景 是否允许并发
同一 pipeline 的 sync-rpmsync-deb :white_check_mark: 允许(写入不同 OSS 目录,互不影响)
不同 pipeline 的 sync-rpm :cross_mark: 禁止(写入同一 OSS 目录,有并发风险)
不同 pipeline 的 sync-deb :cross_mark: 禁止(写入同一 OSS 目录,有并发风险)

解决方案:resource_group

GitLab 提供了 resource_group 关键字,用于限制同一时间只有一个 job 在运行。当多个 job 属于同一个 resource_group 时,GitLab Runner 会串行执行它们,后续的 job 显示 “waiting for resource” 直到锁被释放。

官方文档:Resource group

使用方法

.gitlab-ci.yml 的 job 定义中添加 resource_group 字段:

sync-rpm:
  stage: sync
  resource_group: sync-rpm-lock   # 👈 添加 resource_group
  image: ...
  script: ...

sync-deb:
  stage: sync
  resource_group: sync-deb-lock   # 👈 添加 resource_group
  image: ...
  script: ...

关键设计:使用不同的 resource_group 名称

Job resource_group 效果
sync-rpm sync-rpm-lock 跨 pipeline 串行 RPM 同步
sync-deb sync-deb-lock 跨 pipeline 串行 DEB 同步

为什么不用同一个 resource_group

如果把两者都设为 sync-lock,同一 pipeline 内的 sync-rpmsync-deb 也会串行执行,但这两个 job 写入的 OSS 目录不同,没有并发冲突,串行会白白增加总耗时。

使用不同resource_group 则实现了细粒度的控制:

Pipeline #1:
  sync-rpm (sync-rpm-lock) ──────────────┐
  sync-deb (sync-deb-lock) ──────────────┤ 并行 ✅
                                         │
Pipeline #2:                             │
  sync-rpm (sync-rpm-lock) ── 等待 ───────┘ 串行 ⏳
  sync-deb (sync-deb-lock) ────────────── 并行 ✅(deb lock 空闲)

锁的粒度选择

策略 配置 适用场景
粗粒度锁 所有 job 共用同一个 resource_group 所有 job 写入同一资源,或简单场景不太关心效率
细粒度锁 按资源路径拆分不同 resource_group 不同 job 操作不同资源,希望最大化并行度
动态锁 使用变量作为 resource_group 名 按分支/环境/参数动态隔离

动态 resource_group 示例

如果需要按环境或参数进一步细化锁的粒度,可以使用变量:

sync-rpm:
  resource_group: sync-rpm-$ENVIRONMENT-lock
  # 开发环境和生产环境各自独立锁,互不阻塞

其他并发控制方式

interruptible — 取消旧 pipeline

如果希望新 pipeline 触发时直接取消正在运行的旧 job(而非等待):

sync-rpm:
  interruptible: true
  resource_group: sync-rpm-lock

当新 pipeline 触发且 resource 被旧 job 占用时,GitLab 会尝试取消旧 job,立即启动新 job。适合"最新的构建结果才重要"的场景。

不同 Stage 串行

如果只关心同一 pipeline 内的顺序,直接使用 stage 即可:

stages:
  - sync-rpm
  - sync-deb

但这不解决跨 pipeline 并发问题,因此仍需配合 resource_group 使用。

对比总结

机制 同一 pipeline 内 跨 pipeline 行为
不同 stage 串行 :cross_mark: 不控制 按 stage 顺序执行
resource_group 按需控制 :white_check_mark: 串行 同组 job 排队
resource_group + interruptible 按需控制 :white_check_mark: 取消旧→新 只保留最新执行

我们的最终配置

stages:
  - sync

variables:
  PROXY_URL: https://gh-proxy.org
  BUCKET_NAME: mirrors-xuxiaowei
  LANG: C.UTF-8

sync-rpm:
  stage: sync
  resource_group: sync-rpm-lock   # 👈 RPM 同步跨 pipeline 排队
  image: $SYNC_RPM_IMAGE
  # ... (其他配置不变)

sync-deb:
  stage: sync
  resource_group: sync-deb-lock   # 👈 DEB 同步跨 pipeline 排队
  image: $SYNC_DEB_IMAGE
  # ... (其他配置不变)

效果:

  • 同一 pipeline 中 sync-rpmsync-deb 并行运行(操作不同 OSS 目录)
  • 不同 pipeline 的同类型 job 排队执行(保护同一 OSS 目录不被并发写入)
  • 不同 pipeline 的不同类型 job 互不影响(各自独立的锁)

总结

resource_group 是 GitLab CI/CD 中控制 job 并发执行的简洁有效手段。关键设计要点:

  1. 按资源粒度拆分 resource_group,而非所有 job 共用一个锁,以实现最大并行度
  2. 配合 interruptible: true 可在新 pipeline 触发时取消旧的等待/运行中的 job
  3. 可以使用变量动态构造 resource_group 名称,实现按环境/分支的隔离

本文对应的配置修改 commit:b4fe3ec