适用场景
本文适用于在 Kubernetes 中运行数据库备份、账单汇总、定时同步、报表生成等周期任务,并遇到以下问题的团队:
- 上一次任务尚未结束,下一次任务已经启动,两个 Pod 同时修改同一批数据;
- 控制面短暂不可用或 CronJob 暂停后恢复,任务突然补跑;
- 配置了
concurrencyPolicy: Forbid,仍然发现某些时间点没有执行记录; - Pod 退出后被重试,业务操作被重复提交;
- 历史 Job 和 Pod 太多,排障时难以区分“没调度”和“执行失败”。
CronJob 解决的是“按计划创建 Job”,不是严格的一次性业务调度器。生产环境必须同时处理调度并发、迟到策略、Job 超时和业务幂等。
现象描述
一个每 5 分钟执行一次的数据同步任务,正常耗时约 2 分钟。某天上游接口变慢,单次执行超过 8 分钟,随后出现两类异常:
- 多个 Job 同时处于
Running,重复写入导致唯一键冲突; - 改成
Forbid后,监控又提示某些计划周期没有成功记录。
先查看 CronJob、Job 和事件,不要只盯着当前 Pod:
kubectl -n data get cronjob order-sync -o wide
kubectl -n data get cronjob order-sync -o yaml
kubectl -n data get jobs --sort-by=.metadata.creationTimestamp
kubectl -n data describe cronjob order-sync
kubectl -n data get events --sort-by=.lastTimestamp | tail -n 30
重点关注:
LAST SCHEDULE:控制器最近一次处理的计划时间;.status.active:当前由该 CronJob 管理的活动 Job;- Job 的
Complete、Failed、active状态; - 事件中是否出现错过调度、创建失败、镜像拉取失败或资源不足;
- CronJob 是否被设置为
suspend: true。
常见原因
1. 默认策略允许重叠
concurrencyPolicy 默认是 Allow。只要到达新的计划时间,控制器就可以再创建一个 Job,不会等待前一个 Job 完成。
2. 把 Forbid 误解为排队
Forbid 的含义是:同一个 CronJob 的前一次 Job 仍在运行时,跳过本次计划,而不是把本次任务排队等待。长任务持续跨越多个周期时,出现“漏跑”是预期行为。
3. 迟到任务没有明确边界
控制面故障、CronJob 暂停、资源不足都可能使 Job 未能按时创建。若未设置 startingDeadlineSeconds,恢复后的补偿行为可能不符合业务预期;设置过小也会让轻微延迟直接变成跳过。该值小于 10 秒时尤其危险,因为 CronJob 控制器通常以约 10 秒的周期检查计划。
4. 调度不重复不等于业务只执行一次
即使没有两个活动 Job,Pod 重启、节点故障、Job 重试或客户端超时后的重试,仍可能让同一批业务操作执行多次。因此,支付、发券、库存扣减等动作不能只依赖 Forbid 防重。
排查思路
第一步:确认计划表达式与时区
kubectl -n data get cronjob order-sync \
-o jsonpath='{.spec.schedule}{"\n"}{.spec.timeZone}{"\n"}{.spec.suspend}{"\n"}'
建议显式设置 .spec.timeZone,避免维护人员按本地时间理解,而控制器按另一时区执行。不要在 schedule 中写 CRON_TZ 或 TZ,应使用专门的 timeZone 字段。
第二步:区分未创建、运行失败和仍在执行
kubectl -n data get jobs \
-l app=order-sync \
-o custom-columns='NAME:.metadata.name,START:.status.startTime,ACTIVE:.status.active,SUCCEEDED:.status.succeeded,FAILED:.status.failed'
- 没有对应 Job:优先检查 CronJob 事件、暂停状态、截止时间和控制器日志;
- Job 存在但
Failed:查看 Job 条件和 Pod 退出原因; - 多个 Job 为
ACTIVE=1:检查并发策略是否为Allow,或是否存在多个不同 CronJob; - Job 长期不结束:检查外部调用超时、锁等待和
activeDeadlineSeconds。
第三步:确认活动 Job 的归属
concurrencyPolicy 只约束同一个 CronJob 创建的 Job。两个 CronJob 即使执行相同程序,也可以并发运行。
kubectl -n data get job order-sync-12345678 \
-o jsonpath='{range .metadata.ownerReferences[*]}{.kind}{"/"}{.name}{"\n"}{end}'
如果线上同时存在 order-sync 和 order-sync-v2,需要在业务层使用同一把租约锁或幂等键,而不能指望两个对象共享并发策略。
推荐配置
下面的配置适合“允许偶尔跳过,但禁止同一任务重叠;单次执行最多 20 分钟”的同步任务:
apiVersion: batch/v1
kind: CronJob
metadata:
name: order-sync
namespace: data
spec:
schedule: "*/5 * * * *"
timeZone: "Asia/Shanghai"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
metadata:
labels:
app: order-sync
spec:
activeDeadlineSeconds: 1200
backoffLimit: 2
template:
metadata:
labels:
app: order-sync
spec:
restartPolicy: Never
terminationGracePeriodSeconds: 30
containers:
- name: worker
image: registry.example.com/order-sync:2026.09.18
args: ["python", "-m", "order_sync"]
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
memory: 512Mi
几个字段必须结合业务解释:
Forbid:上一轮未完成就跳过新一轮,适合不能并发但允许少量周期缺失的任务;Replace:终止旧 Job 并启动新 Job,只适合可安全中断、支持断点恢复的任务;startingDeadlineSeconds: 120:计划时间过去 120 秒仍未启动,就放弃该轮;activeDeadlineSeconds: 1200:Job 总运行时间超过 20 分钟后终止,防止永久占用并发槽位;backoffLimit: 2:失败后最多重试两次,但每次重试仍可能重复执行部分业务;- 历史保留数量只影响已结束对象的清理,不影响业务审计数据。
如果业务要求“每个周期都必须完成”,不要简单使用 Forbid。更合适的设计是 CronJob 只负责投递一个带时间窗口的消息,由常驻消费者串行处理并记录进度。
用幂等键兜住重复执行
以每 5 分钟一个同步窗口为例,使用窗口起始时间作为幂等键。数据库先抢占执行权,再处理业务:
CREATE TABLE job_execution (
job_name varchar(64) NOT NULL,
window_start timestamp NOT NULL,
status varchar(16) NOT NULL,
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (job_name, window_start)
);
from datetime import datetime, timezone
def acquire_window(connection, job_name: str, window_start: datetime) -> bool:
"""原子占用一个调度窗口,已存在时返回未获得执行权。"""
with connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO job_execution (job_name, window_start, status)
VALUES (%s, %s, 'running')
ON CONFLICT (job_name, window_start) DO NOTHING
""",
(job_name, window_start.astimezone(timezone.utc)),
)
return cursor.rowcount == 1
幂等记录与核心业务写入应放在可控的事务边界内。若任务包含外部接口调用,还应把请求幂等键传给下游,或使用 outbox 表记录待发送事件,避免“数据库已提交但进程在回执前退出”造成重复副作用。
发布与验证
先暂停创建新 Job,等待当前任务结束或人工确认可以终止:
kubectl -n data patch cronjob order-sync \
--type merge -p '{"spec":{"suspend":true}}'
kubectl -n data get jobs -l app=order-sync
应用配置后恢复调度:
kubectl apply -f order-sync-cronjob.yaml
kubectl -n data patch cronjob order-sync \
--type merge -p '{"spec":{"suspend":false}}'
解除暂停前必须确认 startingDeadlineSeconds。暂停期间的计划会被视为错过的执行;若没有合理截止时间,恢复时可能立即创建补跑 Job。
用手工 Job 验证镜像、权限和业务逻辑,不必等待下一个周期:
kubectl -n data create job \
--from=cronjob/order-sync \
order-sync-manual-$(date +%s)
kubectl -n data logs -f job/order-sync-manual-1234567890
手工 Job 不受该 CronJob 的 concurrencyPolicy 保护,验证时要避开正式执行窗口,或依赖业务幂等机制。
预防措施
- 为 CronJob 监控“距上次成功时间”“活动 Job 数”“失败 Job 数”和执行耗时分位数,而不只监控 Pod 是否存活。
- 让任务输出稳定的
job_name、window_start、execution_id和处理数量,固定日志文案使用中文,禁止记录密钥和完整敏感请求体。 - 为所有外部请求设置连接、读取和总超时,确保 Job 能在
activeDeadlineSeconds前自行清理资源。 - 在压测中故意让执行时间超过调度周期,验证
Allow、Forbid或Replace的行为是否符合预期。 - 对无法容忍漏跑的任务建立业务进度表,按时间窗口补偿,不用遍历历史 Pod 日志猜测缺口。
总结
CronJob 重叠或漏跑通常不是单一参数错误,而是四层边界没有一起设计:concurrencyPolicy 决定同一 CronJob 是否并发,startingDeadlineSeconds 决定迟到多久仍值得执行,Job 的超时和重试决定单次执行如何收敛,业务幂等保证重复尝试不会产生重复副作用。生产配置的目标不是追求“绝不重复”的假象,而是让每次重复、跳过和补偿都有明确且可验证的规则。
Discussion
评论