Kubernetes Job Controller 原理和原始碼分析(一)

胡說雲原生發表於2021-10-19

概述什麼是 JobJob 入門示例Job 的 specPod Template併發問題其他屬性

概述

Job 是主要的 Kubernetes 原生 Workload 資源之一,是在 Kubernetes 之上執行批處理任務最簡單的方式,在 AI 模型訓練等場景下最基礎的實現版本就是拉起一個 Job 來完成一次訓練任務,然後才是各種自定義 “Job” 實現進階處理,比如分散式訓練需要一個 “Job” 同時拉起多個 Pod,但是每個 Pod 的啟動引數會有差異。所以深入理解 Job 的功能和實現細節是進一步開發自定義 “Job” 型別工作負載的基礎。今天開始我們計劃分成幾講來從 Job 的特性介紹、原始碼分析等方面深度剖析 Job 資源和其背後的 Job 控制器。


《Kubernetes Job Controller 原理和原始碼分析》分為三講:

什麼是 Job

我們建立一個 Job 後,Job 控制器會拉起一個或多個 Pod 開始執行,直到成功退出的 Pod 達到了指定數量。Job 執行過程中可以被掛起,掛起操作會直接刪掉正在執行中的 Pod;另外 Job 被刪除時對應的 Pod 也會全部被刪掉,這個特性和其他控制器是一致的。

一個最常見的 Job 使用方式是通過 Job 拉起一個 Pod,確保這個 Pod 成功執行一次。如果唯一的 Pod 因為某種原因執行失敗了,這時候 Job 會重新拉起一個 Pod,繼續嘗試完成“成功一次”的目標。當然 Job 支援併發拉起多個 Pod。

Job 入門示例

借用官網計算圓周率的例子:

 1apiVersion: batch/v1
2kind: Job
3metadata:
4  name: pi
5spec:
6  template:
7    spec:
8      containers:
9      - name: pi
10        image: perl
11        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
12      restartPolicy: Never
13  backoffLimit: 4

Apply 這個 yaml 檔案就能床架一個 Job,看下相關資訊:

  • 剛建立時檢視 Job 狀態
1# kubectl get job
2NAME   COMPLETIONS   DURATION   AGE
3pi     0/1           3s         3s
  • 接著檢視對應的 pod 狀態
 1# kubectl get pod --selector=job-name=pi
2NAME          READY   STATUS              RESTARTS   AGE
3pi--1-25l8f   0/1     ContainerCreating   0          8s
4
5# kubectl get pod --selector=job-name=pi
6NAME          READY   STATUS    RESTARTS   AGE
7pi--1-25l8f   1/1     Running   0          12s
8
9# kubectl get pod --selector=job-name=pi
10NAME          READY   STATUS      RESTARTS   AGE
11pi--1-25l8f   0/1     Completed   0          21s
  • Describe 檢視 job 詳細資訊
 1kubectl describe job pi
2Name:             pi
3Namespace:        default
4Selector:         controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
5Labels:           controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
6                  job-name=pi
7Annotations:      <none>
8Parallelism:      1
9Completions:      1
10Completion Mode:  NonIndexed
11Start Time:       Mon, 18 Oct 2021 15:55:02 +0800
12Completed At:     Mon, 18 Oct 2021 15:55:23 +0800
13Duration:         21s
14Pods Statuses:    0 Running / 1 Succeeded / 0 Failed
15Pod Template:
16  Labels:  controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
17           job-name=pi
18  Containers:
19   pi:
20    Image:      perl
21    Port:       <none>
22    Host Port:  <none>
23    Command:
24      perl
25      -Mbignum=bpi
26      -wle
27      print bpi(2000)
28    Environment:  <none>
29    Mounts:       <none>
30  Volumes:        <none>
31Events:
32  Type    Reason            Age    From            Message
33  ----    ------            ----   ----            -------
34  Normal  SuccessfulCreate  5m59s  job-controller  Created pod: pi--1-25l8f
35  Normal  Completed         5m39s  job-controller  Job completed

看這裡的輸出資訊時,我們帶一點“原始碼”的視角去思考一下,一行行看下來可以發現很多 Job controller 的工作痕跡,比如自動配置了 label、selector 等屬性,記錄了 Job 下 pods 的當前狀態,工作耗時,起止時間等等資訊。另外有兩個 Event,分別是 Pod 成功建立和 Job 完成結束時的事件。後面刷原始碼時這裡的所有邏輯我們都可以一一發現。

這時當然也可以通過日誌檢視圓周率計算結果:

  • 檢視計算結果
1# kubectl logs `kubectl get pod --selector=job-name=pi | grep -v NAME | awk '{print $1}'`
23.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901

Job 的 spec

我們在定義一個資源的時候,除了明確的 apiVersionkind 兩個部分,剩下能自己發揮的就是 metadataspec,其中 metadata 主要是“名字”類的配置,影響行為的都在 spec 中,我們看下 Job 資源的 spec 支援哪些屬性配置。

Pod Template

.spec 中唯一必須配置的是 .spec.template,這個 template 是 pod template,也就是類似這樣的格式:

 1metadata:
2  name: hello
3spec:
4  template:
5    spec:
6      containers:
7      - name: hello
8        image: busybox
9        command: ['sh', '-c', 'echo "Hello, Kubernetes!" && sleep 3600']
10      restartPolicy: OnFailure

注意這整段是一個簡單的 pod template,我們通過 kubectl explain job.spec.template 命令可以清晰地看到這段配置和 job.spec 的關係:

 1# kubectl explain job.spec.template
2KIND:     Job
3VERSION:  batch/v1
4
5RESOURCE: template <Object>
6
7DESCRIPTION:
8     Describes the pod that will be created when executing a job. More info:
9     https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
10
11     PodTemplateSpec describes the data a pod should have when created from a
12     template
13
14FIELDS:
15   metadata    <Object>
16     Standard object's metadata. More info:
17     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
18
19   spec    <Object>
20     Specification of the desired behavior of the pod. More info:
21     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
22

也就是說 job.spec.template 下面儲存的是完整的 pod.metadatapod.spec,也就是包含一個 pod 能改變的所有屬性。

這裡有兩個注意點:

  1. 不要輕易設定 .spec.selector,我們在前面的例子中可以看到 Job 控制器會自動新增類似這樣的 selector:controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2,這個 selector 可以保證唯一性且已經滿足幾乎全部的場景需求。如果自己新增一個 selector 但是不小心引入了重名等問題,會導致 job 控制器發生操作到其他控制器管理的 pod 等不必要的錯誤。
  2. Pod 的 RestartPolicy 只能設定成 NeverOnFailure,這個也很明顯,要是設定成 Always 就死迴圈了。

併發問題

Job 從併發角度可以分成三類:

  1. 無併發
  2. 同一時間只啟動一個 Pod,這個 Pod 失敗了才會啟動另外一個新的 Pod;
  3. 有一個 Pod 成功結束時整個 Job 結束;
  4. 這時候 .spec.completions.spec.parallelism 不需要設定,也就是生效的預設值:1
  5. 指定完成數量
  6. 配置 .spec.completions 為正整數;
  7. 成功結束的 Pod 數量達到 completions 指定的數量時,Job 結束;
  8. 可以指定 .spec.completionMode=Indexed,這時候 Pod name 會有編號,從 0 開始;反之預設情況下,比如前面不指定時 Pod 名叫 pi--1-25l8f,我們併發建立多個也是 pi--1-xxxxx 的命名風格,中間的數字一直是1;另外這個 .spec.completionMode=Indexed 配置還有幾點行為是會給 pod 加上類似 batch.kubernetes.io/job-completion-index 格式的 annotation 和 JOB_COMPLETION_INDEX=job-completion-index 的環境變數;
  9. 配置了 .spec.completions 後,可以選擇性配置 .spec.parallelism 來控制併發度;比如 completions 為 10,parallelism 為 3,那麼 Job 控制器就會在達到 10 個 Pod 成功前,儘量保持併發度為 3 去拉起 Pod;
  10. 工作佇列
  11. 不指定 .spec.completions,配置 .spec.parallelism 為一個非負整數(配置為0相當於掛起);
  12. 可以通過 MQ 等方式管理工作佇列,每個 Pod 獨立工作,能夠判斷整個任務是否完成,如果一個 Pod 成功退出,則表示整個任務結束了,這時候不會再建立新的 Pod,整個 Job 也就結束了;

其他屬性

除了上面提到的 templatecompletionsparallelismcompletionModeselector 外,Job.spec 還有5個可以配置的屬性(不同版本有細微區別,我的環境是 v1.22.0):

  • activeDeadlineSeconds 指定這個 Job 的最長允許耗時,掛起狀態會暫停該計時器;
  • backoffLimit 重試次數,超過了之後 Job 被設定為 failed,預設值是 6,也就是 Pod 失敗後可以重試 6 次;
  • manualSelector 配置開啟自定義 selector 功能,絕大多數情況下不需要,也不要去配置;這是一個 bool 值,也就是設定為 true 後才能通過 selector 來覆蓋預設的行為;
  • suspend 配置掛起一個 Job,掛起操作會直接刪除所有執行中的 Pod,並且重置 Job 的 StartTime,暫停 ActiveDeadlineSeconds 計時器;
  • ttlSecondsAfterFinished 需要開啟 TTLAfterFinished 特性才能生效,在 alpha 階段;效果是當一個 Job 結束(成功或失敗)後,過了該引數指定的時間後,Job 會被清理掉。如果設定為0就是立刻清理,預設情況下 Job 執行結束後會保留在環境裡,直到手動刪除。

概述什麼是 JobJob 入門示例Job 的 specPod Template併發問題其他屬性

概述

Job 是主要的 Kubernetes 原生 Workload 資源之一,是在 Kubernetes 之上執行批處理任務最簡單的方式,在 AI 模型訓練等場景下最基礎的實現版本就是拉起一個 Job 來完成一次訓練任務,然後才是各種自定義 “Job” 實現進階處理,比如分散式訓練需要一個 “Job” 同時拉起多個 Pod,但是每個 Pod 的啟動引數會有差異。所以深入理解 Job 的功能和實現細節是進一步開發自定義 “Job” 型別工作負載的基礎。今天開始我們計劃分成幾講來從 Job 的特性介紹、原始碼分析等方面深度剖析 Job 資源和其背後的 Job 控制器。


《Kubernetes Job Controller 原理和原始碼分析》分為三講:

什麼是 Job

我們建立一個 Job 後,Job 控制器會拉起一個或多個 Pod 開始執行,直到成功退出的 Pod 達到了指定數量。Job 執行過程中可以被掛起,掛起操作會直接刪掉正在執行中的 Pod;另外 Job 被刪除時對應的 Pod 也會全部被刪掉,這個特性和其他控制器是一致的。

一個最常見的 Job 使用方式是通過 Job 拉起一個 Pod,確保這個 Pod 成功執行一次。如果唯一的 Pod 因為某種原因執行失敗了,這時候 Job 會重新拉起一個 Pod,繼續嘗試完成“成功一次”的目標。當然 Job 支援併發拉起多個 Pod。

Job 入門示例

借用官網計算圓周率的例子:

 1apiVersion: batch/v1
2kind: Job
3metadata:
4  name: pi
5spec:
6  template:
7    spec:
8      containers:
9      - name: pi
10        image: perl
11        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
12      restartPolicy: Never
13  backoffLimit: 4

Apply 這個 yaml 檔案就能床架一個 Job,看下相關資訊:

  • 剛建立時檢視 Job 狀態
1# kubectl get job
2NAME   COMPLETIONS   DURATION   AGE
3pi     0/1           3s         3s
  • 接著檢視對應的 pod 狀態
 1# kubectl get pod --selector=job-name=pi
2NAME          READY   STATUS              RESTARTS   AGE
3pi--1-25l8f   0/1     ContainerCreating   0          8s
4
5# kubectl get pod --selector=job-name=pi
6NAME          READY   STATUS    RESTARTS   AGE
7pi--1-25l8f   1/1     Running   0          12s
8
9# kubectl get pod --selector=job-name=pi
10NAME          READY   STATUS      RESTARTS   AGE
11pi--1-25l8f   0/1     Completed   0          21s
  • Describe 檢視 job 詳細資訊
 1kubectl describe job pi
2Name:             pi
3Namespace:        default
4Selector:         controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
5Labels:           controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
6                  job-name=pi
7Annotations:      <none>
8Parallelism:      1
9Completions:      1
10Completion Mode:  NonIndexed
11Start Time:       Mon, 18 Oct 2021 15:55:02 +0800
12Completed At:     Mon, 18 Oct 2021 15:55:23 +0800
13Duration:         21s
14Pods Statuses:    0 Running / 1 Succeeded / 0 Failed
15Pod Template:
16  Labels:  controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2
17           job-name=pi
18  Containers:
19   pi:
20    Image:      perl
21    Port:       <none>
22    Host Port:  <none>
23    Command:
24      perl
25      -Mbignum=bpi
26      -wle
27      print bpi(2000)
28    Environment:  <none>
29    Mounts:       <none>
30  Volumes:        <none>
31Events:
32  Type    Reason            Age    From            Message
33  ----    ------            ----   ----            -------
34  Normal  SuccessfulCreate  5m59s  job-controller  Created pod: pi--1-25l8f
35  Normal  Completed         5m39s  job-controller  Job completed

看這裡的輸出資訊時,我們帶一點“原始碼”的視角去思考一下,一行行看下來可以發現很多 Job controller 的工作痕跡,比如自動配置了 label、selector 等屬性,記錄了 Job 下 pods 的當前狀態,工作耗時,起止時間等等資訊。另外有兩個 Event,分別是 Pod 成功建立和 Job 完成結束時的事件。後面刷原始碼時這裡的所有邏輯我們都可以一一發現。

這時當然也可以通過日誌檢視圓周率計算結果:

  • 檢視計算結果
1# kubectl logs `kubectl get pod --selector=job-name=pi | grep -v NAME | awk '{print $1}'`
23.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901

Job 的 spec

我們在定義一個資源的時候,除了明確的 apiVersionkind 兩個部分,剩下能自己發揮的就是 metadataspec,其中 metadata 主要是“名字”類的配置,影響行為的都在 spec 中,我們看下 Job 資源的 spec 支援哪些屬性配置。

Pod Template

.spec 中唯一必須配置的是 .spec.template,這個 template 是 pod template,也就是類似這樣的格式:

 1metadata:
2  name: hello
3spec:
4  template:
5    spec:
6      containers:
7      - name: hello
8        image: busybox
9        command: ['sh', '-c', 'echo "Hello, Kubernetes!" && sleep 3600']
10      restartPolicy: OnFailure

注意這整段是一個簡單的 pod template,我們通過 kubectl explain job.spec.template 命令可以清晰地看到這段配置和 job.spec 的關係:

 1# kubectl explain job.spec.template
2KIND:     Job
3VERSION:  batch/v1
4
5RESOURCE: template <Object>
6
7DESCRIPTION:
8     Describes the pod that will be created when executing a job. More info:
9     https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
10
11     PodTemplateSpec describes the data a pod should have when created from a
12     template
13
14FIELDS:
15   metadata    <Object>
16     Standard object's metadata. More info:
17     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
18
19   spec    <Object>
20     Specification of the desired behavior of the pod. More info:
21     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
22

也就是說 job.spec.template 下面儲存的是完整的 pod.metadatapod.spec,也就是包含一個 pod 能改變的所有屬性。

這裡有兩個注意點:

  1. 不要輕易設定 .spec.selector,我們在前面的例子中可以看到 Job 控制器會自動新增類似這樣的 selector:controller-uid=47c8b54a-76e9-4259-b900-750df8a88dd2,這個 selector 可以保證唯一性且已經滿足幾乎全部的場景需求。如果自己新增一個 selector 但是不小心引入了重名等問題,會導致 job 控制器發生操作到其他控制器管理的 pod 等不必要的錯誤。
  2. Pod 的 RestartPolicy 只能設定成 NeverOnFailure,這個也很明顯,要是設定成 Always 就死迴圈了。

併發問題

Job 從併發角度可以分成三類:

  1. 無併發
  2. 同一時間只啟動一個 Pod,這個 Pod 失敗了才會啟動另外一個新的 Pod;
  3. 有一個 Pod 成功結束時整個 Job 結束;
  4. 這時候 .spec.completions.spec.parallelism 不需要設定,也就是生效的預設值:1
  5. 指定完成數量
  6. 配置 .spec.completions 為正整數;
  7. 成功結束的 Pod 數量達到 completions 指定的數量時,Job 結束;
  8. 可以指定 .spec.completionMode=Indexed,這時候 Pod name 會有編號,從 0 開始;反之預設情況下,比如前面不指定時 Pod 名叫 pi--1-25l8f,我們併發建立多個也是 pi--1-xxxxx 的命名風格,中間的數字一直是1;另外這個 .spec.completionMode=Indexed 配置還有幾點行為是會給 pod 加上類似 batch.kubernetes.io/job-completion-index 格式的 annotation 和 JOB_COMPLETION_INDEX=job-completion-index 的環境變數;
  9. 配置了 .spec.completions 後,可以選擇性配置 .spec.parallelism 來控制併發度;比如 completions 為 10,parallelism 為 3,那麼 Job 控制器就會在達到 10 個 Pod 成功前,儘量保持併發度為 3 去拉起 Pod;
  10. 工作佇列
  11. 不指定 .spec.completions,配置 .spec.parallelism 為一個非負整數(配置為0相當於掛起);
  12. 可以通過 MQ 等方式管理工作佇列,每個 Pod 獨立工作,能夠判斷整個任務是否完成,如果一個 Pod 成功退出,則表示整個任務結束了,這時候不會再建立新的 Pod,整個 Job 也就結束了;

其他屬性

除了上面提到的 templatecompletionsparallelismcompletionModeselector 外,Job.spec 還有5個可以配置的屬性(不同版本有細微區別,我的環境是 v1.22.0):

  • activeDeadlineSeconds 指定這個 Job 的最長允許耗時,掛起狀態會暫停該計時器;
  • backoffLimit 重試次數,超過了之後 Job 被設定為 failed,預設值是 6,也就是 Pod 失敗後可以重試 6 次;
  • manualSelector 配置開啟自定義 selector 功能,絕大多數情況下不需要,也不要去配置;這是一個 bool 值,也就是設定為 true 後才能通過 selector 來覆蓋預設的行為;
  • suspend 配置掛起一個 Job,掛起操作會直接刪除所有執行中的 Pod,並且重置 Job 的 StartTime,暫停 ActiveDeadlineSeconds 計時器;
  • ttlSecondsAfterFinished 需要開啟 TTLAfterFinished 特性才能生效,在 alpha 階段;效果是當一個 Job 結束(成功或失敗)後,過了該引數指定的時間後,Job 會被清理掉。如果設定為0就是立刻清理,預設情況下 Job 執行結束後會保留在環境裡,直到手動刪除。

相關文章