Kubernetes 设计模式笔记 —— Job & CronJob

Batch Job

Batch Job 模式适合处理隔离的原子化的工作任务,能够在分布式的环境中,可靠地运行 short-lived Pods,直到工作任务成功地结束。

在 Kubernetes 中,可以通过不同的方式创建 Pod:

  • Bare Pod:可以手动创建 Pod 来运行容器应用,但是当此类 Pod 所在的节点失效时,Pod 不会自动重启。除非用于开发或测试目的,此类方式并不推荐
  • ReplicaSet:当 Pod 应该长时间持续运行时(比如 web server),就适合用此方式来创建 Pod 和管理其生命周期。它会确保在任意时刻,运行着的 Pod 副本数量都是稳定的
  • DaemonSet:负责在每一个节点上都部署一个 Pod。通常情况下用于平台管理工作,比如监控、日志聚合、存储等

上述 Pod 有一个共同点,它们都代表着长时间运行的进程,并不是在一段时间后就需要被关掉。但是在某些场景下,仍需要执行一类预先定义好的、有限的工作流,当该工作流程可靠地完成后,再关闭对应的容器。

Kubernetes Job 类似于 ReplicaSet,它也会创建 1 个或者多个 Pods 并确保它们成功运行。区别在于,当特定数量的 Pods 成功终止后,Job 就变为完成状态,不会再有额外的 Pod 被启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: batch/v1
kind: Job
metadata:
name: random-generator
spec:
completions: 5
parallelism: 2
template:
metadata:
name: random-generator
spec:
restartPolicy: OnFailure
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command: [ "java", "-cp", "/", "RandomRunner", "/numbers.txt", "10000" ]

比如上面配置的 Job,会确保有 5 个 Pod 成功执行完毕,可以有两个 Pod 同时运行。此外,Job 配置文件中的 restartPolicy 是必需的,且其值只能是 OnFailureNever,不能是 Always

为什么不通过 bare Pods 来执行 Job 对应的任务呢?因为 Job 相比于 bare Pods,能够提供更多可靠性和扩展性方面的好处。

  • Job 并不是临时的 in-memory 任务,而是一个持久化的能够在集群重启后幸存的任务
  • Job 完成后并不会被删除,而是继续保留,方便以后追踪问题。只有当 bare Pods 是 restartPolicy: OnFailure 时,其才会拥有同样的特性
  • Job 可能需要执行多次,可以通过 .spec.completions 指定
  • 当任务确实需要完成多次时,Job 还支持扩展,即同一时间开启多个 Pods。可以通过 .spec.parallelism 指定
  • 若节点失效,或者 Pod 正在运行时因为某些原因被移除,由 Job 创建的 Pods 会被 scheduler 重新分配给健康的节点

两个字段对控制 Job 的行为发挥着关键作用:

  • .spec.completions:指定 Pod 的数量。当特定数量的 Pod 执行完毕后,当前 Job 才算完成
  • .spec.parallelism:指定可以并行执行的 Pod 副本数量

Parallel Batch Job with a fixed completion count

基于上述两个参数,Job 可以分为如下几种类型:

  • Single Pod Job:不设置 .spec.completions.spec.parallelism 的值,或者将它们设置为默认值 1。此类 Job 只会启动一个 Pod,当 Pod 成功退出后,Job 完成
  • Fixed completions count Jobs:.spec.completions 的值大于 1。当特定数量(.spec.completions)的 Pod 执行完毕后,Job 完成
  • Work queue Job:.spec.completions 不设置或者设为默认值,.spec.parallelism 大于 1。适用于工作队列中的 Job。当至少有一个 Pod 成功终止时,所有其他 Pod 也会自行终止。比如,一堆固定数量的待处理项目保存在某个队列中,并行的 Pod 可以按顺序获取并处理它们,当某个 Pod 检测到队列为空并成功退出后,Job controller 等待其他 Pod 终止运行
总结

Job 帮助我们将隔离的工作单元变成一个可靠的、可扩展的执行单元。并不是所有的服务都需要一直运行,比如某些服务可能需要按需运行,某些必须在特定的时间窗口运行,某些必须按照计划重复执行。
通过 Job 可以只在需要的时候运行 Pod,且任务完成后就退出。使用 Job 处理 short-lived 任务可以节约系统资源。

Periodic Job

Periodic Job 是对 Batch Job 的扩展,为其添加了时间维度,同时允许临时的事件触发工作流的执行。
在分布式系统的世界里,有一种比较清晰的倾向,借助 HTTP 和轻量的消息系统实现实时、事件驱动的应用。不考虑软件开发中的此类倾向,计划任务仍然是一种历史悠久且至今常用的手段。
它们通常用于自动化的系统维护工作或者管理员任务,在商业应用方面的场景比如文件同步、发送邮件、清理和归档旧文件等。

传统的处理 Periodic Job 的方式是借助专门的计划任务软件比如 Cron。但是 Cron jobs 运行在单一的服务器上,难以维护且有发生单点故障的风险。
这也是为什么很多开发者会尝试实现自己的方案,比如 Java 中的 Quartz、Spring Batch 等。但是类似于 Cron,它们也会遇到弹性和高可用性方面的挑战,导致较高的资源使消耗。此外在这类方案里,Job 调度器是应用的一部分,为了获得高可用,通常就需要运行多个应用实例,同时还需要确保同一时刻下只有一个实例是活跃的。从而引入 leader election 等分布式系统问题。

面对以上的一些问题,Kubernetes 实现了 CronJob,允许开发者以广为熟知的 Cron 格式将 Job 设置为计划任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: random-generator
spec:
# Every three minutes
schedule: "*/3 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command: [ "java", "-cp", "/", "RandomRunner", "/numbers.txt", "10000" ]
restartPolicy: OnFailure

与 Job 相比,CronJob 有一些额外的字段:

  • .spec.schedule:指定 Job 的 schedule 模式(如 0 * * * * 表示每个小时触发一次)
  • .spec.startingDeadlineSeconds:Job 启动时的截止时间。有些时候由于资源不够或者缺少其他依赖,Job 错过了预定的触发时间。此字段用于指定错过多少秒后就直接跳过此次执行
  • .spec.concurrencyPolicy:用于控制同一个 CronJob 的并发执行。默认值为 Allow,即使前一个 Job 并未结束,也允许新的 Job 实例被创建;可以指定为 Forbid,若当前 Job 并未结束,则跳过下一次执行;或者改为 Replace,取消当前还未结束的 Job 并启动一个新的 Job 实例
  • .spec.suspend:暂停所有后续执行,但不影响已经开始的执行
  • .spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit:应保留多少已完成和失败的 Job 作为审计数据
总结

CronJob 其实是一个非常简单的原语,在现有的 Job 定义中添加类似 Cron 的行为。但是当它与 Kubernetes 提供的其他原语比如 Pods、资源隔离结合起来时,就成为一个非常强大的任务调度系统。
它的调度行为是平台的一部分,实现在应用的外部,使得开发者能够专注于应用的业务逻辑,无需在应用内部额外设计一套调度逻辑。同时提供了高可用、高弹性、高容积以及由策略驱动的 Pod 部署等特性。
当然,和 Job 一样,CronJob 容器在部署时,也需要考虑所有的特殊情况,比如重复执行、未触发、并发执行和任务取消等。

参考资料

Kubernetes Patterns