Kubernetes — Job與CronJob

有一類做業顯然不知足這樣的條件,這就是「離線業務」,或者叫做 Batch Job(計算業務)。shell

這 種業務在計算完成後就直接退出了,而此時若是你依然用 Deployment 來管理這種業務的話,就會 發現 Pod 會在計算結束後退出,而後被 Deployment Controller 不斷地重啓;而像「滾動更 新」這樣的編排功能,更無從談起了。 因此,早在 Borg 項目中,Google 就已經對做業進行了分類處理,提出了 LRS(Long Running Service)和 Batch Jobs 兩種做業形態,對它們進行「分別管理」和「混合調度」。ubuntu

不過,在 2015 年 Borg 論文剛剛發佈的時候,Kubernetes 項目並不支持對 Batch Job 的管理。直 到 v1.4 版本以後,社區才逐步設計出了一個用來描述離線業務的 API 對象,它的名字就是:Job。小程序

Job API 對象的定義很是簡單,我來舉個例子,以下所示:api

 

job.yaml併發

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: resouer/ubuntu-bc 
        command: ["sh", "-c", "echo 'scale=10000; 4*a(1)' | bc -l "]
      restartPolicy: Never
  backoffLimit: 4

 

此時,相信你對 Kubernetes 的 API 對象已經再也不陌生了。在這個 Job 的 YAML 文件裏,你確定一 眼就會看到一位「老熟人」:Pod 模板,即 spec.template 字段。app

在這個 Pod 模板中,我定義了一個 Ubuntu 鏡像的容器(準確地說,是一個安裝了 bc 命令的 Ubuntu 鏡像),它運行的程序是: 函數

echo "scale=10000; 4*a(1)" | bc -l 

其中,bc 命令是 Linux 裏的「計算器」;-l 表示,我如今要使用標準數學庫;而 a(1),則是調用數 學庫中的 arctangent 函數,計算 atan(1)。這是什麼意思呢?工具

中學知識告訴咱們:tan(π/4) = 1。因此,4*atan(1)正好就是π,也就是 3.1415926…。  ui

 

因此,這其實就是一個計算π值的容器。而經過 scale=10000,我指定了輸出的小數點後的位數是 10000。spa

在個人計算機上,這個計算大概用時 1 分 54 秒。 可是,跟其餘控制器不一樣的是,Job 對象並不要求你定義一個 spec.selector 來描述要控制哪些 Pod。具體緣由,我立刻會講解到。 如今,咱們就能夠建立這個 Job 了:

kubectl create -f job.yaml

  

在成功建立後,咱們來查看一下這個 Job 對象,以下所示:

$ kubectl describe jobs/pi
Name:             pi
Namespace:        default
Selector:         controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
Labels:           controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                  job-name=pi
Annotations:      <none>
Parallelism:      1
Completions:      1
..
Pods Statuses:    0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:       controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                job-name=pi
  Containers:
   ...
  Volumes:              <none>
Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason            Message
  ---------    --------    -----    ----            -------------    --------    ------            -------
  1m           1m          1        {job-controller }                Normal      SuccessfulCreate  Created pod: pi-rq5rl

  

能夠看到,這個 Job 對象在建立後,它的 Pod 模板,被自動加上了一個 controller-uid=< 一個隨 機字符串 > 這樣的 Label。

而這個 Job 對象自己,則被自動加上了這個 Label 對應的 Selector,從 而 保證了 Job 與它所管理的 Pod 之間的匹配關係。

而 Job Controller 之因此要使用這種攜帶了 UID 的 Label,就是爲了不不一樣 Job 對象所管理的 Pod 發生重合。須要注意的是,這種自動生成的 Label 對用戶來講並不友好,因此不太適合推廣到 Deployment 等長做業編排對象上。 接下來,咱們能夠看到這個 Job 建立的 Pod 進入了 Running 狀態,這意味着它正在計算 Pi 的值。

kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
pi-rq5rl                            1/1       Running   0          10s

  

而幾分鐘後計算結束,這個 Pod 就會進入 Completed 狀態:

kubectl get pods
NAME                                READY     STATUS      RESTARTS   AGE
pi-rq5rl                            0/1       Completed   0          4m

  

這也是咱們須要在 Pod 模板中定義 restartPolicy=Never 的緣由:離線計算的 Pod 永遠都不該該 被重啓,不然它們會再從新計算一遍。

此時,咱們經過 kubectl logs 查看一下這個 Pod 的日誌,就能夠看到計算獲得的 Pi 值已經被打印 了出來:

kubectl logs pi-rq5rl
3.141592653589793238462643383279...

  

這時候,你必定會想到這樣一個問題,若是這個離線做業失敗了要怎麼辦?

好比,咱們在這個例子中定義了 restartPolicy=Never,那麼離線做業失敗後 Job Controller 就 會不斷地嘗試建立一個新 Pod,以下所示:

kubectl get pods
NAME                                READY     STATUS              RESTARTS   AGE
pi-55h89                            0/1       ContainerCreating   0          2s
pi-tqbcz                            0/1       Error               0          5s

  

能夠看到,這時候會不斷地有新 Pod 被建立出來。 固然,這個嘗試確定不能無限進行下去。

因此,咱們就在 Job 對象的 spec.backoffLimit 字段裏定 義了重試次數爲 4(即,backoffLimit=4),而這個字段的默認值是 6。 須要注意的是,Job Controller 從新建立 Pod 的間隔是呈指數增長的,即下一次從新建立 Pod 的 動做會分別發生在 10 s、20 s、40 s …後。 而若是你定義的 restartPolicy=OnFailure,那麼離線做業失敗後,Job Controller 就不會去嘗試 建立新的 Pod。

可是,它會不斷地嘗試重啓 Pod 裏的容器。這也正好對應了 restartPolicy 的含義 (你也能夠藉此機會再回顧一下第 15 篇文章《深刻解析 Pod 對象(二):使用進階》中的相關內 容)。 如前所述,當一個 Job 的 Pod 運行結束後,它會進入 Completed 狀態。可是,若是這個 Pod 因 爲某種緣由一直不願結束呢? 在 Job 的 API 對象裏,有一個 spec.activeDeadlineSeconds 字段能夠設置最長運行時間,好比:

spec:
 backoffLimit: 5
 activeDeadlineSeconds: 100

  

一旦運行超過了 100 s,這個 Job 的全部 Pod 都會被終止。而且,你能夠在 Pod 的狀態裏看到終 止的緣由是 reason: DeadlineExceeded。

以上,就是一個 Job API 對象最主要的概念和用法了。不過,離線業務之因此被稱爲 Batch Job, 固然是由於它們能夠以「Batch」,也就是並行的方式去運行。

接下來,我就來爲你講解一下Job Controller 對並行做業的控制方法。 在 Job 對象中,負責並行控制的參數有兩個: 1. spec.parallelism,它定義的是一個 Job 在任意時間最多能夠啓動多少個 Pod 同時運行; 2. spec.completions,它定義的是 Job 至少要完成的 Pod 數目,即 Job 的最小完成數。

這兩個參數聽起來有點兒抽象,因此我準備了一個例子來幫助你理解。 如今,我在以前計算 Pi 值的 Job 裏,添加這兩個參數

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  parallelism: 2
  completions: 4
  template:
    spec:
      containers:
      - name: pi
        image: resouer/ubuntu-bc
        command: ["sh", "-c", "echo 'scale=5000; 4*a(1)' | bc -l "]
      restartPolicy: Never
  backoffLimit: 4

  

這樣,咱們就指定了這個 Job 最大的並行數是 2,而最小的完成數是 4。 接下來,咱們來建立這個 Job 對象:

kubectl create -f job.yaml

  

能夠看到,這個 Job 其實也維護了兩個狀態字段,即 DESIRED 和 SUCCESSFUL,以下所示:

kubectl get job
NAME      DESIRED   SUCCESSFUL   AGE
pi        4         0            3s

其中,DESIRED 的值,正是 completions 定義的最小完成數。 而後,咱們能夠看到,這個 Job 首先建立了兩個並行運行的 Pod 來計算 Pi:  

kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-5mt88   1/1       Running   0          6s
pi-gmcq5   1/1       Running   0          6s

  

而在 40 s 後,這兩個 Pod 相繼完成計算。 這時咱們能夠看到,每當有一個 Pod 完成計算進入 Completed 狀態時,就會有一個新的 Pod 被自 動建立出來,而且快速地從 Pending 狀態進入到 ContainerCreating 狀態:

kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-gmcq5   0/1       Completed   0         40s
pi-84ww8   0/1       Pending   0         0s
pi-5mt88   0/1       Completed   0         41s
pi-62rbt   0/1       Pending   0         0s

$ kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-gmcq5   0/1       Completed   0         40s
pi-84ww8   0/1       ContainerCreating   0         0s
pi-5mt88   0/1       Completed   0         41s
pi-62rbt   0/1       ContainerCreating   0         0s

  

緊接着,Job Controller 第二次建立出來的兩個並行的 Pod 也進入了 Running 狀態:

kubectl get pods 
NAME       READY     STATUS      RESTARTS   AGE
pi-5mt88   0/1       Completed   0          54s
pi-62rbt   1/1       Running     0          13s
pi-84ww8   1/1       Running     0          14s
pi-gmcq5   0/1       Completed   0          54s

最終,後面建立的這兩個 Pod 也完成了計算,進入了 Completed 狀態。 這時,因爲全部的 Pod 均已經成功退出,這個 Job 也就執行完了,因此你會看到它的 SUCCESSFUL 字段的值變成了 4:  

 

kubectl get pods 
NAME       READY     STATUS      RESTARTS   AGE
pi-5mt88   0/1       Completed   0          5m
pi-62rbt   0/1       Completed   0          4m
pi-84ww8   0/1       Completed   0          4m
pi-gmcq5   0/1       Completed   0          5m

$ kubectl get job
NAME      DESIRED   SUCCESSFUL   AGE
pi        4         4            5m

  

經過上述 Job 的 DESIRED 和 SUCCESSFUL 字段的關係,咱們就能夠很容易地理解Job Controller 的工做原理了。

首先,Job Controller 控制的對象,直接就是 Pod。 其次,Job Controller 在控制循環中進行的調諧(Reconcile)操做,是根據實際在 Running 狀態 Pod 的數目、已經成功退出的 Pod 的數目,以及 parallelism、completions 參數的值共同計算出 在這個週期裏,應該建立或者刪除的 Pod 數目,而後調用 Kubernetes API 來執行這個操做。 以建立 Pod 爲例。在上面計算 Pi 值的這個例子中,當 Job 一開始建立出來時,實際處於 Running 狀態的 Pod 數目 =0,已經成功退出的 Pod 數目 =0,而用戶定義的 completions,也就是最終用 戶須要的 Pod 數目 =4。

因此,在這個時刻,須要建立的 Pod 數目 = 最終須要的 Pod 數目 - 實際在 Running 狀態 Pod 數 目 - 已經成功退出的 Pod 數目 = 4 - 0 - 0= 4。也就是說,Job Controller 須要建立 4 個 Pod 來 糾正這個不一致狀態。 但是,咱們又定義了這個 Job 的 parallelism=2。也就是說,咱們規定了每次併發建立的 Pod 個數 不能超過 2 個。

因此,Job Controller 會對前面的計算結果作一個修正,修正後的指望建立的 Pod 數目應該是:2 個。

這時候,Job Controller 就會併發地向 kube-apiserver 發起兩個建立 Pod 的請求。 相似地,若是在此次調諧週期裏,Job Controller 發現實際在 Running 狀態的 Pod 數目,比 parallelism 還大,那麼它就會刪除一些 Pod,使二者相等。 綜上所述,Job Controller 實際上控制了,做業執行的並行度,以及總共須要完成的任務數這兩個 重要參數。而在實際使用時,你須要根據做業的特性,來決定並行度(parallelism)和任務數 (completions)的合理取值。

接下來,我再和你分享三種經常使用的、使用 Job 對象的方法。

外部管理器 +Job 模板

 

第一種用法,也是最簡單粗暴的用法:外部管理器 +Job 模板。 這種模式的特定用法是:把 Job 的 YAML 文件定義爲一個「模板」,而後用一個外部工具控制這 些「模板」來生成 Job。這時,Job 的定義方式以下所示:

 

apiVersion: batch/v1
kind: Job
metadata:
  name: process-item-$ITEM
  labels:
    jobgroup: jobexample
spec:
  template:
    metadata:
      name: jobexample
      labels:
        jobgroup: jobexample
    spec:
      containers:
      - name: c
        image: busybox
        command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
      restartPolicy: Never

  

能夠看到,咱們在這個 Job 的 YAML 裏,定義了 $ITEM 這樣的「變量」。

因此,在控制這種 Job 時,咱們只要注意以下兩個方面便可:

  • 1. 建立 Job 時,替換掉 $ITEM 這樣的變量;
  • 2. 全部來自於同一個模板的 Job,都有一個 jobgroup: jobexample 標籤,也就是說這一組 Job 使 用這樣一個相同的標識。

而作到第一點很是簡單。好比,你能夠經過這樣一句 shell 把 $ITEM 替換掉:

mkdir ./jobs
for i in apple banana cherry
do
  cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
done

 

這樣,一組來自於同一個模板的不一樣 Job 的 yaml 就生成了。接下來,你就能夠經過一句 kubectl create 指令建立這些 Job 了 

kubectl create -f ./jobs
kubectl get pods -l jobgroup=jobexample
NAME                        READY     STATUS      RESTARTS   AGE
process-item-apple-kixwv    0/1       Completed   0          4m
process-item-banana-wrsf7   0/1       Completed   0          4m
process-item-cherry-dnfu9   0/1       Completed   0          4m

  

這個模式看起來雖然很「傻」,但倒是 Kubernetes 社區裏使用 Job 的一個很廣泛的模式。

緣由很簡單:大多數用戶在須要管理 Batch Job 的時候,都已經有了一套本身的方案,須要作的往 往就是集成工做。這時候,Kubernetes 項目對這些方案來講最有價值的,就是 Job 這個 API 對 象。因此,你只須要編寫一個外部工具(等同於咱們這裏的 for 循環)來管理這些 Job 便可。 這種模式最典型的應用,就是 TensorFlow 社區的 KubeFlow 項目。 很容易理解,在這種模式下使用 Job 對象,completions 和 parallelism 這兩個字段都應該使用默 認值 1,而不該該由咱們自行設置。而做業 Pod 的並行控制,應該徹底交由外部工具來進行管理 (好比,KubeFlow)。

第二種用法:擁有固定任務數目的並行 Job

這種模式下,我只關心最後是否有指定數目(spec.completions)個任務成功退出。至於執行時的 並行度是多少,我並不關心。 好比,咱們這個計算 Pi 值的例子,就是這樣一個典型的、擁有固定任務數目(completions=4)的 應用場景。 它的 parallelism 值是 2;或者,你能夠乾脆不指定 parallelism,直接使用默認的並行 度(即:1)。 此外,你還可使用一個工做隊列(Work Queue)進行任務分發。這時,Job 的 YAML 文件定義 以下所示:

 

apiVersion: batch/v1
kind: Job
metadata:
  name: job-wq-1
spec:
  completions: 8
  parallelism: 2
  template:
    metadata:
      name: job-wq-1
    spec:
      containers:
      - name: c
        image: myrepo/job-wq-1
        env:
        - name: BROKER_URL
          value: amqp://guest:guest@rabbitmq-service:5672
        - name: QUEUE
          value: job1
      restartPolicy: OnFailure

  

咱們能夠看到,它的 completions 的值是:8,這意味着咱們總共要處理的任務數目是 8 個。也就 是說,總共會有 8 個任務會被逐一放入工做隊列裏(你能夠運行一個外部小程序做爲生產者,來提 交任務)。

在這個實例中,我選擇充當工做隊列的是一個運行在 Kubernetes 裏的 RabbitMQ。

因此,咱們需 要在 Pod 模板裏定義 BROKER_URL,來做爲消費者。 因此,一旦你用 kubectl create 建立了這個 Job,它就會以併發度爲 2 的方式,每兩個 Pod 一 組,建立出 8 個 Pod。每一個 Pod 都會去鏈接 BROKER_URL,從 RabbitMQ 裏讀取任務,而後各 自進行處理。這個 Pod 裏的執行邏輯,咱們能夠用這樣一段僞代碼來表示:

 

/* job-wq-1 的僞代碼 */
queue := newQueue($BROKER_URL, $QUEUE)
task := queue.Pop()
process(task)
exit

  

能夠看到,每一個 Pod 只須要將任務信息讀取出來,處理完成,而後退出便可。

而做爲用戶,我只關 心最終一共有 8 個計算任務啓動而且退出,只要這個目標達到,我就認爲整個 Job 處理完成了。

所 以說,這種用法,對應的就是「任務總數固定」的場景。

指定並行度

 

第三種用法,也是很經常使用的一個用法:指定並行度(parallelism),但不設置固定的 completions 的值。 此時,你就必須本身想辦法,來決定何時啓動新 Pod,何時 Job 纔算執行完成。在這種情 況下,任務的總數是未知的,因此你不只須要一個工做隊列來負責任務分發,還須要可以判斷工做 隊列已經爲空(即:全部的工做已經結束了)。 這時候,Job 的定義基本上沒變化,只不過是再也不須要定義 completions 的值了而已:

 

apiVersion: batch/v1
kind: Job
metadata:
  name: job-wq-2
spec:
  parallelism: 2
  template:
    metadata:
      name: job-wq-2
    spec:
      containers:
      - name: c
        image: gcr.io/myproject/job-wq-2
        env:
        - name: BROKER_URL
          value: amqp://guest:guest@rabbitmq-service:5672
        - name: QUEUE
          value: job2
      restartPolicy: OnFailure

  

而對應的 Pod 的邏輯會稍微複雜一些,我能夠用這樣一段僞代碼來描述:

/* job-wq-2 的僞代碼 */
for !queue.IsEmpty($BROKER_URL, $QUEUE) {
  task := queue.Pop()
  process(task)
}
print("Queue empty, exiting")
exit

  

因爲任務數目的總數不固定,因此每個 Pod 必須可以知道,本身何時能夠退出。

好比,在這 個例子中,我簡單地以「隊列爲空」,做爲任務所有完成的標誌。因此說,這種用法,對應的 是「任務總數不固定」的場景。 不過,在實際的應用中,你須要處理的條件每每會很是複雜。好比,任務完成後的輸出、每一個任務 Pod 之間是否是有資源的競爭和協同等等。 因此,在今天這篇文章中,我就再也不展開 Job 的用法了。

由於,在實際場景裏,要麼乾脆就用第一 種用法來本身管理做業;要麼,這些任務 Pod 之間的關係就不那麼「單純」,甚至仍是「有狀態應 用」(好比,任務的輸入 / 輸出是在持久化數據卷裏)。在這種狀況下,我在後面要重點講解的 Operator,加上 Job 對象一塊兒,可能才能更好的知足實際離線任務的編排需求。 最後,我再來和你分享一個很是有用的 Job 對象,叫做:CronJob。 顧名思義,CronJob 描述的,正是定時任務。它的 API 對象,以下所示:

 

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

  

在這個 YAML 文件中,最重要的關鍵詞就是jobTemplate。看到它,你必定恍然大悟,原來 CronJob 是一個 Job 對象的控制器(Controller)! 沒錯,CronJob 與 Job 的關係,正如同 Deployment 與 Pod 的關係同樣。

CronJob 是一個專門用 來管理 Job 對象的控制器。只不過,它建立和刪除 Job 的依據,是 schedule 字段定義的、一個標 準的Unix Cron格式的表達式。 好比,"*/1 * * * *"。 這個 Cron 表達式裏 */1 中的 * 表示從 0 開始,/ 表示「每」,1 表示偏移量。因此,它的意思就 是:從 0 開始,每 1 個時間單位執行一次。

那麼,時間單位又是什麼呢? Cron 表達式中的五個部分分別表明:分鐘、小時、日、月、星期。 因此,上面這句 Cron 表達式的意思是:從當前開始,每分鐘執行一次。 而這裏要執行的內容,就是 jobTemplate 定義的 Job 了。 因此,這個 CronJob 對象在建立 1 分鐘後,就會有一個 Job 產生了,以下所示:

 

kubectl create -f ./cronjob.yaml
cronjob "hello" created

# 一分鐘後
kubectl get jobs
NAME               DESIRED   SUCCESSFUL   AGE
hello-4111706356   1         1         2s

  

此時,CronJob 對象會記錄下此次 Job 執行的時間:

 

kubectl get cronjob hello
NAME      SCHEDULE      SUSPEND   ACTIVE    LAST-SCHEDULE
hello     */1 * * * *   False     0         Thu, 6 Sep 2018 14:34:00 -070

  

須要注意的是,因爲定時任務的特殊性,極可能某個 Job 尚未執行完,另一個新 Job 就產生 了。這時候,你能夠經過 spec.concurrencyPolicy 字段來定義具體的處理策略。

好比:

  • 1. concurrencyPolicy=Allow,這也是默認狀況,這意味着這些 Job 能夠同時存在;
  • 2. concurrencyPolicy=Forbid,這意味着不會建立新的 Pod,該建立週期被跳過;
  • 3. concurrencyPolicy=Replace,這意味着新產生的 Job 會替換舊的、沒有執行完的 Job。

而若是某一次 Job 建立失敗,此次建立就會被標記爲「miss」。當在指定的時間窗口內,miss 的數 目達到 100 時,那麼 CronJob 會中止再建立這個 Job。

這個時間窗口,能夠由 spec.startingDeadlineSeconds 字段指定。好比 startingDeadlineSeconds=200,意味着在過去 200 s 裏,若是 miss 的數目達到了 100 次,那麼 這個 Job 就不會被建立執行了。 總結 在今天這篇文章中,我主要和你分享了 Job 這個離線業務的編排方法,講解了 completions 和 parallelism 字段的含義,以及 Job Controller 的執行原理。

緊接着,我經過實例和你分享了 Job 對象三種常見的使用方法。可是,根據我在社區和生產環境中 的經驗,大多數狀況下用戶仍是更傾向於本身控制 Job 對象。因此,相比於這些固定的「模式」, 掌握 Job 的 API 對象,和它各個字段的準確含義會更加劇要。

相關文章
相關標籤/搜索