极狐GitLab

定时流水线总在整点撞车,调度与幂等该怎么配

极狐GitLab
2026年10月27日
24518
分享:

定时流水线总在整点撞车,调度与幂等该怎么配

定义框:定时流水线(Pipeline schedule)是按 cron 表达式周期性触发、与代码变更无关的流水线。它适合"无论有没有人提交代码都必须执行"的任务,比如保持部署环境为最新、跑夜间回归、做定期维护。

定时流水线最容易出问题的地方,不是 cron 写错,而是它天然缺少三个约束:没有明确的负责人、没有幂等语义、没有天然的错峰。项目一多,几十条计划全落在 0 * * * * 上,Runner 队列在整点被撑爆,随后的连锁反应是作业超时、流水线互相抢占、失败重试把队列压得更死。

这篇讲清楚四件事:谁该拥有这条计划、什么时候该触发、怎么把负载摊平、失败了怎么不重复跑。

为什么定时流水线会慢慢失控

定时流水线在极狐GitLab 里的运行方式有三个特性,恰好都是失控的源头。

特性

官方行为

带来的后果

权限跟随所有者

创建计划的用户即所有者,流水线以该用户的权限运行,可以访问受保护环境,并按该用户的访问级别使用 CI/CD 作业令牌

人一离职或被移出项目,计划变 Inactive,任务静默停摆

与代码变更解耦

独立于提交与合并请求运行

代码里加了新的必需检查,定时流水线不会自动跟上

变量只对本次生效

计划上配置的变量仅在该定时流水线运行时可用,其他任何流水线都读不到

同一份 .gitlab-ci.yml 在手动触发时行为完全不同,排查时极易误判

第二条常被忽略:定时流水线不会因为 rules 里新增了条件就自动改变行为。今天能跑的夜间构建,可能因为三个月前某次 rules 调整就一直被跳过,而没人发现——因为没人盯着"应该跑但没跑"这件事。

怎么把调度配清楚

第一步:把所有权收归团队账号,而不是个人

先决条件是创建者拥有该项目的开发者、维护者或所有者角色,且主邮箱已验证;如果计划指向受保护分支,还必须对该分支有合并权限。这几条决定了你不能随便找个实习号建计划。

实践做法是建一个服务账号(或团队公共账号)专门持有计划,人员变动时只需要交接账号,不需要在几十个项目里逐条"取得所有权"。已经变成 Inactive 的计划,维护者或所有者可以在计划列表里点取得所有权重新激活。

想盘点自己名下有多少条计划,路径在个人资料里:头像 → 编辑个人资料 → 帐户 → 滚动到"您拥有的定时流水线"。这个页面是排查"计划莫名停摆"的第一入口。

第二步:用 rules 把定时流水线和其他触发源切开

如果一份配置既服务于推送触发,也服务于定时触发,务必显式区分,否则夜间任务会和日常构建抢同一批 Runner。

stages: [build, test, nightly]

default:
  interruptible: true

# 日常构建:只在推送和合并请求时跑
build:
  stage: build
  script:
    - ./gradlew assemble
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_PIPELINE_SOURCE == "push"
    - when: never

# 夜间回归:只在定时流水线里跑
nightly-regression:
  stage: nightly
  interruptible: false
  script:
    - ./gradlew regressionSuite
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - when: never

interruptible: false 用在夜间长任务上是有意的:它保证新提交触发的自动取消不会把跑到一半的回归任务干掉。代价是这条流水线运行期间无法被自动取消,所以只给真正不能中断的作业加,别整条流水线都加。

第三步:能写进 spec:inputs 的,别塞进变量

计划上可以挂任意数量的 CI/CD 变量,但官方明确建议用 inputs 替代变量来配置流水线,理由是安全性和灵活性更好。一个计划最多可以有 20 个输入。

spec:
  inputs:
    target_env:
      default: staging
    suite:
      default: smoke
---
deploy-check:
  script:
    - echo "checking $[[ inputs.target_env ]] with $[[ inputs.suite ]]"

$[[ inputs.* ]] 在流水线创建、拉取配置时就被插值替换,比运行时变量更早、更可验证,也不会像变量那样被后续作业继承导致歧义。

第四步:把 cron 摊平

官方在故障排查里给了明确的诊断手段:在实例上导出所有活跃计划的 cron 分布,看有没有大量计划挤在同一个模式上。

outfile=/tmp/gitlab_ci_schedules.tsv
sudo gitlab-psql --command "
COPY (SELECT
    ci_pipeline_schedules.cron,
    ci_pipeline_schedules.cron_timezone,
    namespaces.path AS group,
    projects.path   AS project,
    users.email
FROM ci_pipeline_schedules
JOIN projects ON projects.id = ci_pipeline_schedules.project_id
JOIN namespaces ON namespaces.id = projects.namespace_id
JOIN users    ON users.id    = ci_pipeline_schedules.owner_id
WHERE ci_pipeline_schedules.active = 't'
) TO '$outfile' CSV HEADER DELIMITER E'\t' ;"
sort "$outfile" | uniq -c | sort -n

输出里如果 0 * * * * 出现几十次,就是典型的整点拥堵。官方建议是改成错开的形式,例如 5 * * * *、15 * * * *、25 * * * *,大仓库尤其要注意。另外 cron 的运行频率不能高于实例设置的最大定时流水线频率,超出的部分会被直接忽略——这也是"配了但不生效"的常见原因。

第五步:处理时区,别让 22 点变成 6 点

通过 API 创建计划时,cron_timezone 的默认值是 UTC。这意味着你在页面上填 0 22 * * * 的直觉(本地时间晚上 10 点),落到 UTC 上可能是第二天早上 6 点。

curl --request POST \
  --header "PRIVATE-TOKEN: <your_token>" \
  --form "description=nightly-regression" \
  --form "ref=main" \
  --form "cron=17 18 * * *" \
  --form "cron_timezone=Asia/Shanghai" \
  --form "active=true" \
  "https://gitlab.example.com/api/v4/projects/<project_id>/pipeline_schedules"

cron_timezone 接受 ActiveSupport::TimeZone 或 TZInfo::Timezone 的写法,Asia/Shanghai 两种都认。新计划一律显式写时区,不要依赖默认值。

几个真实的坑

坑一:计划变 Inactive 没有任何通知。 所有者被阻止或移出项目时,计划状态变为 Inactive,流水线就此停摆。定期在监控里对"过去 N 天该跑却零运行"的计划做告警,比事后追查省事得多。

坑二:variables 里写的值在调试时读不到。 这些变量只在定时流水线里存在,你在本地或在手动触发的流水线里 echo 出来都是空的。误以为是配置没生效,其实配置是对的。

坑三:手动运行用的是你的权限,不是所有者的权限。 计划页面上的运行按钮每分钟最多点一次,它按操作人身份执行。这意味着"我手动跑没问题、定时跑就失败"这类现象,根因几乎总是权限差异,而不是 cron。

坑四:项目或群组被标记删除时,定时流水线立即停止运行。 归档项目同理需要留意,迁移或清理前先导出计划清单。

常见问题

Q1:定时流水线能用受保护环境的部署审批吗?

能,但取决于所有者权限。计划以所有者身份运行,只要所有者有权限访问该受保护环境,审批规则照常生效。这也是为什么服务账号必须给足环境权限,否则夜间部署会卡在审批门口无人处理。

Q2:计划数量有上限吗?

有,受项目流水线计划数量上限约束。到达上限时需要先删除未使用的计划才能新增。清理时优先按 cron 和 owner 两个维度找重复项。

Q3:怎么确认某条计划真的按预期跑了?

两个位置:计划列表里能看到上次运行时间;更可靠的是在流水线列表按来源 schedule 过滤,看是否有缺失的时间点。只看"上次运行"会掩盖"中间漏跑"。

下一步

如果你现在的定时流水线还散落在各个项目、由不同的人建、cron 全挤在整点,建议先做两件事:跑一次上面那段 gitlab-psql 导出,看清分布;再把所有计划的所有权收归到一个服务账号,并统一显式设置 cron_timezone。

想直接上手验证调度行为?可以在测试项目里用 API 建一条 cron=*/5 * * * * 的计划,配合 CI_PIPELINE_SOURCE == "schedule" 的 rules 观察触发边界。极狐GitLab 的定时流水线属于基础版即可使用的功能,私有化部署和 JihuLab.com 都支持。