Batch Job模式适合管理孤立的原子工作单元。它基于Job抽象,在分布式环境中可靠地运行短暂的Pod,直到完成。

存在问题

Kubernetes中管理和运行容器的主要基元是Pod。创建Pod的方式有不同的特点。

  • Bare Pod
    可以手动创建一个Pod来运行容器。然而,当这种Pod运行的节点出现故障时,Pod不会被重新启动。不鼓励以这种方式运行Pod,除非出于开发或测试目的。这种机制也被称为非托管或裸露的Pod。
  • ReplicaSet
    该控制器用于创建和管理预期连续运行的Pod的生命周期(例如,运行一个Web服务器容器)。在任何给定时间中,它维护一组稳定的副本Pods运行,并保证指定数量相同的Pods可用。
  • DaemonSet
    控制器以单个Pod方式运行在每个节点上。通常用于管理平台功能,如监控、日志收集、存储容器等。

这些Pod的一个共同点是,它们代表了长期运行的进程,并不是要在一段时间后停止。然而,在某些情况下,需要可靠地执行一个预定义的限定的工作,然后关闭容器。对于这个任务,Kubernetes提供了Job资源。

解决方案

Kubernetes Job类似于ReplicaSet,因为它创建了一个或多个Pod,并确保它们成功运行。然而,不同的是,一旦预期数量的Pod成功终止,该作业就被认为是完成的,不再启动额外的Pod。一个Job定义看起来像例 1-1。

1.1 Job实例
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应该运行五个Pods来完成,这五个Pods必须全部成功。
有两个pod是并行的。

Job 和 ReplicaSet 定义之间的一个重要区别是 .spec.template.spec.restartPolicy。ReplicaSet的默认值是Always,这对于必须始终保持运行的长期进程来说是有意义的。对于一个Job来说,不允许使用 Always 值,唯一可能的选项是 OnFailure 或Never。

那么,为什么还要创建一个Job及只运行一次Pod,而不是使用裸Pod呢?使用Job提供了许多可靠性和可扩展性的优势,使其成为首选。

  • 一个Job不是一个短暂的内存中的任务,而是一个持久的任务,可以在集群重启后存活下来。
  • 当一个作业完成时,它不会被删除,但会被保留以用于跟踪。作为Job的一部分创建的Pods也不会被删除,但可用于检查(例如,检查容器日志)。对于裸露的Pods也是如此,但只适用于restartPolicy: OnFailure。
  • 一个Job可能需要执行多次。使用.spec.completions字段可以指定一个Pod在Job本身完成之前应该成功完成多少次。
  • 当一个Job必须多次完成时(通过.spec.completions设置),它也可以通过同时启动多个Pod来扩展和执行。这可以通过指定.spec.parallelism字段来实现。
  • 如果节点出现故障,或者当Pod因某种原因被驱逐,而仍在运行时,调度器会将Pod调度在一个新的健康节点上并重新运行。裸露的Pod将保持在失败的状态,因为现有的Pod永远不会被移动到其他节点上。

所有这些都使得Job基元对于那些需要对单位工作的完成进行一些保证的场景具有吸引力。在Job的行为中起主要作用的两个字段是:

  • .spec.completions
    指定应运行多少个Pods来完成一个Job。
  • .spec.parallelism
    指定多少个Pod副本可以并行运行。设置较高的数字并不能保证较高的并行性,实际的Pod数量可能仍然少于(在某些特殊的情况下,更多)所需的数量(例如,由于节流、资源配额、剩余的完成量不够以及其他原因)。将此字段设置为 0,可以有效地暂停作业。

图1-1显示了例1-1中定义的完成数为5,并行度为2的Batch Job的处理方式。

批量job.png

根据这两个参数,有以下几种类型的Job:

  • Single Pod Job
    当您不考虑.spec.completions和.spec.parallelism或将它们设置为默认值1时,就会选择这种类型。这样的Job只启动一个Pod,一旦单个Pod成功终止(退出代码为0),就会完成。
  • Fixed completion count Jobs
    当你指定.spec.completions的数字大于1时,这个数量的Pod必须成功。您可以选择设置.spec.parallelism,或者将其保留为默认值1。这样的Job在.spec.completions数量的Pods成功完成后,就被认为是完成了。例1-1展示了这种模式的运行情况,当我们事先知道Jobs的数量,并且单个工作项的处理成本证明了使用专用Pod的合理性时,这是最好的选择。
  • Work queue Jobs
    当您不使用 .spec.completions 并将 .spec.parallelism 设置为大于 1 的整数时,您就拥有了一个并行Job的工作队列。当至少有一个Pod成功终止,并且所有其他Pod也终止时,一个工作队列Job就被认为已经完成。这种设置需要Pods之间相互协调,确定每个Pods正在进行的工作,这样才能以协调的方式完成。例如,当队列中存储了固定但未知数量的工作项目时,并行的Pods可以逐个拾取这些项目进行工作。第一个检测到队列为空并成功退出的Pod表示Job完成。Job控制器也会等待所有其他Pod终止。由于一个Pod处理多个工作项目,这种Job类型是细化工作项目的最佳选择–当每个工作项目的一个Pod的开销是不合理的。

如果你有无限的工作项流需要处理,其他控制器(如ReplicaSet)是管理处理这些工作项目的Pod的更好选择。

讨论

Job抽象是一个非常基本但也是基本的基元,其他基元(如 CronJobs)都是基于这个基元。Job有助于将孤立的工作单元转化为可靠和可扩展的执行单元。然而,Job 并不决定如何将可单独处理的工作项映射到 Jobs 或 Pods 中。这是你必须在考虑每个选项的利弊后确定的事情。

  • One Job per work item
    这个选项有创建Kubernetes Jobs的开销,也为平台管理大量消耗资源的Job。当每个工作项目都是一个复杂的任务时,必须记录、跟踪或缩放时,这个选项很有用。
  • One Job for all work items
    这个选项适用于大量的工作项目,这些项目不需要由平台独立跟踪和管理。在这种情况下,工作项目必须通过批处理框架从应用程序内部进行管理。

Job 基元只为工作项目的调度提供了最基本的基础知识。任何复杂的实现都必须将Job基元与批处理应用框架(例如,在Java生态系统中,我们将Spring Batch和JBeret作为标准实现)结合起来,以达到预期的结果。

不是所有的服务都必须一直运行。有些服务必须按需运行,有些必须在特定的时间运行,有些必须定期运行。使用Jobs可以只在需要的时候运行Pod,并且只在任务执行期间运行。Jobs被安排在具有所需容量的节点上,满足Pod调度策略和其他容器依赖性考虑。使用Jobs来执行短时任务,而不是使用长期运行的抽象(如ReplicaSet),可以为平台上的其他工作负载节省资源。所有这些都使得Jobs成为一个独特的基元,而Kubernetes则是一个支持多样化工作负载的平台。