Kubernetes 基础体系 · 第 19/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Job 与 CronJob:并行、重试、补跑、时区、幂等和清理
Job 和 CronJob 都用于运行“最终应当完成”的任务,但它们解决的是两个不同层次的问题:
- Job 负责让一组 Pod 完成指定次数的任务,并处理失败重试。
- CronJob 负责按照时间表创建 Job;它本身不执行任务,也不替代 Job 的重试和完成语义。
因此,一个定时任务的完整链路是:
CronJob
└── 按 schedule 创建 Job
└── 创建 Pod
└── 启动容器
└── 成功或失败
如果 Pod 被驱逐、节点故障、容器重启、Job 控制器重建,任务都可能再次执行。Kubernetes 能保证的是控制器层面的状态收敛,不是业务代码天然的“只执行一次”。
一、先区分 Pod、Job 和 CronJob
Pod 是执行载体
Pod 的 status.phase 描述 Pod 的总体阶段:
PendingRunningSucceededFailedUnknown
但 phase 不是任务结果的全部信息。例如:
- Pod 处于
Running,说明任务还没有结束; - Pod 处于
Succeeded,通常表示所有容器都以退出码0结束; - Pod 处于
Failed,表示至少有一个容器失败,且 Pod 已经结束; - Pod 被驱逐时,可能表现为失败或终止相关 Condition,不能只看应用日志判断原因。
restartPolicy 决定容器在同一个 Pod 内是否重启:
restartPolicy: Never
表示容器失败后不在原 Pod 内重启,由 Job 控制器决定是否创建新的 Pod。
restartPolicy: OnFailure
表示容器失败时,kubelet 尝试在同一个 Pod 内重启容器。它通常更快,但会把“同一个 Pod 内的容器重启”和“Job 创建新 Pod”混在一起,不利于观察每次尝试。
Job 的 Pod 模板只允许:
restartPolicy: Never
或:
restartPolicy: OnFailure
不能使用 Always,因为 Job 需要知道任务何时结束。
Job 是一次性工作负载
Job 关注的是:
是否已经达到成功完成条件?
它可以创建一个或多个 Pod,并根据 Pod 结果继续创建替代 Pod。Job 完成后,Job 对象和已结束的 Pod 默认仍然保留,除非由 TTL 或其他清理机制删除。
CronJob 是 Job 创建器
CronJob 关注的是:
什么时候应该创建一个新的 Job?
CronJob 不会把同一个 Job“重置”后再次运行。每一个调度时间通常对应一个独立的 Job 对象:
CronJob
├── report-29123450
├── report-29123510
└── report-29123570
因此:
- Job 的
backoffLimit控制一次 Job 内部的失败重试; - CronJob 的
schedule控制未来是否创建新的 Job; - “补跑”可能是 Job 内重试,也可能是 CronJob 对错过时间的补创建,二者不是同一个机制。
二、Job 的完成条件:completions、parallelism 和 completionMode
1. 单次任务
最简单的 Job 不设置 completions:
apiVersion: batch/v1
kind: Job
metadata:
name: one-shot
spec:
template:
spec:
restartPolicy: Never
containers:
- name: task
image: busybox:1.36
command: ["sh", "-c", "echo start; sleep 5; echo done"]
此时:
completions默认是1;parallelism默认是1;- 一个 Pod 成功后,Job 达到完成条件。
可以用以下命令观察:
kubectl apply -f one-shot.yaml
kubectl get job one-shot
kubectl get pods -l job-name=one-shot
kubectl describe job one-shot
典型结果:
NAME COMPLETIONS DURATION AGE
one-shot 1/1 6s 20s
1/1 的含义是:需要成功 1 次,已经成功 1 次。它不是 Pod 数量,也不是尝试次数。
2. 多次成功完成
如果设置:
spec:
completions: 10
Job 需要累计 10 次成功完成。默认 parallelism 仍然是 1,所以通常会串行执行 10 个 Pod。
如果设置:
spec:
completions: 10
parallelism: 3
Job 会尽量同时运行 3 个活动 Pod,直到累计有 10 个成功 Pod。
可以把非 Indexed Job 的基本目标写成:
其中:
- 是成功完成次数;
- 是
.spec.completions。
并发活动 Pod 数量通常受以下条件约束:
其中:
- 是当前活动 Pod 数量;
- 是
.spec.parallelism。
当剩余任务数量不足 parallelism 时,控制器不会为了满足并发数而继续创建多余 Pod。因此更完整地表示为:
这描述的是目标,不应被理解为严格的实时不变量。控制器通过异步监听和创建 Pod 工作,短时间内可能存在状态滞后,节点故障或控制器重启也可能导致实际 Pod 数量与期望值暂时不同。
3. parallelism 不是任务分片
下面的配置:
spec:
completions: 10
parallelism: 3
并不自动把数据分成 10 份,也不向容器传入“当前是第几份”。它只表示:
最终需要 10 个成功 Pod,并尽量同时运行 3 个 Pod。
如果每个 Pod 都执行完全相同的任务,结果可能是同一份数据被处理 10 次。要处理不同分片,应使用 Indexed Job 或由外部系统分配任务。
三、Indexed Job:并行分片和每个索引的完成状态
需要让每个 Pod 对应一个确定的任务分片时,可以使用:
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-example
spec:
completions: 4
parallelism: 2
completionMode: Indexed
backoffLimit: 6
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command:
- sh
- -c
- |
echo "processing index=${JOB_COMPLETION_INDEX}"
sleep 2
test "${JOB_COMPLETION_INDEX}" != "2"
在支持 Indexed Job 的集群中,Kubernetes 会为索引 0 到 3 分配任务。容器可以通过 JOB_COMPLETION_INDEX 环境变量获得自己的索引。
这段示例中索引 2 故意失败,因此 Job 不会顺利完成。实际业务中,索引通常用于计算分片范围,例如:
总记录数 = 1,000,000
分片数 = 10
索引 = 3
处理区间:
start = 3 × 100,000
end = 4 × 100,000 - 1
此时任务身份应当由多个字段共同确定:
业务批次 + 分片索引
例如:
2025-03-08T00:00Z + shard-3
不能只用 Pod 名称作为业务任务身份,因为失败重试会产生新的 Pod 名称。
每索引重试
在支持每索引重试字段的较新 Kubernetes 版本中,可以使用:
spec:
completionMode: Indexed
completions: 10
parallelism: 4
backoffLimitPerIndex: 2
maxFailedIndexes: 3
语义是:
- 每个索引最多允许指定次数的失败;
- 某个索引成功后,不再需要为该索引创建任务;
- 失败索引数量超过
maxFailedIndexes时,整个 Job 失败。
这些字段依赖集群版本和控制器能力。部署前应检查:
kubectl explain job.spec.backoffLimitPerIndex
kubectl explain job.spec.maxFailedIndexes
如果服务器不认识字段,不能仅因为客户端能生成 YAML 就认为集群支持它。
四、重试:容器重启、Pod 重建和 Job 失败不是一回事
Job 的“重试”至少包含三层含义。
第一层:容器在同一个 Pod 内重启
当 restartPolicy: OnFailure 时,kubelet 可能在原 Pod 内重启失败容器:
Pod A
└── Container attempt 1: exit 1
└── Container attempt 2: exit 1
└── Container attempt 3: exit 0
这种情况下,Pod 最终可能直接进入 Succeeded,Job 不一定创建新的 Pod。
容器日志若需要查看上一次实例,应使用:
kubectl logs pod-name -c task --previous
第二层:Job 创建新的 Pod
当 Pod 以失败状态结束,Job 控制器可以创建替代 Pod:
Pod A: Failed
└── Job 创建 Pod B
Pod B: Failed
└── Job 创建 Pod C
Pod C: Succeeded
使用:
restartPolicy: Never
时,每个 Pod 通常就是一个可见的尝试,便于诊断和审计。
第三层:Job 达到失败阈值
backoffLimit 控制 Job 允许的失败重试数量,默认值是 6。当失败次数达到阈值,Job 会被标记为失败,不再继续创建正常重试 Pod。
spec:
backoffLimit: 3
表示失败达到相应阈值后,Job 进入失败状态。这里的“失败次数”不能简单等同于 Pod 数量,因为 OnFailure 下同一个 Pod 内也可能发生容器重启,Job 控制器还会结合 Pod 状态和容器状态进行失败判断。
重试间隔通常采用逐步增加的退避,常见实现表现为:
约 10 秒
约 20 秒
约 40 秒
...
上限约 6 分钟
这是控制器实现中的退避行为,不应把它当作业务级精确计时器。节点资源、API Server 延迟、调度延迟和镜像拉取时间都会增加实际间隔。
失败重试的完整状态路径
stateDiagram-v2
[*] --> Pending: Job 创建
Pending --> Running: Pod 被调度并启动
Running --> Succeeded: 任务退出码为 0
Running --> Failed: Pod 失败
Failed --> Running: 未超过 backoffLimit,创建新 Pod
Failed --> JobFailed: 达到失败阈值
Succeeded --> JobComplete: 达到 completions
Succeeded --> Running: 尚未达到 completions
JobComplete --> [*]
JobFailed --> [*]
图中有两个容易混淆的点:
- 一个失败 Pod 不必然意味着整个 Job 失败;
- 一个成功 Pod 也不必然意味着 Job 完成,除非已经达到
completions。
可以用以下命令查看 Job 的 Condition:
kubectl get job my-job -o yaml
kubectl describe job my-job
重点观察:
status.succeededstatus.failedstatus.activestatus.conditions- Events 中的调度、拉镜像、配额和节点错误
五、重试并不等于安全:Job 通常是至少一次执行
Kubernetes 控制器是异步系统。考虑下面的故障路径:
1. Pod 执行业务写入
2. 业务写入已经提交
3. Pod 在上报成功状态前崩溃
4. Job 控制器看到 Pod 未成功
5. 创建新的 Pod
6. 业务再次写入
从 Kubernetes 角度看,第二次执行是合理的,因为第一次执行的最终状态不可确认。从业务角度看,就可能产生重复扣款、重复发货、重复发送消息或重复导入数据。
因此,任务代码必须按“可能重复执行”设计。
幂等的定义
如果同一个业务操作执行一次和执行多次,最终可观察结果相同,则称该操作具有幂等性:
这里的 是业务操作, 是系统状态。
但“函数幂等”不能简单等同于“数据库插入不会重复”。例如:
INSERT INTO payment_log(order_id, amount)
VALUES ('order-123', 100);
如果没有唯一约束,重复执行会产生两行记录,因此不是幂等操作。
一种常见实现是使用业务幂等键:
CREATE TABLE payment_log (
idempotency_key TEXT PRIMARY KEY,
order_id TEXT NOT NULL,
amount NUMERIC NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
业务处理时:
INSERT INTO payment_log(idempotency_key, order_id, amount)
VALUES ('order-123:charge:20250308', 'order-123', 100)
ON CONFLICT (idempotency_key) DO NOTHING;
关键不在 SQL 语法本身,而在于幂等键必须稳定:
- 不能使用 Pod 名称;
- 不能使用随机 UUID;
- 不能每次重试都生成新键;
- 应使用业务批次、对象 ID、分片索引、逻辑时间等确定性字段。
例如 Indexed Job 的幂等键可以是:
daily-report:2025-03-08:shard-3
幂等与事务边界
以下流程仍然可能重复:
1. 数据库事务提交
2. 调用外部支付接口
3. 在第 2 步后进程崩溃
4. 重试
应尽量使用:
- 数据库唯一键;
- 数据库事务;
- 外部 API 的幂等键;
- Outbox 或 Inbox 模式;
- 可查询结果的状态机;
- 对账和补偿流程。
Kubernetes 只能负责重新运行 Pod,不能替业务确认跨系统操作是否已经完成。
六、CronJob 的调度模型
一个 CronJob 示例:
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Shanghai"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 1800
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
containers:
- name: report
image: example/report:1.0
args: ["--date", "previous-day"]
字段的关系是:
CronJob.spec.schedule
│
▼
CronJob 控制器计算到期时间
│
▼
创建一个 Job
│
▼
Job 使用 jobTemplate.spec
│
▼
Job 创建 Pod
jobTemplate 是创建出来的 Job 模板。修改 CronJob 的模板不会修改已经创建的 Job,也不会重启正在运行的 Job。
七、Cron 表达式和时区
1. 五字段 Cron 表达式
Kubernetes CronJob 的常见格式是:
分钟 小时 日期 月份 星期
例如:
0 2 * * *
表示每天的 02:00。
几个例子:
*/5 * * * * 每 5 分钟
0 * * * * 每小时整点
30 9 * * 1-5 工作日 09:30
0 0 1 * * 每月 1 日零点
CronJob 不适合表达复杂日历规则,例如:
- 最后一个工作日;
- 某个节假日前一天;
- 每月倒数第二天;
- 依赖业务数据库状态的时间条件。
这些规则应由任务代码、外部调度系统或明确的补跑机制处理。
2. timeZone
在支持该字段的 Kubernetes 版本中:
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Shanghai"
表示按照上海时区解释 02:00,而不是按照控制器所在节点的本地时区解释。
时区名称应使用 IANA 时区数据库名称,例如:
UTC
Asia/Shanghai
Europe/Berlin
America/New_York
不要把以下写法放入 schedule:
CRON_TZ=Asia/Shanghai 0 2 * * *
TZ=Asia/Shanghai 0 2 * * *
时区应通过 .spec.timeZone 指定。Kubernetes 会校验时区;如果时区无效,CronJob 会产生相应状态或事件,而不是可靠地按预期调度。
如果集群版本不支持 timeZone,客户端可能仍能保存 YAML 之外的结构差异,但服务器行为不会自动获得该能力。部署前检查:
kubectl explain cronjob.spec.timeZone
3. 夏令时边界
在使用夏令时的地区,一天可能有:
- 不存在的本地时间;
- 重复出现的本地时间。
例如某些地区在切换到夏令时的夜间会跳过一个小时,而切回冬令时会重复一个小时。CronJob 控制器对这类边界不能提供业务意义上的“绝不漏、绝不重”。
对于财务结算、账单、日终快照等任务,通常更容易验证的设计是:
- 使用
UTC; - 在业务中明确逻辑结算日;
- 用业务批次键做幂等;
- 对缺失批次进行显式扫描和补偿。
八、CronJob 不是精确计时器:调度、错过和补跑
CronJob 控制器周期性检查 CronJob,而不是在每个时间点由一个硬实时定时器精准触发。因此,下面因素都可能影响创建时间:
- 控制器调谐周期;
- API Server 延迟;
- 控制器重启或故障转移;
- 集群压力;
- 时钟和时间数据库问题;
- 前一个 Job 的并发策略。
startingDeadlineSeconds
该字段限制一个错过的调度时间可以延迟多久仍然允许创建 Job:
spec:
startingDeadlineSeconds: 1800
表示错过某次计划时间后,超过 1800 秒就不再为这次计划创建 Job。
它不是“保证在 1800 秒内启动”,而是一个可接受迟到窗口:
如果控制器恢复时发现某次计划时间仍在窗口内,可能创建对应 Job;如果已经超过窗口,则跳过。
没有设置该字段时,错过调度的处理窗口更宽,但这不应被理解为无限可靠的补跑队列。控制器对错过次数有保护限制;当错过数量过大时会停止逐个补建,并在事件中报告相关问题。高频 CronJob 长时间停机后,不能假设 Kubernetes 会无限创建历史 Job。
补跑的两种含义
“补跑”至少有两种不同操作。
CronJob 自动处理错过的调度
这是控制器根据 startingDeadlineSeconds 和调度历史做出的行为,受:
- 控制器运行状态;
- 错过时间;
- 并发策略;
- 时区;
- 控制器实现和版本
影响。
它不适合作为财务或数据管道的唯一补偿机制。
人工创建一个新的 Job
可以从 CronJob 模板创建一次性 Job:
kubectl create job daily-report-rerun-20250308 \
--from=cronjob/daily-report
这条命令的含义是:
- 读取
daily-report的 Job 模板; - 创建名为
daily-report-rerun-20250308的独立 Job; - 该 Job 不会改变 CronJob 的调度状态;
- 该 Job 完成后不会自动变回 CronJob 的一部分。
如果任务需要处理特定日期,应显式传入业务日期,而不是让容器猜测当前日期:
kubectl create job daily-report-rerun-20250308 \
--from=cronjob/daily-report \
--dry-run=client -o yaml
先检查生成的 YAML,再修改参数并应用,是比直接人工改线上对象更容易审计的方式。
九、并发策略:Allow、Forbid 和 Replace
CronJob 的 .spec.concurrencyPolicy 控制同一个 CronJob 创建的 Job 之间是否允许重叠。
Allow
spec:
concurrencyPolicy: Allow
默认策略。新的 Job 按计划创建,即使旧 Job 仍在运行。
若每个 Job 运行 40 分钟,而调度周期为 10 分钟,则理论上可能同时存在约 4 个活动 Job。更一般地,若任务运行时长为 ,调度周期为 ,在长期稳定状态下,重叠数量大约受:
影响,但实际数量还受启动延迟、失败重试和控制器行为影响。
Allow 适合任务天然可并行且共享资源不会冲突的情况。
Forbid
spec:
concurrencyPolicy: Forbid
如果上一次 Job 仍在运行,新的计划时间到达时不会启动新的 Job。
这并不意味着“等旧任务完成后自动补跑一次”。该次计划可能被跳过,尤其在任务持续时间较长时。
例如:
计划:02:00、03:00、04:00
任务:02:00 启动,运行到 03:40
03:00 到达:
旧 Job 仍在运行
新 Job 不创建
04:00 到达:
如果旧 Job 已结束,则创建 04:00 对应的 Job
03:00 这一轮不是排队等待,而是被并发策略抑制。是否仍能在控制器视为“错过调度”时补建,还受调度检查和截止时间影响,不能用它实现严格的补跑队列。
Replace
spec:
concurrencyPolicy: Replace
新的调度到达时,CronJob 控制器会尝试删除旧 Job,再创建新的 Job。
这不是强制终止的瞬时替换:
- 删除请求先发送到 API Server;
- Job 和 Pod 进入终止流程;
- 容器可能需要执行
preStop或等待优雅终止; - 新 Job 可能已经开始;
- 旧任务在短时间内仍可能产生副作用。
因此 Replace 不能替代业务取消协议。若任务会写数据库、上传文件或调用外部接口,必须处理“旧任务正在退出、新任务已经开始”的交叠窗口。
十、终止、Eviction 和优先级会如何影响 Job
Job 的 Pod 不是特殊的免故障 Pod。
节点故障和 Eviction
Pod 可能因为以下原因结束:
- 节点宕机;
- 资源压力;
- kubelet 驱逐;
- 人工删除;
- 节点排空;
- 抢占;
- 容器自身失败。
一个被删除或驱逐的 Pod 可能导致 Job 创建替代 Pod,但这不表示原来的业务副作用已经回滚。若 Pod 在外部系统操作完成后才被驱逐,替代 Pod 仍可能重复操作。
可以检查:
kubectl describe pod pod-name
kubectl get pod pod-name -o jsonpath='{.status.reason}{"\n"}'
kubectl get events --sort-by=.lastTimestamp
不要只根据 Pod 的 phase=Failed 断言“业务没有执行”。Pod 终止和业务事务提交是两个不同系统的事实。
Priority 与 Preemption
Job Pod 可以设置优先级:
spec:
template:
spec:
priorityClassName: batch-low
高优先级 Pod 可能抢占低优先级 Pod 的资源。对 Job 而言,抢占会带来两个后果:
- 被抢占的 Pod 可能失败,Job 随后创建替代 Pod;
- 批处理任务可能反复等待资源,直到达到失败阈值或长期无法调度。
优先级不会改变 Job 的完成定义,也不会让 Job 获得“至少执行一次”的额外保证。为关键批处理任务设置高优先级前,应确认它不会挤压在线服务;为低优先级任务设置抢占风险后,应让任务具备可重试、可恢复和幂等能力。
十一、清理:Job 对象、Pod 和历史记录是不同层次
任务完成后,至少有三类对象需要考虑:
CronJob
└── Job
└── Pod
1. CronJob 历史保留
CronJob 可以限制成功和失败 Job 的历史数量:
spec:
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
含义是:
- 保留最近若干个成功 Job;
- 保留最近若干个失败 Job;
- 旧 Job 会被 CronJob 控制器删除;
- 设置为
0表示不保留对应类型的历史 Job。
历史数量不是严格的实时上限。控制器是异步清理的,故障和调谐延迟期间可能暂时超出配置值。
2. TTL 控制器
Job 可以设置:
spec:
ttlSecondsAfterFinished: 86400
表示 Job 完成或失败后,至少等待约 86400 秒,再由 TTL 控制器清理 Job。Job 删除时,其从属 Pod 通常会随级联删除。
TTL 更适合表达“每个 Job 完成后保留多久”,CronJob 历史限制更适合表达“保留多少个最近 Job”。二者可以同时使用,但实际删除时间取决于两个清理机制中更早触发者。
例如:
successfulJobsHistoryLimit: 3
ttlSecondsAfterFinished: 86400
可能出现:
- 任务完成后不到一天,因为已经超过历史数量限制而被删除;
- 任务一直没有超过历史数量限制,但一天后被 TTL 删除。
3. 为什么不要立即删除所有对象
清理过于激进会损失:
- 失败原因;
- Pod 终止原因;
- 容器日志;
- Job Condition;
- 调度和资源事件。
生产环境通常应先确保日志、指标和审计信息已经发送到外部系统,再缩短对象保留时间。否则“清理成功”可能换来“无法解释为什么失败”。
4. 手工清理
查看对象:
kubectl get cronjob
kubectl get job --sort-by=.metadata.creationTimestamp
kubectl get pods -l job-name=some-job
删除 CronJob:
kubectl delete cronjob daily-report
删除 CronJob 通常不会自动替你决定如何处理已经创建的 Job。需要根据删除传播策略和现场验证结果确认残留对象:
kubectl get jobs
kubectl get pods
删除单个 Job:
kubectl delete job daily-report-rerun-20250308
不要在尚未确认任务是否仍有外部副作用时批量删除所有失败 Job。失败 Job 可能是唯一的诊断证据,也可能正等待人工补跑。
十二、一个完整的生产型模板
下面的模板展示了几个核心字段,但镜像和业务命令需要替换为实际任务。
apiVersion: batch/v1
kind: CronJob
metadata:
name: shard-report
labels:
app.kubernetes.io/name: shard-report
spec:
schedule: "15 2 * * *"
timeZone: "UTC"
# 上一次尚未结束时,不启动同一 CronJob 的下一次任务
concurrencyPolicy: Forbid
# 控制器恢复后,最多允许迟到 30 分钟的调度被处理
startingDeadlineSeconds: 1800
# 仅保留有限历史,避免对象无限增长
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
metadata:
labels:
app.kubernetes.io/name: shard-report
spec:
completions: 16
parallelism: 4
completionMode: Indexed
# 每个索引最多允许的失败次数
backoffLimitPerIndex: 2
# 失败索引达到该数量时,整个 Job 失败
maxFailedIndexes: 4
# Job 完成一天后允许 TTL 控制器清理
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app.kubernetes.io/name: shard-report
spec:
restartPolicy: Never
containers:
- name: worker
image: example/report-worker:1.0.0
env:
- name: JOB_KIND
value: "daily"
args:
- "--index-env"
- "JOB_COMPLETION_INDEX"
- "--idempotency-prefix"
- "daily-report"
部署前检查字段:
kubectl apply --dry-run=server -f shard-report.yaml
如果集群支持,可以检查实际对象:
kubectl get cronjob shard-report -o yaml
kubectl get jobs -l app.kubernetes.io/name=shard-report
这个模板仍然没有解决几个业务问题:
--index-env是否真的被应用解析;- 每个索引如何计算数据范围;
- 失败后如何恢复中间状态;
- 多次执行如何保证幂等;
- 任务是否允许跨逻辑日期运行;
- 任务被
Replace或节点驱逐时如何停止外部操作。
这些必须由任务自身和业务系统定义。
十三、常见误解和对应验证方法
误解一:parallelism: 10 表示有 10 个不同任务
错误。它只表示最多同时运行 10 个活动 Pod。是否处理不同数据,取决于应用是否使用 Indexed Job、分片配置或外部分配器。
验证:
kubectl get job name -o jsonpath='{.spec.completions}{" "}{.spec.parallelism}{"\n"}'
误解二:Job 成功一次就不会再执行
错误。如果 completions 大于 1,还需要更多成功完成。如果 Pod 成功状态在控制器获知前发生故障,业务也可能被重试。
验证:
kubectl get job name -o wide
kubectl get pods -l job-name=name
误解三:Forbid 会把错过的任务排队
错误。Forbid 的核心语义是阻止重叠运行,不是建立可靠的等待队列。计划轮次可能被跳过,补建还受截止时间和控制器调谐影响。
误解四:Replace 能保证旧任务完全停止后才启动新任务
错误。删除是异步终止过程。必须让任务对终止信号敏感,并让外部写操作幂等。
误解五:Job 失败说明业务完全没有产生结果
错误。Pod 可能在业务提交后、状态上报前失败。应在业务数据库、消息系统或外部 API 中查询幂等键,而不是只看 Job Condition。
误解六:设置了 timeZone 就能解决日期边界
不完全正确。它只规定 Cron 表达式如何解释本地时间。业务“结算日”“账单日”“上一自然日”的定义仍需在应用中明确,尤其是 UTC 与本地时区跨日时。
十四、诊断一条失败的 CronJob 链路
建议沿着对象关系逐层定位:
# 1. 查看 CronJob 的调度字段和状态
kubectl describe cronjob shard-report
# 2. 查看由 CronJob 创建的 Job
kubectl get jobs --sort-by=.metadata.creationTimestamp
# 3. 查看某个 Job 的 Condition、失败计数和事件
kubectl describe job job-name
# 4. 查看该 Job 的 Pod
kubectl get pods -l job-name=job-name -o wide
# 5. 查看 Pod 事件和终止原因
kubectl describe pod pod-name
# 6. 查看容器日志,包括上一次容器实例
kubectl logs pod-name -c worker
kubectl logs pod-name -c worker --previous
常见故障应区分:
| 现象 | 可能层次 |
|---|---|
| 没有 Job 被创建 | CronJob 被暂停、schedule 或时区无效、错过截止时间、并发策略、控制器问题 |
| Job 有但没有 Pod | 配额、Admission、控制器、对象模板或 API 错误 |
| Pod 一直 Pending | 资源不足、节点选择器、污点、亲和性、优先级或 PVC |
| Pod 很快 Failed | 应用退出码、参数、权限、Secret、网络或外部依赖 |
| Job 有成功 Pod 但仍未完成 | completions 未达到,或 Indexed Job 仍有未完成索引 |
| 任务重复写入 | 重试、驱逐、控制器确认延迟或 CronJob 并发重叠,业务缺少幂等 |
检查 CronJob 是否被暂停:
kubectl get cronjob shard-report \
-o jsonpath='{.spec.suspend}{"\n"}'
spec.suspend: true 会阻止新 Job 创建,但不会删除或暂停已经运行的 Job。恢复时,之前错过的调度是否补建仍受截止时间和控制器规则影响,不能把暂停当作可靠的暂停队列。
十五、设计时应先回答的几个确定问题
一个 Job 或 CronJob 能否安全运行,不取决于 YAML 是否短小,而取决于以下问题是否有明确答案:
-
完成条件是什么?
是一个批次成功,还是所有分片都成功? -
失败能否重试?
网络超时可以重试,数据格式错误通常不应无限重试。 -
同一任务执行两次会怎样?
是否有稳定幂等键、唯一约束或业务状态机? -
任务之间能否重叠?
如果不能,Forbid是否会造成业务漏跑?如果会,是否需要独立的任务队列表? -
错过调度如何处理?
是允许跳过、只补最近一次,还是必须扫描所有缺失批次? -
时间按哪个时区定义?
调度时区、数据分区时区和业务结算时区是否一致? -
失败对象保留多久?
是否能在 TTL 和历史限制触发前把日志、指标和失败批次保存到外部系统? -
节点驱逐和抢占后能否恢复?
Job 可能重新创建 Pod,但应用必须能够从已提交的状态继续执行。
Job 提供的是面向完成状态的控制循环,CronJob 提供的是面向时间的 Job 创建规则。把二者组合起来时,最重要的边界是:调度、执行、重试、补跑、幂等和清理分别由不同机制负责,任何一个机制都不会自动替另一个机制完成业务保证。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:DaemonSet:节点级 Agent、调度、更新、容忍和资源治理
- 下一篇:Kubernetes Probe:Startup、Readiness、Liveness、gRPC 和误配置风险
- 延伸:Pod 生命周期:Phase、Condition、RestartPolicy、终止和 Eviction
- 延伸:Kubernetes Priority 与 Preemption:优先级、抢占、公平和关键服务保护
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论