使用展开的方式进行并行处理

本任务展示基于一个公共的模板运行多个Jobs。 你可以用这种方法来并行执行批处理任务。

在本任务示例中,只有三个工作条目:applebananacherry。 示例任务处理每个条目时打印一个字符串之后结束。

参考在真实负载中使用 Job了解更适用于真实使用场景的模式。

准备开始

你应先熟悉基本的、非并行的 Job 的用法。

你必须拥有一个 Kubernetes 的集群,同时你必须配置 kubectl 命令行工具与你的集群通信。 建议在至少有两个不作为控制平面主机的节点的集群上运行本教程。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:

任务中的基本模板示例要求安装命令行工具 sed。 要使用较高级的模板示例,你需要安装 Python, 并且要安装 Jinja2 模板库。

一旦 Python 已经安装好,你可以运行下面的命令安装 Jinja2:

  1. pip install --user jinja2

基于模板创建 Job

首先,将以下作业模板下载到名为 job-tmpl.yaml 的文件中。

application/job/job-tmpl.yaml 使用展开的方式进行并行处理 - 图1

  1. apiVersion: batch/v1
  2. kind: Job
  3. metadata:
  4. name: process-item-$ITEM
  5. labels:
  6. jobgroup: jobexample
  7. spec:
  8. template:
  9. metadata:
  10. name: jobexample
  11. labels:
  12. jobgroup: jobexample
  13. spec:
  14. containers:
  15. - name: c
  16. image: busybox:1.28
  17. command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
  18. restartPolicy: Never
  1. # 使用 curl 下载 job-tmpl.yaml
  2. curl -L -s -O https://k8s.io/examples/application/job/job-tmpl.yaml

你所下载的文件不是一个合法的 Kubernetes 清单。 这里的模板只是 Job 对象的 yaml 表示,其中包含一些占位符,在使用它之前需要被填充。 $ITEM 语法对 Kubernetes 没有意义。

基于模板创建清单

下面的 Shell 代码片段使用 sed 将字符串 $ITEM 替换为循环变量, 并将结果写入到一个名为 jobs 的临时目录。

  1. # 展开模板文件到多个文件中,每个文件对应一个要处理的条目
  2. mkdir ./jobs
  3. for i in apple banana cherry
  4. do
  5. cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
  6. done

检查上述脚本的输出:

  1. ls jobs/

输出类似于:

  1. job-apple.yaml
  2. job-banana.yaml
  3. job-cherry.yaml

你可以使用任何一种模板语言(例如:Jinja2、ERB),或者编写一个程序来 生成 Job 清单。

基于清单创建 Job

接下来用一个 kubectl 命令创建所有的 Job:

  1. kubectl create -f ./jobs

输出类似于:

  1. job.batch/process-item-apple created
  2. job.batch/process-item-banana created
  3. job.batch/process-item-cherry created

现在检查 Job:

  1. kubectl get jobs -l jobgroup=jobexample

输出类似于:

  1. NAME COMPLETIONS DURATION AGE
  2. process-item-apple 1/1 14s 22s
  3. process-item-banana 1/1 12s 21s
  4. process-item-cherry 1/1 12s 20s

使用 kubectl 的 -l 选项可以仅选择属于当前 Job 组的对象 (系统中可能存在其他不相关的 Job)。

你可以使用相同的 标签选择算符 来过滤 Pods:

  1. kubectl get pods -l jobgroup=jobexample

输出类似于:

  1. NAME READY STATUS RESTARTS AGE
  2. process-item-apple-kixwv 0/1 Completed 0 4m
  3. process-item-banana-wrsf7 0/1 Completed 0 4m
  4. process-item-cherry-dnfu9 0/1 Completed 0 4m

我们可以用下面的命令查看所有 Job 的输出:

  1. kubectl logs -f -l jobgroup=jobexample

输出类似于:

  1. Processing item apple
  2. Processing item banana
  3. Processing item cherry

清理

  1. # 删除所创建的 Job
  2. # 集群会自动清理 Job 对应的 Pod
  3. kubectl delete job -l jobgroup=jobexample

使用高级模板参数

第一个例子中,模板的每个示例都有一个参数 而该参数也用在 Job 名称中。不过,对象 名称 被限制只能使用某些字符。

这里的略微复杂的例子使用 Jinja 模板语言 来生成清单,并基于清单来生成对象,每个 Job 都有多个参数。

在本任务中,你将会使用一个一行的 Python 脚本,将模板转换为一组清单文件。

首先,复制下面的 Job 对象模板到一个名为 job.yaml.jinja2 的文件。

  1. {% set params = [{ "name": "apple", "url": "http://dbpedia.org/resource/Apple", },
  2. { "name": "banana", "url": "http://dbpedia.org/resource/Banana", },
  3. { "name": "cherry", "url": "http://dbpedia.org/resource/Cherry" }]
  4. %}
  5. {% for p in params %}
  6. {% set name = p["name"] %}
  7. {% set url = p["url"] %}
  8. ---
  9. apiVersion: batch/v1
  10. kind: Job
  11. metadata:
  12. name: jobexample-{{ name }}
  13. labels:
  14. jobgroup: jobexample
  15. spec:
  16. template:
  17. metadata:
  18. name: jobexample
  19. labels:
  20. jobgroup: jobexample
  21. spec:
  22. containers:
  23. - name: c
  24. image: busybox:1.28
  25. command: ["sh", "-c", "echo Processing URL {{ url }} && sleep 5"]
  26. restartPolicy: Never
  27. {% endfor %}

上面的模板使用 python 字典列表(第 1-4 行)定义每个作业对象的参数。 然后使用 for 循环为每组参数(剩余行)生成一个作业 yaml 对象。 我们利用了多个 YAML 文档(这里的 Kubernetes 清单)可以用 --- 分隔符连接的事实。 我们可以将输出直接传递给 kubectl 来创建对象。

接下来我们用单行的 Python 程序将模板展开。

  1. alias render_template='python -c "from jinja2 import Template; import sys; print(Template(sys.stdin.read()).render());"'

使用 render_template 将参数和模板转换成一个 YAML 文件,其中包含 Kubernetes 资源清单:

  1. # 此命令需要之前定义的别名
  2. cat job.yaml.jinja2 | render_template > jobs.yaml

你可以查看 jobs.yaml 以验证 render_template 脚本是否正常工作。

当你对输出结果比较满意时,可以用管道将其输出发送给 kubectl,如下所示:

  1. cat job.yaml.jinja2 | render_template | kubectl apply -f -

Kubernetes 接收清单文件并执行你所创建的 Job。

清理

  1. # 删除所创建的 Job
  2. # 集群会自动清理 Job 对应的 Pod
  3. kubectl delete job -l jobgroup=jobexample

在真实负载中使用 Job

在真实的负载中,每个 Job 都会执行一些重要的计算,例如渲染电影的一帧, 或者处理数据库中的若干行。这时,$ITEM 参数将指定帧号或行范围。

在此任务中,你运行一个命令通过取回 Pod 的日志来收集其输出。 在真实应用场景中,Job 的每个 Pod 都会在结束之前将其输出写入到某持久性存储中。 你可以为每个 Job 指定 PersistentVolume 卷,或者使用其他外部存储服务。 例如,如果你在渲染视频帧,你可能会使用 HTTP 协议将渲染完的帧数据 用 ‘PUT’ 请求发送到某 URL,每个帧使用不同的 URl。

Job 和 Pod 上的标签

你创建了 Job 之后,Kubernetes 自动为 Job 的 Pod 添加 标签,以便能够将一个 Job 的 Pod 与另一个 Job 的 Pod 区分开来。

在本例中,每个 Job 及其 Pod 模板有一个标签:jobgroup=jobexample

Kubernetes 自身对标签名 jobgroup 没有什么要求。 为创建自同一模板的所有 Job 使用同一标签使得我们可以方便地同时操作组中的所有作业。 在第一个例子中,你使用模板来创建了若干 Job。 模板确保每个 Pod 都能够获得相同的标签,这样你可以用一条命令检查这些模板化 Job 所生成的全部 Pod。

说明: 标签键 jobgroup 没什么特殊的,也不是保留字。 你可以选择你自己的标签方案。 如果愿意,有一些建议的标签 可供使用。

替代方案

如果你有计划创建大量 Job 对象,你可能会发现:

  • 即使使用标签,管理这么多 Job 对象也很麻烦。
  • 如果你一次性创建很多 Job,很可能会给 Kubernetes 控制面带来很大压力。 一种替代方案是,Kubernetes API 可能对请求施加速率限制,通过 429 返回 状态值临时拒绝你的请求。
  • 你可能会受到 Job 相关的资源配额 限制:如果你在一个批量请求中触发了太多的任务,API 服务器会永久性地拒绝你的某些请求。

还有一些其他作业模式 可供选择,这些模式都能用来处理大量任务而又不会创建过多的 Job 对象。

你也可以考虑编写自己的控制器 来自动管理 Job 对象。