使用 K8s 進行作業排程實戰分享

後端進階發表於2020-08-27

最近在公司的資料同步專案(以下簡稱 ZDTP)中,需要使用到分散式排程資料同步執行單元,目前使用的方案是將資料同步執行單元打包成映象,使用 K8s 進行排程。

在 ZDTP 中,資料同步的動作可抽象成一個執行單元(以下稱為 worker),類似於執行緒執行單元 Runnable ,Runnable 放入一個佇列中等待執行緒的排程執行,執行完 Runnable 即完成了它的使命。當使用者在 ZDTP 控制檯中建立同步任務並啟動任務時,會根據同步任務的配置,產生若干個用於該任務的 worker,假設這些 worker 都在本地執行,可以將其包裝成一個 Runnable,然後建立一個執行緒執行,如下圖表示:

但是在單機模式下,就會遇到效能瓶頸,此時就需要分散式排程,將 worker 排程到其他機器執行:

問題是我們如何將 worker 更好地排程到其它機器中執行呢?

Worker 部署方式調研

1、基於虛擬機器部署 Worker

Worker 在提前建立好的虛擬機器中執行, 任務啟動時需要根據當前 Worker 負載情況進行選擇空閒的 Worker,相當於 Worker 是以 Agent 的形式執行,如下圖表示:

伴隨而來的缺點主要有以下幾點:

  1. Worker Agent 數量相對固定,虛擬機器建立成本高,擴/縮容麻煩;
  2. 任務執行情況依賴 zk 監聽機制,如果某個任務在執行中掛掉了,需要自行實現故障轉移與自動重啟機制,增加開發週期;
  3. Worker Agent 負載獲取邏輯需要專案實現,精確獲取負載資訊實現難度大,增加開發週期。

2、基於 K8s 部署 Worker

將 Worker 打包成 Docker 映象,使用 K8s 對 worker 容器進行排程作業,並且一個 Worker 只執行一個任務,如下圖表示:

使用 K8s 的優點如下:

  1. 使用 K8s 叢集排程的 Worker 容器具備故障恢復功能,只要將 Pod 的重啟策略設定為 restartPolicy=Always,無論 Worker 容器在執行過程中發生什麼異常,K8s 都會自動嘗試重啟 Worker 容器,大大減少了運維成本,提高了資料同步的高可用性;
  2. 自動實現負載,比如當某個節點負載高,就會將 Worker 容器排程到負載低的節點上,更重要的是,某個節點當機,其上的工作負載會被 K8s 自動將其轉移到其它節點上面;
  3. Worker 的生命週期完全交由 K8s 叢集管理,只需呼叫相關介面即可清楚 Worker 執行情況,大大減少開發週期。

K8s 容器排程方案調研

K8s 叢集的排程物件是 Pod,排程方式有多種,這裡主要介紹以下幾種方式:

1、Deployment(全自動排程)

在講 Deployment 前,先來說下 Replica Set,它是 K8s 一個非常重要的概念,它是在 Pod 這個抽象上更為上層的一個抽象,一般大家用 Deployment 這個抽象來做應用的真正的管理,而 Pod 是組成 Deployment 最小的單元。它可以定義某種 Pod(比如包裝了 ZDTP Worker 容器的 Pod)在任意時刻都保持符合 Replica Set 設定的預期值, 比如 Replica Set 可預期設定 Pod 副本數,當 k8s 叢集定期巡檢發現某種 Pod 的副本數少於 Replica Set 設定的預期值,它就會按照 Replica Set 設定的 Pod 模版建立 Pod 例項,使得 Pod 的數量維持在預期值,也是通過 Replica Set 的特性,實現了叢集的高可用性,同時減少了運維成本。

Deployment 內部使用了 Replica Set 來實現,他們之間高度相似,也可以將 Deployment 看作是 Replica Set 的升級版本。

2、Job(批處理排程)

我們可以通過 k8s Job 資源物件定義並啟動一個批處理任務,並行或者序列處理一批工作項(Work item),處理完成後任務就結束。

1)Job Template Expansion 模式

根據 ZDTP Worker 執行方式,我們可以使用一個 Job 對像對應一個 Worker,有多少個 worker 就建立多少個 Job,除非 Pod 異常,才會重啟該 Pod,正常執行完後 Job 就退出,如下圖表示:

2)Queue with Pod Per Work Item 模式

這種模式將客戶端生成的 worker 存放在一個佇列中,然後只會建立一個 job 去消費佇列中的 worker item,通過設定 parallelism 引數可以同時啟動多少個 worker Pod 同時處理 worker,值得一體的是,這種模式下的 Worker 處理程式邏輯只會從佇列拉取 worker 處理,處理完就立即退出,completions 引數用於控制正常退出的 Pod 數量,當退出的 Pod 數量達到了 completions 後,Job 結束,即 completions 引數可以控制 Job 的處理 Worker 的數量。如下圖所示:

3)Queue with Variable Pod Count 模式

這種排程模式看起來跟 Queue with Pod Per Work Item 模式差不多,其實不然,Queue with Variable Pod Count 模式的 Job 只要有一個 Pod 正常退出,即說明 Job 已經處理完資料,處於終止狀態了,因為它的每個 Pod 都有查詢佇列是否還有 worker 的邏輯,一旦發現佇列中沒有了 worker,Pod 正常退出,因此 Queue with Variable Pod Count 模式 completions 引數只能設定 1, parallelism 引數可以同時啟動多少個 worker Pod 同時處理 worker。

這種模式也要求佇列能夠讓 Pod 感知是否還存在 worker,像 RocketMQ/Kafka 之類的訊息中介軟體並不能做到,只會讓客戶端一直等待,因此這種模式不能選用 RocketMQ/Kafka,可以選擇資料庫或者 Redis 來實現。如下圖所示:

當然如果後面還有定時執行 Worker 的需求,使用 K8s 的 cronjob(定時任務排程)是一個非常好的選擇。

3、Pod(預設排程)

直接通過 kind=pod 的方式啟動容器,這種方式不能設定容器的執行例項數,即 replicas = 1,通常生產應用叢集都不會通過這個方式啟動容器,因為這種方式啟動容器不具備 Pod 自動擴縮容的特性。

值得一提的是,即使你的 Pod 副本只有 1 個,官方也推薦使用 Replica Set 的方式進行部署。

Pod 重啟策略分析

Pod 的重啟策略包括 Always、onFailure、Never:

  • Always:當容器失效時,k8s 自動重啟該容器;
  • onFailure:當容器終止執行時並且退出碼不為 0 時,k8s 自動重啟該容器;
  • Never:不論容器執行狀態如何,k8s 都不會重啟該容器

Deployment/Replica Set 必須設定為 Always(因為它們都需要保持 Pod 期待的副本數),而 Job 只能設定為 onFailure 和 Never,以確保容器執行完成後不再重啟,直接 Pod 啟動容器以上三個重啟策略都可以設定。

這裡需要說明一點,如果使用 Job,情況可能稍微複雜些:

1)Pod 重啟策略 RestartPolicy=Never

假設 Job 排程過程中 Pod 發生非正常退出,儘管此時容器不再重啟,由於 Job 需要至少一個 Pod 執行完成(即 completions 最少等於 1),Job 才算完成。因此,雖然非正常退出的 Pod 不再重啟,但 Job 會嘗試重新啟動一個 Pod 執行,直到 Pod 正常完成的數量為 completions。

$ kubectl get pod --namespace zdtp-namespace

NAME                   READY   STATUS               RESTARTS   AGE
zdtp-worker-hc6ld      0/1     ContainerCannotRun   0          64s
zdtp-worker-hfblk      0/1     ContainerCannotRun   0          60s
zdtp-worker-t9f6v      0/1     ContainerCreating    0          11s
zdtp-worker-v2g7s      0/1     ContainerCannotRun   0          31s

2)Pod 重啟策略 RestartPolicy=onFailure

當 RestartPolicy=onFailure,Pod 發生非正常退出時,Pod 會嘗試重啟,直到該 Pod 正常執行完成,此時 Job 就不會重新啟動一個 Pod 執行了,如下:

$ kubectl get pod --namespace zdtp-namespace

NAME                READY   STATUS             RESTARTS   AGE
zdtp-worker-5tbxw   0/1     CrashLoopBackOff   5          67s

如何選擇 K8s 排程策略?

以上內容把 K8s 的排程方案與 Pod 的重啟策略都研究了一番後,接下來就需要針對專案的排程需求選擇合適的排程方式。

1、增量同步 Worker

增量同步 Worker 會一直同步下去,中途不停止,這意味著 Pod 的重啟策略必須為 RestartPolicy=Always,那麼這種方式只能選擇 Deployment 排程或者直接建立 Pod 部署,但建議使用 Deployment,官方已經說明了即使 Pod 副本為 1,依然建議使用 Deployment 進行部署。

2、 全量同步 Worker

全量同步 Worker 在資料同步完就退出,看起來 Job 排程或者直接建立 Pod 部署都可以滿足,但現階段由於全量同步暫時沒有記錄同步進度,因此要求中途發生任何錯誤容器退出後都不能自動重啟,目前的做法是當 Worker 執行過程中發生非正常退出時,需要使用者自行刪除已同步的資源,再手動啟動 Worker 再次進行全量同步。

因此,Job 目前還還不適合排程 Worker Pod,全量同步 Worker 現階段只適合直接使用 Pod 進行部署,且需要設定 Pod 重啟策略 RestartPolicy=Never。

作者簡介

作者張乘輝,擅長訊息中介軟體技能,負責公司百萬 TPS 級別 Kafka 叢集的維護,作者維護的公號「後端進階」不定期分享 Kafka、RocketMQ 系列不講概念直接真刀真槍的實戰總結以及細節上的原始碼分析;同時作者也是阿里開源分散式事務框架 Seata Contributor,因此也會分享關於 Seata 的相關知識;當然公號也會分享 WEB 相關知識比如 Spring 全家桶等。內容不一定面面俱到,但一定讓你感受到作者對於技術的追求是認真的!

公眾號:後端進階

技術部落格:https://objcoding.com/

GitHub:https://github.com/objcoding/

公眾號「後端進階」,專注後端技術分享!

相關文章