Deployment、StatefulSet和DaemonSet這三個編排概念編排的對象主要都是在線業務(Long Running Task,這些應用一旦運行起來,除非出錯或者停止,它的容器進程會一直保持在Running狀態)。
但是對於離線業務(Batch Job,計算業務)在計算完成后就直接退出了,如果依然使用Deployment來管理,就會發現Pod會在計算結束后退出,然后被Deployment Controller不斷重啟。
在Kubernetes v1.4版本之后,設計出來一個用來描述離線業務的API對象:Job。
舉個Job的栗子
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: pi image: resouer/ubuntu-bc #安裝了ba明了的Ubuntu鏡像 command: ["sh", "-c", "echo 'scale=10000; 4*a(1)' | bc -l "] #bc命令是Linux里的計算器,; -l表示使用標准數學課; a(1)調用arctangent函數,scale指定小數點后的位數 restartPolicy: Never backoffLimit: 4
創建這個Pod
$ kubectl create -f job.yaml $ 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模板被自動加上了一個Labels, controller-uid=<一個隨機的字符串>。而Job對象本身,則被自動加上了這個Label對應的Selector,從而保證了Job與它所管理的Pod之間的匹配關系。
Job Controller之所以要使用這種攜帶了UID的Label,就是為了避免不同Job對象管理的Pod重合
### 接下來可以看到這個Job創建的Pod進入了Running狀態,這意味着它正在計算Pi的值 $ kubectl get pods NAME READY STATUS RESTARTS AGE pi-rq5rl 1/1 Running 0 10s ###計算結束后,Pod會進入Completed狀態,這也是需要在Pod模板中定義restartPolicy=Never的原因:離線計算的Pod永遠不應該被重啟 $ kubectl get pods NAME READY STATUS RESTARTS AGE pi-rq5rl 0/1 Completed 0 4m
### 通過查看log可以看到計算的值
$ kubectl logs pi-rq5rl
3.141592653589793238462643383279...
那如果這個離線作業失敗了怎么辦?因為這個例子中定義了restartPolicy=Never,那離線作業失敗后Job Controller就會不斷地嘗試創建一個新Pod
Job對象的spec.backoffLimit字段限制嘗試的次數,在例子中定義為4,默認值為6。Job Controller重新創建Pod的間隔是呈倍數增加的,即下一次重新創建Pod的動作會分別發生在10s,20s,40s……
那如果定義的restartPolicy=OnFailure呢?離線作業失敗后,Job Controller就不會去嘗試創建新的Pod,但是它會不斷嘗試重啟Pod里的容器。
那如果這個Pod一直不肯結束呢?spec.activeDeadlineSeconds字段可以設置最長運行時間
spec: backoffLimit: 5 activeDeadlineSeconds: 100 # 運行超過100s,這個Job的所有Pod都會被終止
Job Controller 對並行作業的控制方法
在Job對象中,負責並行控制的參數有兩個:
spec.parallelism:定義一個Job在任意時間最多可以啟動多少個Pod同時運行
spec.completions:定義Job至少要玩的Pod的數目
再舉個栗子
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
指定了最大並行數是2,最小完成數是4
### 創建Job對象 $ kubectl create -f job.yaml ### Job維護兩個字段,DESIRED即最小完成數 $ kubectl get job NAME DESIRED SUCCESSFUL AGE pi 4 0 3s ### 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 ### 當一個Pod完成計算會進入Completed狀態,就會有一個新的Pod被創建出來,並且快速地從Pending狀態進入ContainerCreating狀態,再到Running狀態,最后完成 $ 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 $ 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 $ 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 ### 所有Pod均已成功退出,SUCCESSFUL字段值變成4 $ kubectl get job NAME DESIRED SUCCESSFUL AGE pi 4 4 5m
通過上述DESIRED和SUCCESSFUL字段的關系,可以看出Job Controller控制的直接對象是Pod,Job Controller在控制循環中進行的調諧(Reconcile)操作,是根據實際在Running狀態Pod數目、已經成功推出的Pod數目,以及parallelism、completions參數的值共同計算在這個周期里,應該創建或刪除的Pod數目,然后調用Kubernetes API來執行這個操作
Job對象使用方法
1、外部管理器 + 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"] ### 定義了$ITEM變量 restartPolicy: Never
在控制這種Job時,只要注意如下兩個方面即可
-
- 創建Job時,替換掉$ITEM這樣的變量
- 所有來自同一個目標的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 $ 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
這種模式雖然看起很傻,但卻很普遍,因為大多數用戶在需要管理Batch Job時,都已經有自家的一套方案,需要做的往往就是集成工作。這時候Kubernetes對這些方案最有價值的就是Job這個API對象,因此只需要編寫一個外部工具(如上面for循環)來管理這些Job即可。
這種模式下使用Job對象,completions和parallelism這兩個字段都應該使用默認值1,作業Pod的並行控制,應該完全交由外部工具來進行管理
2、擁有固定任務數目的並行Job
只關心最后是否有指定數目(spec.completions)個任務成功推出,不關心執行時的並行度多少。
比如上面計算pi的例子,或者可以不指定parallelism
3、指定並行度(parallelism),不設置固定的completions的值
任務總數未知,需要決定什么時候啟動Pod,什么時候Job才算執行完成。因此需要一個工作隊列來負責任務分發,還需要能夠判斷工作隊列已經為空(所有工作已經結束)
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 ##工作隊列采用RabbitMQ - 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
CronJob
CronJob描述的是定時任務,舉個栗子
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,即一個Job對象的控制器(Controller)。如Deployment和Pod的關系一樣,CronJob是一個專門用來管理Job對象的控制器。
CronJob創建和刪除Job的依據是schedule字段定義的、一個標准的Unix Cron格式的表達式
如,“*/1****”,這個Cron表達式里*/1中的*表示從0開始,/表示每,1表示偏移量,它的意思就是從零開始,每1個時間單位執行一次
那時間單位又是什么意思?Cron表達式中的五個部分分別代表:分鍾、小時、日、月、星期。因此上面的意思就是每分鍾執行一次。
$ 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字段來定義具體的處理策略
-
- concurrencyPolicy=Allow:默認情況,Job可以同時存在;
- concurrencyPolicy=Forbid:不會創建新的Jod,該創建周期被跳過
- concurrencyPolicy=Replace:新產生的Job會替換舊的、沒有執行完的Job
如果一個Job創建失敗,這次創建就會被標記為“miss”,當在指定的時間窗口內,miss的數目達到100時,那么CronJob會停止創建這個Job。這個時間窗口可以由spec.startingDeadlineSeconds字段指定。
例如,startingDeadlineSeconds=200,意味着在過去的200s里,如果miss的數目到達了100次,那么這個Job就不會被創建執行了。