场景:CronJob 定时备份凌晨没执行 + Job 重试耗尽(K8s v1.25) 路径:kubectl describe cronjob → kubectl get job -o yaml → kubectl get pods

上篇讲了 DaemonSet 优雅终止,核心是通过 PDB 和 preStop hook 控制 Pod 的销毁节奏。而这篇的 Job/CronJob 正好相反——它管的是 Pod 的创建节奏。

定时任务为什么没跑?——CronJob 时区/并发/退避三连坑

【坐标】→ Job 没执行 + Pod 反复失败

备份用的 CronJob(定时任务资源,cronjob-controller 负责按 schedule 创建 Job)配置每天早上 8:00 执行,但连续三天发现备份根本没在预期时间触发。

kubectl describe cronjob 一看:

CronJob 时区未配置

Last Schedule Time 显示 08:00 UTC——按调度表达式 0 8 * * * 确实在 08:00 触发了,但那是 UTC 08:00(北京时间 16:00)。期望的北京时间 08:00(UTC 00:00)反而没反应。差 8 小时。

根因:CronJob 的 spec.timeZone 没配置,cronjob-controller 使用 kube-controller-manager 所在节点的系统时区(UTC)。cron 表达式 0 8 * * * 在 UTC 时区下就是 UTC 08:00。

同一个集群的 data 命名空间里,另一个数据清洗 Job 踩了不同的问题:

$ kubectl get jobs
NAME                  COMPLETIONS   DURATION   AGE
data-cleanup-job      0/1           15m        15m
data-cleanup-job-1    0/1           12m        12m
data-cleanup-job-2    0/1           10m        10m

Job 名字后面有自增序号——说明了和第一个问题不同,这个 Job 不是"没跑",而是跑了三次还在跑。每次启动一个新 Pod,处理到一半就退出了,然后又启动一个新的。

【分层】→ 3 层排查,从 Job 到控制器

K8s 排查不能跳层——标准流程是 Pod 层 → CRI 层 → Node 层 → 集群层。Job/CronJob 的根因在控制器行为,CRI(容器运行时)层和 Node 层在本场景中通过不影响结论(容器本身正常启动,只是退出码非 0),所以走 3 层:Job/CronJob 自身层 → Pod 层 → 控制器层。

第 1 层:Job/CronJob 资源层

$ kubectl describe cronjob backup-job
...
Schedule:                   0 8 * * *
Time Zone:                  <none>
Last Schedule Time:         Tue, 06 Jul 2026 00:00:00 UTC
Events:
  Normal  Scheduled  8h  cronjob-controller  Job created

$ kubectl get cronjob backup-job -o yaml | grep -A 2 timeZone
  # timeZone 字段根本没配

CronJob 的 spec.timeZone 字段从 K8s v1.25 进入 stable。不配置时,cronjob-controller 使用 kube-controller-manager 所在节点的时区——生产环境往往部署在 UTC 时区的节点上。

$ kubectl describe job data-cleanup-job
...
Status:
  Conditions:
    Type       Status  LastProbeTime  Reason
    BackoffLimitExceeded  True             -            Job has reached the specified backoff limit
  Failed:      6
  Start Time:  Tue, 06 Jul 2026 01:00:00 UTC
Events:
  Warning  BackoffLimitExceeded  job-controller  Job has reached the specified backoff limit

backoffLimit 默认 6——Job 的 Pod 失败了 6 次,耗尽退避限额,job-controller 不再重启。

第 2 层:Pod 层

$ kubectl get pods --selector=job-name=data-cleanup-job
NAME                       READY   STATUS             RESTARTS   AGE
data-cleanup-job-m9qpn     0/1     CrashLoopBackOff   3          5m
data-cleanup-job-x2rvt     0/1     Error              0          8m
data-cleanup-job-h3kws     0/1     Error              0          10m

Job Pod 全部 Error

Pod 全部是 Error 或 CrashLoopBackOff,说明应用本身有问题。但问题在于——Job 还在不断创建新 Pod,即使前面的 Pod 已经明显失败。

$ kubectl logs data-cleanup-job-h3kws
...
Error: timeout connecting to database

日志显示数据库连接超时。但这个超时是瞬时的,下一次 Pod 启动时可能恢复了——所以部分 Pod 成功运行了一会儿才失败。每次重试间隔很短,数据库连接池还没完全恢复,新的 Pod 又杀进来了。

第 3 层:控制器层

两个问题的根因都在控制器行为:

CronJob 时区判定:cronjob-controller 计算 lastScheduleTime 时,直接把 cron 表达式解释为 UTC。如果用户期望的是北京时间 08:00(0 8 * * *),实际触发是在 UTC 08:00(北京时间 16:00)。

CronJob 执行流程 — 调度/并发/退避

并发策略默认为 Allowspec.concurrencyPolicy 默认为 Allow,意味着前一个 Job 还在运行时,新的一次调度周期到达,cronjob-controller 会再创建一个新 Job。如果 Job 执行时间 > 调度间隔,Pod 会层层叠加。上面 data-cleanup 场景里,Pod 的执行时间大约 8-10 分钟,而调度间隔也是 10 分钟——刚好卡在重叠边缘。

并发策略状态机:
  Allow   → 总是创建新 Job(默认)
  Forbid  → 前一个 Job 没完成就跳过本次
  Replace → 杀死前一个 Job,创建新 Job

Backoff 指数退避:job-controller 在 Pod 失败后按照指数退避重试,序列为 10s → 20s → 40s → 80s → 160s → 320s(上限 360s)。backoffLimit 默认 6——计的是失败 Pod 数,达到上限后 job-controller 标记 BackoffLimitExceeded,不再创建新 Pod。

【路径】→ 🔍 3 个命令定位 Job/CronJob 问题

核心排查命令

【定位】→ 三大误判

维度 ❌ 误判 ✅ 真相
时区 "CronJob 按我设的 08:00 跑" CronJob 默认 UTC。kube-controller-manager 在 UTC 节点上,spec.timeZone 不配永远按 UTC 跑。LastScheduleTime 显示的就是 UTC 时间
并发 "Job 没执行完是业务代码出错了" concurrencyPolicy=Allow(默认)让调度周期重叠时创建新 Job。Job 执行时间 > 调度间隔 → Pod 叠加 → 资源争夺 → 更频繁的失败
Backoff "Job 失败了 6 次就放弃,指数退避是 10/20/40/80/160/320s" backoffLimit 默认 6,退避序列 10s → 20s → 40s → 80s → 160s → 320s(上限 360s)。backoffLimit 计的是失败 Pod 数,不是重试次数——Pod 创建了但没等退避结束就删除,不算

三大误判对比

时区场景的真相

不是 K8s 不处理时区——K8s v1.25 起 spec.timeZone 已经是 stable。问题是:CronJob 的 controller 在计算下次调度时间时,使用 kube-controller-manager 的系统时区。很多生产集群把控制面部署在 UTC 时区的节点上。如果你的 CronJob 没配 timeZone: "Asia/Shanghai",cron 表达式 0 8 * * * 在控制面看来就是 UTC 08:00。

并发场景的真相

并发策略不是用来"提升性能"的——它是用来防止重叠的。默认 Allow 是个危险默认值。对于执行时间和调度间隔接近的 Job,Allow 等于把每一次调度都变成一个独立副本,它们在竞争同一份资源。

# 正确的并发策略选择
concurrencyPolicy: Forbid     # 定时备份/数据清洗(执行时间不确定,不能重叠)
concurrencyPolicy: Allow      # 事件处理(每个调度周期独立,可以并行)
concurrencyPolicy: Replace    # 配置同步(只需最新一次,旧的可以杀掉)

Backoff 场景的真相

'Job 失败了 6 次就放弃'——这个说法只对了一半。backoffLimit 默认 6 次是指失败 Pod 数量的上限。但每次失败后的退避时间不是线性的:

退避序列:10s → 20s → 40s → 80s → 160s → 320s(上限360s)
                    ↑ 每次翻倍,但不超过 360s

第 1 次失败 → 等 10s 重试 第 2 次失败 → 等 20s 重试 ... 第 6 次失败 → 等 320s 后 job-controller 检查 backoffLimit 耗尽 → 不再重试

activeDeadlineSeconds 也设置时,Job 的超时是"谁先到谁说了算"——backoff 还没耗尽但超时到了,Job 直接终止。反之亦然。

【标点】→ 修复 + Check-list

CronJob 时区修复

修复后的 CronJob 配置

apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup-job
spec:
  schedule: "0 8 * * *"
  timeZone: "Asia/Shanghai"    # ✅ 显式指定时区
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: backup-tool:latest
          restartPolicy: OnFailure

并发策略修复

spec:
  schedule: "*/10 * * * *"
  concurrencyPolicy: "Forbid"   # ✅ 防止调度重叠
  startingDeadlineSeconds: 120  # 错过调度窗口后 2 分钟内还可启动

Backoff 设置修复

spec:
  jobTemplate:
    spec:
      backoffLimit: 3           # 最多重试 3 次(不是默认 6)
      activeDeadlineSeconds: 300 # 5 分钟超时

Check-list

检查项 命令 判断标准
□ 时区配置 kubectl describe cronjob <name> \| grep "Time Zone" 应有 Asia/Shanghai 或目标时区
□ 并发策略 kubectl get cronjob <name> -o yaml \| grep concurrencyPolicy 非 Allow 以外的值需要确认是否合理
□ Backoff 限制 kubectl get job <name> -o yaml \| grep -A 5 backoffLimit 根据业务容忍度调整(默认 6 可能太多)
□ Job 状态 kubectl get job <name> -o wide COMPLETIONS 应与预期一致
□ Job Events kubectl describe job <name> BackoffLimitExceeded / DeadlineExceeded

排查命令速记

下次遇到 CronJob 没跑 / Job 反复失败──按这个顺序排查:

1. kubectl describe cronjob <name>     # 时区 + Events
2. kubectl get job <name> -o yaml       # backoff + conditions
3. kubectl get pods --selector=job-name= # 确认 Pod 状态
4. kubectl describe job <name>           # Backoff 耗尽原因

K8s 不是没有告诉你问题在哪——Events 已经说了,只是你没看。


下篇我们聊 HPA 扩缩容不灵敏——metrics 采集延迟和目标阈值设错导致的异常行为。


📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」

附:完整命令清单

# 查看 CronJob 状态和时区
kubectl describe cronjob <name>

# 查看 CronJob YAML(确认 timeZone/concurrencyPolicy)
kubectl get cronjob <name> -o yaml

# 查看 Job 状态和退避限制
kubectl get job <name> -o yaml

# 查看 Job 创建的所有 Pod
kubectl get pods --selector=job-name=<name>

# 查看 CronJob 历史执行记录
kubectl get jobs --watch

# 查看退避原因
kubectl describe job <name>

# 暂停 CronJob(排查期)
kubectl patch cronjob <name> -p '{"spec":{"suspend":true}}'

# 手动触发一次 Job
kubectl create job --from=cronjob/<name> manual-job