CronJob 就是一份 Job 模板加一条 cron 表达式。表达式每次匹配,控制器就新建一个 Job,由它把 Pod 跑到完成再停掉。两轮之间没有任何常驻进程,这正是它吸引人的地方:批处理任务直接继承集群的重试、资源与调度机制,不需要谁养一个守护进程来触发它。
代价是时间表只被“近似”兑现。Kubernetes 承诺每个执行时刻“大约”创建一个 Job,这句措辞把两个方向的偏差都包了进去——同一时刻冒出两个 Job,或者一个都没有。凡是被排进 CronJob 的活都必须能安全跑两遍,因为你真正拿到的保证是至少一次,而不是恰好一次。
一张时间表催生一个 Job
schedule 用熟悉的五个字段——分、时、日、月、星期——支持 Vixie cron 的区间与步长写法,所以 */10 * * * * 每十分钟触发一次,*/15 9-17 * * mon-fri 只落在工作时段内。? 与 * 等价,@daily、@hourly 这类宏会展开成对应的表达式。没有设置 timeZone 的 CronJob 跟随控制器管理器所在时区的本地时钟,控制面一换地方,任务的执行时刻就跟着变。少写一个字段是校验错误,这也让它成为 CronJob 少数会大声失败的配置错误之一。
每一次匹配都会生成一个以 CronJob 命名的独立 Job,这也是 CronJob 名字被限制在 52 个字符以内的原因:控制器还要再拼上十一个字符,而 Job 名字的上限是 63。CronJob 自己从不执行任务,它只创建 Job,Pod 归 Job 所有。从 Kubernetes v1.32 起,生成的 Job 会带上 batch.kubernetes.io/cronjob-scheduled-timestamp 注解,记录它本该在什么时候跑——控制器重启之后,这是区分“迟到的运行”和“准时的运行”的唯一依据。
并发是一道策略,不是猜
concurrencyPolicy 决定上一轮 Job 还在跑时又到点该怎么办。默认的 Allow 直接再起一份;Forbid 跳过这一次;Replace 取消正在跑的那个 Job 换新的。这条策略只约束同一个 CronJob 生成的 Job,两个不同的 CronJob 之间互不阻塞。
危险的是 Replace:如果 Job 稳定地跑得比间隔慢,每个到点时刻都会杀掉那个眼看就要跑完的运行,活永远做不完。大家说“别重叠”时想要的通常是 Forbid,但有两个附带条件——被跳过的那些轮次就是丢了;而且当前 Job 跑完之后,startingDeadlineSeconds 仍会被重新评估,一个迟到大半天的运行可能照样起来。
漏跑次数有一百的额度
startingDeadlineSeconds 划出“迟到多久还值得补跑”的边界,过了这条线的那一轮直接跳过并记为失败;而跳过某一次并不会让时间表停下,下一个匹配时刻照样会触发。不设这个字段时,漏跑本身没有截止时间,但控制器还有一道硬上限:它统计自上次计划时间以来漏掉了多少个时刻,超过一百个就一个都不补,并打出 too many missed start times. Set or decrease .spec.startingDeadlineSeconds or check clock skew。
官方例子把这笔账算得很清楚。一个每分钟触发的 CronJob,控制器从 08:29 停到 10:21,中间漏掉 112 个时刻,于是恢复后什么都不会补跑。改成 startingDeadlineSeconds: 200,控制器只统计最近 200 秒内漏掉的时刻——三个——Job 就起来了。同一个字段还藏着一个更细的坑:小于十秒的值不可靠,因为控制器每十秒才检查一次。suspend: true 能让 CronJob 停摆而不影响已在运行的 Job,但被挂起的执行时刻照样计入漏跑,所以没有截止时间就恢复挂起,积压会一次性全放出来。
坏掉的样子
最经典的是 Job 和自己重叠。一份跑二十五分钟的报告放在十分钟的间隔上,用默认的 Allow,峰值会同时有三份在读同一份输入、写同一份输出。它从不以报错的形式出现:你看到的是重复行、翻倍的 API 开销,或者三份里有一份报锁超时。要么换成 Forbid 并接受被跳过的轮次,要么把 Job 做成幂等,再给它加一把租约,让迟来的那份提前退出。
第二种是静默的补跑丢失。控制面停机时间超过一百个调度间隔,积压就会在恢复时被整体丢弃,唯一的痕迹是控制器管理器日志里的一行。对每分钟一次的轮询来说这是对的,对每晚一次的备份来说就是错的——这正是 startingDeadlineSeconds 的用途:把它设成真实的容忍度(一天两次的备份就是八小时),既限定了运行可以有多陈旧,也让“一百个间隔”这道上限永远够不着。
快速验证
把时间表、并发策略和最近一次触发时间放在一起看:
kubectl get cronjob nightly-report -o jsonpath='{.spec.schedule} {.spec.concurrencyPolicy} {.spec.startingDeadlineSeconds}{"\n"}'
kubectl get cronjob nightly-report -o jsonpath='{.status.lastScheduleTime}{"\n"}'
kubectl get jobs --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,START:.status.startTime,SUCCEEDED:.status.succeeded
kubectl get events --field-selector involvedObject.kind=CronJob --sort-by=.lastTimestamp | tail -5
lastScheduleTime 应该落在距当前一个调度间隔之内;差出几个小时,说明控制器停过,或者表达式从未匹配。START 时间互相重叠而 SUCCEEDED 仍为空,是 Allow 配上跑不完的 Job 的典型特征;事件里出现 too many missed start times,就直接指向漏跑规则。
来源
- Kubernetes CronJob,调度语法、并发策略、漏跑一百次的规则与历史上限,已核验 2026-09-16。
- CronJob v1 API 参考,CronJob spec 的字段级定义,已核验 2026-09-16。