批处理任务的场景与挑战
业务里有一大类工作不是「一直在跑的服务」,而是跑完就结束的任务:凌晨做数据迁移、每天定时生成报表、定期清理过期日志与缓存、批量给用户发通知。这类任务以前经常跑在服务器 crontab 或 Jenkins 里,问题也不少:脚本散落、没有重试、失败靠人工发现、执行机器挂了任务就静默消失。
Kubernetes 为此提供了两个原生控制器:Job 负责「跑一次就退出」的一次性批处理,CronJob 负责按 cron 表达式定时的任务。它们把任务变成了集群里的资源:有状态可查询(kubectl get jobs)、失败可重试(backoffLimit)、并发可控(parallelism)、过期自动清理(TTL),比在虚拟机里堆 crontab 靠谱得多。
# 任务在 K8s 里的样子
Job ──创建──▶ Pod(跑完退出)──▶ 完成状态(可查询、可清理)
CronJob ──按 schedule 定时──▶ 每次生成一个 Job ──▶ Pod 跑完即止
- Job一次性批处理:跑完即止、失败可重试。
- CronJob定时任务:cron 表达式驱动、并发可控。
- 编排并行度、依赖顺序、任务队列。
- 监控成功率、延迟与告警闭环。
Job 基础:跑一次就退出
Job 的语义很明确:创建一组 Pod,等它们全部成功退出。Pod 里的容器如果正常结束(exit 0),Job 就标记完成;如果失败,Job 按 backoffLimit 自动重新创建 Pod 重试,超过上限就标记失败。一个关键配置是 restartPolicy 必须设为 Never 或 OnFailure(服务类 Pod 常用的 Always 在 Job 里不合法,因为任务要允许退出)。
# job-migrate.yaml:一次性数据迁移任务
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
backoffLimit: 3 # 最多重试 3 次
activeDeadlineSeconds: 600 # 超过 10 分钟整体放弃
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: registry.example.com/db-tools:2.1
command: ["/bin/sh", "-c", "db-migrate --up"]
想要「并发跑 N 个」就用 parallelism + completions:比如 completions: 8, parallelism: 2 表示总共要成功 8 个、同时最多跑 2 个。适合把大任务拆成分片并行处理;配合「按序号分片」的环境变量,同一个镜像就能处理不同的数据切片。
CronJob:像 crontab 一样跑定时任务
CronJob 在 Job 外面加了一层「定时触发」:按 schedule(标准 5 段 cron 表达式,如 0 2 * * * 表示每天凌晨两点)到点生成一个新的 Job。它有两个必须认真配的参数:concurrencyPolicy 决定上一轮还没跑完时这一轮怎么办(Allow 并发跑 / Forbid 跳过 / Replace 顶掉旧的),startingDeadlineSeconds 决定错过调度窗口后还补不补跑。
# cronjob-cleanup.yaml:每天 02:00 清理过期数据
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-cleanup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid # 上一轮没跑完就跳过本轮
startingDeadlineSeconds: 300
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: registry.example.com/cleaner:1.4
args: ["--older-than=30d"]
历史 Job 会越积越多,用 successfulJobsHistoryLimit 和 failedJobsHistoryLimit 控制保留条数(比如各留 3 条),既保住「可回看最近执行」又不至于把 etcd 撑爆。排查时 kubectl get cronjobs 看上次调度时间、kubectl get jobs 看最近的执行,一眼能判断「到点没跑」还是「跑了失败」。
任务参数化与失败重试
同一个任务脚本要跑不同参数(比如按日期回补数据),不要写死镜像,用环境变量或命令行参数把变化点暴露出来。Job 里通过 env 传入参数,CronJob 想「每天带当天日期」可以在 Job 模板里加 date +%F 之类的容器内替换,或者挂 ConfigMap 动态更新。
失败的兜底设计比「尽量不失败」更重要:任务要幂等(重复跑同一参数结果一致,可以安全重试),重试要有上限(backoffLimit 挡住死循环),成功要有明确退出码(脚本里区分「业务失败」与「环境故障」,别把数据库连不上也当成业务结果返回 0)。
# 参数化 + 主动清理(TTL 自动删完成 Job)
apiVersion: batch/v1
kind: Job
metadata:
name: report-gen
annotations:
batch.kubernetes.io/job-ttl: "3600" # 完成后 1 小时自动清理
spec:
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: report
image: registry.example.com/reporter:3.2
env:
- name: REPORT_DATE
value: "2026-10-28"
- name: OUTPUT_BUCKET
value: "s3://reports/monthly"
command: ["/bin/sh", "-c", "gen-report --date $REPORT_DATE --out $OUTPUT_BUCKET && exit 0 || exit 1"]
ttlSecondsAfterFinished 是省心的关键:完成或失败的 Job 到点自动被回收,不用定期手动清理。要排查历史时,把失败任务的历史保留期适当调大,成功任务调小,兼顾「能复盘」与「不占资源」。
任务编排:依赖与并行
单个任务好管,多个任务之间「先 A 后 B、B 和 C 并行、都完了再 D」的依赖关系才是批处理真正难的地方。轻量方案是 initContainer 做前置检查:主容器启动前先等依赖就绪(比如等另一个 Job 的完成标记写进 ConfigMap 或数据库)。重型方案是上工作流引擎(Argo Workflows、Tekton),把 DAG 声明成代码,由引擎负责调度、重试和并行。
# 用 initContainer 表达「等依赖 Job 完成再干活」
spec:
initContainers:
- name: wait-for-seed
image: registry.example.com/wait:1.0
command: ["/bin/sh", "-c", "until check-seed-ready; do sleep 5; done"]
containers:
- name: main
image: registry.example.com/loader:2.0
选择标准很朴素:两三个任务有先后,initContainer + ConfigMap 标记就够了;任务多、分支多、要并行分发,直接上工作流引擎,别自己发明调度器。无论哪种方式,任务之间传递结果建议写进对象存储或数据库,而不是靠「上一步的 Pod 名」这种脆弱的隐式依赖。
监控与告警:任务成功率与延迟
定时任务最容易出「静默失败」:凌晨两点跑挂了,没人看见,早上八点才发现报表没出。所以要给任务配上监控闭环:成功率(每天任务完成/失败数)、延迟(任务执行耗时)、「该跑没跑」(CronJob 上次调度时间距今超过一个周期)。Kube 指标 API 能直接拿到 job 完成状态,Prometheus 里也有 kube_job_status_succeeded / kube_job_status_failed 等现成指标。
# Prometheus 告警规则(片段):任务失败即告警
groups:
- name: batch-alerts
rules:
- alert: CronJobJobFailed
expr: kube_job_status_failed > 0
for: 5m
labels: { severity: warning }
annotations:
summary: "批处理任务 {{ $labels.job_name }} 失败"
- alert: CronJobNotScheduled
expr: time() - kube_cronjob_status_last_schedule_time > 1800
for: 5m
labels: { severity: warning }
告警之外还要有「兜底操作」:失败任务要么自动重跑(工作流引擎可配重试策略),要么打进待办队列由值班人处理。只有告警没有兜底,凌晨的任务失败依然要靠人来救。把「任务成功率」放进周报里,让批处理的健康度像在线服务一样被持续盯住。
一套批处理任务清单
把全文收敛成一张清单,从今天就能开始执行:
批处理任务清单
1. 任务容器化 脚本打包成镜像,参数走 env/args
2. 幂等优先 重复跑同一参数结果一致,可安全重试
3. 退出码明确 区分业务失败与环境故障
4. backoffLimit 限制重试次数,防死循环
5. 超时兜底 activeDeadlineSeconds 卡整体时长
6. 并发策略 concurrencyPolicy 明确并发/跳过/替换
7. 历史保留 successful/failedJobsHistoryLimit 限条数
8. TTL 清理 完成后自动回收 Job
9. 监控告警 成功率 + 延迟 + 该跑没跑三告警
10. 依赖明确 initContainer 或工作流引擎,别靠隐式顺序
把批处理搬进 Kubernetes 的真正收益,不是「少了一台 cron 机器」,而是任务从「看不见的脚本」变成了「有状态、可查询、可重试、可告警的集群资源」。从最简单的 Job 迁移脚本开始,逐步加上 CronJob 定时、TTL 清理和成功率告警,一套批处理平台就长出来了;等任务多到需要依赖编排时,再平滑引入工作流引擎,不会推翻重来。任务会越跑越多,但管理它的方法应该越跑越少。