最近在公司的數據同步項目(以下簡稱 ZDTP)中,需要使用到分布式調度數據同步執行單元,目前使用的方案是將數據同步執行單元打包成鏡像,使用 K8s 進行調度。
在 ZDTP 中,數據同步的動作可抽象成一個執行單元(以下稱為 worker),類似于線程執行單元 Runnable ,Runnable 放入一個隊列中等待線程的調度執行,執行完 Runnable 即完成了它的使命。當用戶在 ZDTP 控制臺中創建同步任務并啟動任務時,會根據同步任務的配置,產生若干個用于該任務的 worker,假設這些 worker 都在本地執行,可以將其包裝成一個 Runnable,然后創建一個線程執行,如下圖表示:
但是在單機模式下,就會遇到性能瓶頸,此時就需要分布式調度,將 worker 調度到其他機器執行:
問題是我們如何將 worker 更好地調度到其它機器中執行呢?
Worker 部署方式調研
1、基于虛擬機部署 Worker
Worker 在提前創建好的虛擬機中運行, 任務啟動時需要根據當前 Worker 負載情況進行選擇空閑的 Worker,相當于 Worker 是以 Agent 的形式運行,如下圖表示:
伴隨而來的缺點主要有以下幾點:
- Worker Agent 數量相對固定,虛擬機創建成本高,擴/縮容麻煩;
- 任務運行情況依賴 zk 監聽機制,如果某個任務在運行中掛掉了,需要自行實現故障轉移與自動重啟機制,增加開發周期;
- Worker Agent 負載獲取邏輯需要項目實現,精確獲取負載信息實現難度大,增加開發周期。
2、基于 K8s 部署 Worker
將 Worker 打包成 Docker 鏡像,使用 K8s 對 worker 容器進行調度作業,并且一個 Worker 只運行一個任務,如下圖表示:
使用 K8s 的優點如下:
- 使用 K8s 集群調度的 Worker 容器具備故障恢復功能,只要將 Pod 的重啟策略設置為 restartPolicy=Always,無論 Worker 容器在運行過程中發生什么異常,K8s 都會自動嘗試重啟 Worker 容器,大大減少了運維成本,提高了數據同步的高可用性;
- 自動實現負載,比如當某個節點負載高,就會將 Worker 容器調度到負載低的節點上,更重要的是,某個節點宕機,其上的工作負載會被 K8s 自動將其轉移到其它節點上面;
- 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。