博客 / 詳情

返回

Spark on K8s 在vivo大數據平台的混部實戰

作者:vivo 互聯網大數據團隊- Qin Yehai

在離線混部可以提高整體的資源利用率,不過離線Spark任務部署到混部容器集羣需要做一定的改造,本文將從在離線混部中的離線任務的角度,講述離線任務是如何進行容器化、平台上的離線任務如何平滑地提交到混部集羣、離線任務在混部集羣中如何調度的完整實現以及過程中的問題解決。

一、在離線業務差異

互聯網數據業務服務一般可以分為在線服務和離線任務兩大類,在線服務是指那些長時間運行、隨時響應對實時性要求高、負載壓力隨着接收流量起伏的服務,如電商、遊戲等服務,離線任務是指運行週期短、可執行時間提交對實時性要求低、有一定容錯性、負載壓力基本可控的服務,如離線計算任務、模型訓練等。一般在線服務在白天時段繁忙,離線任務在凌晨繁忙,兩者的業務高峯期存在錯峯現象,如果按傳統方式在線和離線都是分別獨立機器部署,業務高峯時期需要更多機器來支持,業務低峯期又存在部分機器空閒,整體資源利用率都不高。因此行業提出來在離線混部的解決方案,在線和離線業務通過混部系統部署在同一批機器,實現共享資源並錯峯互補,提高整體的資源利用率。目前業內利用混部技術可以將數據中心的CPU利用率提升至40%左右,vivo在2023年混部平台投入生產也已經將部分混部集羣的CPU利用率提升至30%左右,整體收益也是可觀的。

混部系統需要有強大的隔離能力,絕大部分都是基於容器,所以混部的前提是在線和離線業務都容器化,對於容器管理工具如K8s來説是更適應於運行時間長、啓停次數少、容器數量少的在線服務,在線服務也能比較容易地上容器,而對於運行時間短、啓停頻繁、容器數量大的離線任務,對K8s來説不是天然地適應,但容器化已是大勢所趨,K8s也推出了性能更好的調度器、用於離線任務的控制器,Spark在2.3版本後也支持容器化,諸多技術的發展也推動離線任務實現容器化以及在離線混部的落地。

本文將從在離線混部中的離線任務的角度,講述離線任務是如何進行容器化、平台上的離線任務如何平滑地提交到混部集羣、離線任務在混部集羣中如何調度的完整實現以及過程中的問題解決。

二、離線任務容器化

2.1 Spark Operator 方案

2.1.1 方案對比

vivo離線任務大部分任務是以Spark作為執行引擎,Spark任務運行在K8s上,目前業界有兩種架構的方案:Spark on K8s及Yarn on K8s。兩者部分優缺點對比如下:

圖片

Spark on K8s是Spark容器化,由K8s直接創建Driver和Executor的Pod來運行Spark作業,Yarn on K8s是Yarn的容器化,由K8s創建RM和NM的Pod,Spark的Driver和Executor運行在NM Pod的container中,正是由於兩種架構方案的區別,它們各自也會存在優缺點。

Yarn on K8s方案可以支持原生的Hive、Spark、Flink等引擎,它僅需要創建一定數量的NodeManager Pod來滿足作業需求,Pod運行相對穩定因此對K8s的壓力比較小,本身Yarn支持調度性能和調度策略也是專門為離線任務設計的,調度性能比K8s的強很多。由於NodeManager ESS服務是對磁盤有容量和讀寫性能要求的,混部機器的磁盤一般難以滿足,所以也需要能支持不同引擎的Remote Shuffle Service。在資源利用上,NodeManager需要滿足多個作業的資源,最小單位是Container,Pod的資源粒度比較大,自身也會佔用一些資源,如果資源粒度得不到有效地彈性伸縮,也會造成資源的浪費,因此需要引入額外的組件來協調,根據Kubernetes集羣節點的剩餘資源,動態調整NodeManager的CPU和內存,然而這也需要一定的改造成本。在資源緊張的情況下,NodeManager Pod如果被驅逐也就意味着整個NodeManager被銷燬,將會影響多個任務。

Spark on K8s方案目前在Spark 3.1以上版本才正式可用,它需要頻繁的創建、查詢、銷燬大量的Executor Pod,對K8s的ApiServer和ETCD等組件都會造成比較大的壓力,K8s的調度器也不是專門為離線的大批量任務設計的,調度性能也比較弱。另一方面,Spark on K8s雖然只能支持Spark3.X的RSS,不過目前有較多的開源產品可選擇。在資源利用上,最小單位是Driver和Executor的Pod,資源粒度小,可以填充到更多的碎片資源,調度時直接與K8s對接,資源的彈性調度更多由K8s來承擔,不需要額外的組件,改造成本比較低。在資源緊張的情況下,Executor、Driver的Pod將依次逐個被驅逐,任務的穩定性會更高。

而對於Spark on K8s方案,還細分2種實現方案:Spark Submit on K8s和Spark Operator on K8s。

圖片

SparkOnK8s架構圖

(圖片來源:Spark官網)

圖片

Spark Operator架構圖

(圖片來源:Spark Operator開源項目)

以spark-submit方式提交到K8s集羣是Spark在2.3版本後提供的原生功能,客户端通過spark-submit設置K8s的相關參數,內部再調用K8sApi在K8s集羣中創建Driver Pod,Driver再調用K8sApi創建需要的Executor Pod,共同組成Spark Application,作業結束後Executor Pod會被Driver Pod銷燬,而Driver Pod則繼續存在直到被清理。使用spark-submit方式的最大好處是由spark-submit來與K8s的進行交換,提交作業的方式幾乎保持一致。但是因為使用的便利性所需要的封裝也會帶來一些缺點,spark-submit是通過K8sApi創建Pod,使用非聲明式的提交接口,如果需要修改K8s配置就需要重新開發新接口,二次開發複雜繁瑣,雖然Spark提供了大量的K8s配置參數,但也遠比不了K8s YAML的聲明式的提交方式更加靈活,而且Spark Application和K8s Workload的生命週期還不能較好地對應起來,生命週期不能靈活控制,任務監控也比較難接入Prometheus集羣監控。雖然Spark社區也不斷地在推出新特性來和K8s集成地更加靈活,不過對於些複雜場景需要定製開發,spark-submit的封裝性也會成為阻礙。

spark-submit還是離線任務提交的思維,而Spark Operator方式就更傾向於K8s作業的思維,作為K8s的自定義控制器,在集成了原生的Spark on K8s的基礎上利用K8s原生能力提供了更全面管控功能。Spark Operator使用聲明式的YAML提交Spark作業,並提供額外組件來管理Spark作業的生命週期,SparkApplication控制器,負責SparkApplicationObject的創建、更新和刪除,同時處理各種事件和作業狀態,Submission Runner, 負責調用spark-submit提交Spark作業,Driver和Executor的運行流程是一致的,Spark Pod Monitor,負責監控和同步Spark作業相關Pod的狀態。Spark Operator最大的好處是為在K8s中的Spark作業提供了更好的控制、管理和監控的功能,可以更加緊密地與K8s結合並能靈活使用K8s各種特性來滿足複雜場景,例如混部場景,而相對地它也不再像spark-submit那樣方便地提交任務,所以如何使用Spark Operator優雅提交任務將是在離線混部中一項重要的工作。

2.1.2 最終選項

在大的架構選型上,我們選擇了Spark on K8s,一方面因為Spark3.X是vivo當前及未來2~3年的主流離線引擎,另一方面vivo有比較完善的K8s生態體系,內部對K8s研發也比較深入,環境和能力都能很好地支持,在應用的小方向上,我們選擇了Spark Operator,因為它在混部這種複雜場景下使用更加靈活、擴展性更強、改造成本更低,我們最終決定使用Spark Operator方案。

2.2 Spark優化

2.2.1 Spark鏡像

Spark任務容器化的第一步就是構建具有Spark相關環境的鏡像,Spark任務類型主要分為sql任務和jar任務,在實踐的過程中我們發現Spark的鏡像構建需要注意幾個問題

  • Spark環境的完整性:鏡像中除了打入自研的Spark包以外,還需要打入相應的依賴如Hadoop、ZSTD、RSS等包,對於SparkJar任務還有直接調用Hadoop客户端的,因此Hadoop客户端也需要打入鏡像中。
  • JDK版本問題:K8s使用的Spark是基於3.2.0版本,鏡像打包工具默認使用JDK11,而自研的Spark用的JDK1.8,由於在Yarn和K8s上使用的JDK版本不同,導致在雙跑驗證數據一致性時發現了hash函數、時間戳不一致的問題,因此Spark鏡像中的JDK版本需要和Yarn保持一致。
  • 環境變量問題:鏡像生成容器後需要預置如Spark、Hadoop的環境變量,如果鏡像中相關目錄的位置不能完全和Yarn的提交節點保持一致,則需要檢查各啓動腳本,如spark-env.sh中的環境變量的路徑是否存在,發生衝突時可以修改為絕對路徑。

Spark鏡像構建完成後,區分SparkSql任務和SparkJar任務實質就是啓動命令的不同,事實上SparkSql任務也就是SparkJar任務的一種,只是啓動的主類是固定的,兩者的啓動參數如下:

SparkSql任務:

driver --class org.apache.spark.sql.hive.thriftserver.SparkSQLCLIDriver -f {sql文件}

SparkJar任務:

driver --class {jar任務主類} {jar任務jar包} {參數}

早期不僅構建了Spark鏡像,還構建了Spark日誌鏡像,容器組成結構會複雜一些。如圖例如Driver容器,我們將Spark、Hadoop等配置文件構建了configMap,啓動initContainer來拉取從configMap拉取配置文件,然後啓動Driver容器執行Spark任務,同時也使用sidecar創建日誌上報的容器,在Spark任務運行完成後上報Driver和Executor日誌到Spark HistoryServer。這樣的方案看似充分應用了K8s技術,但是在實踐的過程中這些技術卻被一一棄用,轉而逐步地把各種功能集中到了一個Driver容器上。

圖片

具體演進如下:

  • 移除initContainer,拉取Spark等配置文件步驟寫在啓動命令中,Spark作業執行前執行下載配置,原因在多個namespace下不方便統一管理,而且configmap內容較大,會導致Pod啓動時配置加載的延遲增加,影響了Pod創建速度,同時K8s的內存和CPU資源佔用增加,對kube-apiserver、ETCD負載有一些影響。去掉initContainer還有個重要的好處就是減小ETCD的存儲壓力,事實上我們在移除initContainer拉取配置的功能後的一段時間內還保留着initContainer,在任務逐漸上量後發現ETCD的存儲比較滿,分析後發現Spark作業中的一個Pod生命週期大約8次更新,其中initContainer更新會佔用2次,移除了之後理論上是可以減少1/4的ETCD存儲,實際應用中完全去除了initContainer也確實能減小了ETCD的存儲壓力。
  • 移除sidecar創建日誌上報的容器,Driver和Executor日誌上報步驟寫在啓動命令中,Spark作業執行完後再執行腳本上報,原因是sidecar在同一個Pod中與主容器共享相同的生命週期,不使用sidecar方式就能更快創建Pod,Spark任務執行完成後能更快釋放資源。

對於Spark作業會頻繁創建、更新和銷燬大量的Pod,所以去除非必要的容器,提高Pod生命週期流轉速度,就能降低kube-apiserver、ETCD工作負載,也能提高Spark的作業效率。

2.2.2 Spark改造

Spark任務運行在K8s上,對於一些使用的兼容問題也進行了相關改造

  • HistoryServer改造,因為Spark Operator沒有存儲已結束作業的日誌,因此參考了on Yarn的方式,在Spark作業結束後,通過日誌上傳腳本把Driver和Executor的日誌上傳HDFS,與Yarn日誌聚合類似,同時也在Spark HistoryServer做了二次開發工作,增加了on K8s方式的日誌查看接口,用户查看已完成的Executor日誌時,不再請求JobHistory Server,而是請求Spark HistoryServer接口。但日誌上傳方式需要Executor執行完才能查看到日誌,為了能實時查看到執行中的日誌,可以在Executor內部實現一個HTTP服務,根據Pod以及端口信息拼接出日誌請求URL,Executor啓動一個Servlet自動獲取本地日誌並返回。日誌查看體驗上做到了基本與Yarn一致。
  • 主機ip通信,Spark Driver和Executor之間的通信通常是通過主機名進行的,不過隨着Spark任務增多,CoreDNS因為頻繁的域名解釋請求導致壓力增大,甚至會影響到在線服務,因此我們將Hadoop的配置文件改為ip格式、設置Driver和Executor使用ip地址,同時去除了對應的K8s Service,通過訪問ip而不是域名的方式來規避這個問題。
  • 文件參數兼容,Spark Driver在K8s上是運行在某一個Pod中的,所以文件需要是全局可視的,如HDFS文件,否則就會報文件未找到的錯誤,但Spark作業運行在大數據作業平台時有的任務使用的上傳的本地文件,因此對於提交到K8s的任務,第一步是要把上傳到大數據作業平台的文件再次上傳到HDFS,第二步是改造add jar和--file等命令邏輯,Spark任務在未能讀取本地文件後將再嘗試讀取二次上傳到HDFS的文件,實現任務無需修改成全局可視的文件路徑也能讀取到文件。
  • non-daemon線程終止,在K8s上運行的Spark任務是指定Client模式,Client模式下Driver遇到異常時停掉SparkContxet,等所有non-daemon線程結束後,Driver才會退出,但如果存在一直運行的non-daemon線程,那麼Driver一直不退出,任務就一直處於執行中。因此需要改造成Cluster模式的異常退出機制,即異常時以非0退出碼退出,不再等待其他的non-daemon線程結束,Driver直接終止,以確保Driver Pod的正常結束。

2.3 Spark Operator優化

隨着在K8s上運行的Spark任務不斷增加,K8s集羣的負載也逐漸顯現。因此,需要對Spark Operator進行一系列優化,以減輕K8s集羣的壓力。

  • 離線使用獨立的kube-apiserver,混部集羣中離線容器佔了很大一部分,而且離線任務由於生命週期短,容器創建銷燬更加頻繁,這對kube-apiserver造成了很大的壓力,然而在線業務需要更高的穩定性,為了減少離線對在線業務的影響,我們拆分了kube-apiserver,離線任務通過指定master參數來使用獨立的kube-apiserver。
  • 使用K8s的HostNetwork網絡模式,在K8s上啓動Driver與Executor雖然使用的是獨立ip+固定端口,但頻繁的ip申請和釋放也對kube-apiserver造成了一定的壓力,因此我們改為使用HostNetwork網絡模式,同時不指定端口避免端口衝突。
  • 優化Spark Operator控制器的隊列,在任務量比較大的情況下,Spark Operator對Pod創建消耗效率會遇到瓶頸,排查後發現是Spark Operator的事件處理隊列的併發數和限速桶的默認配置地太小,因此我們調低Spark maxPendingPods參數,調高schedulerBacklogTimeout、 sustainedSchedulerBacklogTimeout參數,減少Pending Pod個數,使Pod的處理效率符合集羣的承載水平。
  • 優化Spark Driver List Pod接口,使用kube-apiserver緩存,避免對ETCD產生影響,同時修改Spark Driver清理Executor邏輯,直接Delete,減少List Pod對kube-apiserver壓力。
  • 存儲emptydir + log lv存儲優化,開發CSI插件,Spark任務的離線日誌單獨存儲,避免對在線業務pod的影響和磁盤負載高等問題。
  • Spark Secret標記immutable,減少kubelet watch secret請求,降低kube-apiserver的負載。

三、離線任務提交

3.1 平台任務提交平滑切換

離線任務容器化方案確定後就要落地到生產,目前有SparkSql和SparkJar兩種離線任務實現了容器化,這裏以SparkSql任務為例描述Spark提交到混部K8s集羣的流程並達到與傳統客户端提交任務幾乎無差異的平滑切換。目前vivo的離線任務都是通過大數據平台進行提交和調度的,平台會把主要的提交流程進行封裝形成簡單操作的功能,例如在平台上提交SparkSql任務流程一般是編寫sql、提交任務、查看Driver日誌或在跳轉到SparkUI、執行完成後獲取結果以及更新任務狀態。

在平台內部,SparkSql任務使用傳統的spark-submit提交流程是:

  1. 用户編寫好的sql上傳到提交節點生成一個sql文件;
  2. 在提交節點使用Spark客户端執行該sql文件啓動SparkSql任務;
  3. 任務啓動後,通過不斷地tail操作查詢日誌轉存到HBase方便在平台頁面上查詢到Driver日誌;
  4. 任務結束後,再查詢輸出結果轉存到HBase方便在平台頁面上查詢到執行結果;
  5. 根據提交sql任務命令的返回碼來更新任務狀態。

傳統Spark客户端提交任務大部分只會涉及到提交節點的客户端與平台服務器之間的交互,而SparkSql任務提交到混部K8s集羣,從上節的Spark容器化方案的原理可知最終目的是要將Spark任務的任務參數按一定的格式封裝好傳入Spark Operator控制器來創建相關的容器,平台需要通過會調用容器團隊提供的封裝好K8sApi的統一接入層來創建Spark容器。

圖片

在平台內部,SparkSql任務提交到混部K8s集羣的完整流程為:

  1. 用户編寫好的sql上傳到HDFS生成一個遠程可訪問的HDFS文件;
  2. SparkSql任務參數封裝好傳入容器接入層的createSpark接口來調用Spark Operator控制器容器,再由Spark Operator控制器創建Driver Pod,最後由Driver Pod根據Spark任務需要創建多個Executor Pod,這些Driver、Executor的Pod相當於Driver和Executor的角色,共同配合執行Spark作業;
  3. 任務啓動後,通過容器接入層的getDriverLog接口週期性地查詢Driver日誌,實質上是查詢Driver容器的日誌,查詢到的Driver日誌會轉存到HBase方便在平台頁面上查詢;
  4. 任務結束後,一方面通過Spark啓動腳本中的日誌上傳命令,把Driver和Executor的日誌上傳HDFS,可以在改造後的Spark HistoryServer直接查看,另一方面執行結果也會先輸出到HDFS,再從HDFS轉存到HBase方便在平台頁面上查詢到執行結果;
  5. 通過輪詢接入層的getSpark接口根據返回的狀態碼來更新任務狀態,在任務結束後,此時Driver Pod不會主動退出,首先將任務狀態更新為成功,在日誌和結果都存儲完成後,再調用deleteSpark接口主動地殺死Driver Pod釋放資源,完成整個Spark任務流程。

可以看出SparkSql任務提交到混部K8s的執行主體是容器,因此需要增加容器接入層來管理Spark相關的容器,同時容器的使用更傾向於存算分離的效果,因此需要使用HDFS作為遠程文件中轉。

大數據平台上傳統使用spark-submit和onK8s使用spark-operator的SparkSql任務執行流程對比如下:

圖片

3.2 混部任務的資源參數調整

Spark任務的Driver和Executor,在Yarn上執行實質是運行在NodeManager節點上的,而在K8s上執行實質是運行在對應的Pod中的,由於Spark on K8s的提交方式和運行環境都不同於on Yarn,任務的資源參數不能直接套用,需要做一些參數調整才能提交到K8s上。

1、資源參數提取和轉換

SparkSql任務在Yarn上可以靈活地調整sql中的配置來滿足不同特性的任務,sql中的資源配置會覆蓋客户端啓動時的全局配置,因為Executor是運行在NodeManager節點上的,資源會相對充裕能滿足Executor的資源需求,與此不同的是Spark on K8s的Executor是運行在Executor Pod中的,使用的資源會受到Pod資源規格大小的限制,而spark-operator的提交方式是要先獲取Executor全局資源規格並生成相應資源規格大小的Executor Pod,所以在提交Spark任務到K8s前就要準確地獲取任務真正生效的資源參數。在大數據平台中資源參數會存在多中類型的參數中,參數的優先級為:任務配置參數 < 任務模板參數 < sql中設置參數 < HBO優化參數 < 平台統一參數,按此優先級順序依次提取最終的資源參數並傳入容器接入層創建Spark作業。另外容器接入層對於Spark的arguments和sparkConf參數都是要求以字符數組的方式傳入,需要做好對原任務參數中的單引號、雙引號、反斜槓和回車等符號以及分段落的處理和轉換。

2、overheadMemory的計算

在Yarn上Executor是運行在NodeManager節點上的,節點的資源一般都大於並能滿足container申請的資源,所以在Yarn上只需要關心container本身申請的資源即可,而在K8s上Executor運行在對應的Pod中,可以把Pod理解為只一台獨立的節點,除了要滿足container申請的資源量,還需要一些Pod容運行時網絡、存儲等基礎設施的自身開銷資源,如果把Spark任務中Driver和Executor申請的資源直接設置為K8s中Driver Pod和Executor Pod的資源規格,有可能出現OOM情況,另外還要考慮非JVM內存,Spark默認會把申請的Executor內存乘以一個係數或者至少預留384 MiB內存作為額外的非JVM內存緩衝區,用於堆外內存分配、非JVM任務以及各類系統進程的使用,可以通過設置overheadMemory進行覆蓋。因此K8s的Pod除了要滿足申請的Memory和運行時需要的overheadMemory的資源,還會再添加100M資源用於Pod運行的自身開銷。

pod的資源規格 = memory + pod overheadMemory

對於overheadMemory也需要先獲取到並加到Pod的資源規格,如果任務有配置就直接使用配置的overheadMemory,如果沒有配置值則按一定計算公式來計算得到。

有配置

pod overheadMemory = overheadMemory + 100M

無配置

pod overheadMemory = (max(384M,0.1*memory))向上取整到512MB的整數倍 + 100M

不過在實際應用中發現對於個別任務,即使K8s上配置的overheadMemory比在Yarn的配置多100M,完全一樣的任務在K8s上則有較多的Executor OOM情況,而在Yarn上卻完全沒有,目前排查到的現象是有JVM堆外的內存無法回收,如果任務需要較多的對外內存,堆外內存一直增長最終導致OOM,但哪些內存無法回收的還未排查到。目前對於這些OOM過多且實際影響到運行效率的任務,在原overheadMemory基礎上再增加512M後就沒有OOM情況了,同時也有采用了大數據平台的HBO能力自動調整內存參數來事後規避這個問題。

3、CPU超分配置

Spark任務申請的CPU使用一般不會使用完,事實上Executor Pod的CPU利用率也並不是很高,比如Executor申請1個核,通常只能利用0.6個核,存在CPU浪費的現象。Executor Pod的資源規格是創建的時候分配的,利用容器的能力,可以採取CPU超分的方式提高CPU的利用率,例如Executor申請1核,實際用0.6核,如果Pod分配1核,那利用率就只有60%,但如果Pod只分配0.8核,那利用率就有75%了,所以超分的策略就是申請了1核只給0.8核,但還是要按1核的申請量來運行任務。目前平台使用的是靜態的固定比例超分設置為0.8,實施超分配置策略後Pod的實際CPU利用率打到80%以上。

圖片

3.3 混部任務的篩選提交

經過上面的任務提交方式的改造和任務資源參數的調整,原SparkSql和SparkJar任務就可以平滑切換提交到混部K8s上執行了,但在大規模切換之前平台還做了比較長期的雙跑驗證工作,在執行成功率、數據一致性和執行時效等方案都進行了雙跑比較,雙跑通過的任務才能切換到K8s上執行。除了雙跑通過,前期還設置了其他的篩選條件如下。

圖片

前期按這些條件篩選出可以提交到K8s的任務,然後分批的進行K8s任務的參數標記,並把標記的這批任務添加監控進行跟蹤。經過雙跑驗證、任務篩選、批量標記、監控跟蹤和問題解決這一整套SparkSql任務上量K8s的流程,K8s上的任務運行逐步穩定,K8s的兼容問題也基本解決,因此目前取消了雙跑通過的這一條件,主要保留了任務重要性、運行時長和重試次數這幾個篩選指標。隨着SparkSql任務上量和穩定,提交到K8s的任務類型也增加了SparkJar任務,SparkJar任務無法進行雙跑驗證,所以在各種K8s兼容問題解決後再推進會更加穩妥。

目前大數據平台會定期篩選和標記一批SparkSql和SparkJar任務允許提交到混部K8s,用户也可以自行開啓,在任務配置頁面只顯示已開啓混部,則該任務就有機會被提交到混部K8s上執行。當然,用户也可以手動關閉這一開關,並且手動操作的優先級最高,手動關閉後平台的自動開啓功能將不再生效。

四、彈性調度系統

4.1 彈性調度功能矩陣

Spark任務開啓了混部也不是必定能提交到混部,最終能不能在混部集羣上執行,還要根據當時混部集羣的資源和運行情況等來確定,為了更好地協調離線任務和混部集羣的供需關係,大數據平台構建了離線任務混部彈性調度系統。彈性調度系統的設計目是混部集羣有資源了就調度離線任務,但在生產環境中不管是混部集羣還是離線任務都會各自的問題需要解決和優化的需求,彈性調度系統也逐步演變成了全面管理離線任務提交到混部以實現混部資源最大化利用的功能矩陣。

4.1.1 資源水位線調度

圖片

彈性調度的流程,任務按調度時間以任務流的形式過來,如果任務標記了允許提交到混部,那就會先去查詢K8s的各個集羣,如果某一個集羣資源充足就直接提交到K8s,如果當時沒有足夠資源就等待資源再判斷,這裏分為有三類任務,第一類是一直等K8s資源,永不超時,只會提交到K8s;第二類是長時間等待,超時時間在1到5分鐘,可以等久一點;第三類是短時等待,超時時間為30-60秒,稍微等一下,如果K8s沒有資源就回到Yarn上執行,目前平台標記的任務大部分任務都是第三類短時等待。

混部集羣提供給離線任務的資源是呈潮汐波動的,使用百分比的水位線方式才能更好地貼合資源的波動情況。混部集羣提供的資源是指CPU和內存,但離線任務一般不能百分之百地獲取到這部分資源,需要設置一個折算比例也就是水位線來計算出離線任務能使用的真正資源是多少,水位線的設置需要考慮幾個因素

1、混部集羣的碎片化率,混部集羣中的機器規格和正在運行的業務佔用量都是不確定的,但一般大規格的機器多的集羣碎片化率較低,所以小規格的機器多的集羣的水位線要設置低一點。

2、資源動態分配容納率,對於開啓了動態分配的Spark任務,無法提前知道任務所需的資源,需要留有一部分資源用於動態分配的消耗,如果同樣的水位線資源規模大的混部集羣容納率會高,所以資源規模小的集羣的水位線要設置低一點。

3、資源配比的均衡性,不同的集羣或者同一集羣的不同時間段的CPU和內存配比可能會存在很大的差異,例如Spark任務的CPU和內存的平均比例是1核6G,即1:6,如果有CPU和內存比為1:2的,內存會被用完而CPU有剩餘,此時為了內存留有部分餘量,水位線要設置低一點。

混部資源可用量 = 混部資源提供量 * 資源水位線

資源水位線有CPU水位線和內存水位線,設計時以CPU或內存中的最低水位線為準,哪個資源先分配完就停止提交任務,不過在實際生產中大部分混部集羣都是受內存限制較多,個別時段CPU比內存多但通過其他的限制手段即使CPU滿載對任務影響不大,因此目前只開啓了內存資源水位線。以上提到的3點可以當成集羣的固有消耗需要保留有一定的餘量,為了直觀地控制混部資源使用率和引入優先策略,計算方式調整為:

混部資源可用量 = 混部資源提供量 * (1-餘量水位線) * 優先水位線

餘量水位線根據各個集羣來調整,一般為0.05,優先水位線的範圍可以在0-1之間。優先水位線的作用是對於一些符合優先條件的任務可以優先提交,但是任務調度是一有任務就要調度的流式調度,不能夠先集中再挑選優先任務而是先到先得,所以要為優先任務預留一部分資源,例如優先水位線為0.8,混部資源使用到0.8以下的時候任何任務都可以調度上來,但使用量超過了0.8,那只有優先任務能調上來,也就是為優先任務預留了0.2的資源,當然即使資源使用量達到了1,由於餘量水位線的存在,實際的使用量為0.95,混部集羣仍有資源維持週轉。優先水位線是最常用的調整參數,它實質就是控制混部任務提交量,不僅能調整混部資源的使用量,還在灰度測試、壓力測試和問題排查等事項起到了靈活調節的作用。

4.1.2 其他調度能力

1.多集羣管理:混部集羣通常會有多個,vivo目前就有多個生產環境的混部集羣,各混部集羣由於建設週期、機器規格和業務接入的不同,混部資源的規模和變化趨勢都會呈現比較大的差異,因此每個集羣的調度策略配置都需要做到能獨立調整來適應各自的資源特點。

2.分時段控制:每個混部集羣上的在線業務一般是潮汐波動的,給到離線任務的資源也是潮汐波動的,因此每個集羣需要做到在每天不同時段可以調整不同的調度策略,尤其在波峯波谷差異較大的時間段各自調整配置的差異會更大。

3.分散namespace:Spark任務的Driver Pod和Executor Pod都會放在一個namespace中管理,如果所有任務都由一個namespace管理,那需要管理的pod數量會達到數十萬的級別,會對K8s集羣的性能和穩定性產生影響。因此需要將Spark任務平均分配到多個namespace,採用的方案是輪詢填充,任務優先分配到多個namespace中任務最少namespace。

4.失敗回退Yarn:離線任務混部推進的過程中還有會有Spark兼容問題、混部集羣異常和平台變更等問題導致的離線任務在混部K8s上運行失敗,為了減少失敗對任務的影響,任務在K8s上首次執行失敗後就會自動回到Yarn重新執行。

5.資源准入粒度:各混部集羣的機器規格和碎片率是不一樣的,如executorMemory=2G這樣較小粒度的Spark任務即使碎片率較高的混部集羣可以填充,而對於executorMemory=16G這樣較大粒度的Spark任務,機器規格大的集羣才更容易獲取到資源,因此不同混部集羣可以設置不同的准入粒度,小規格和碎片率高的集羣准入粒度可以設置小一些。

6.任務偏好配置:對於一些灰度任務和特殊要求的任務,例如只有在0到8點才允許提交到混部、只提交到某幾個指定的混部集羣等調度要求,需要支持任務偏好配置,在任務參數中調整混部控制參數實現相應的調度需求。

4.2 彈性調度策略優化

彈性調度的核心是通過資源水位線的調節,有混部資源就調度離線任務,但實際生產中還要考慮混部集羣的運行情況,是否能穩定地接收和消化離線任務,同時在存在多個差異較大的集羣時提交到哪個集羣最優。

4.2.1 任務調度穩定優化

大數據平台的離線任務提交高峯在凌晨時段而且調度時間集中在整點半點,還有5分和10分這樣的整分,例如03:00調度的任務達1000個,但在03:01調度的任務只有10個,過於集中地提交任務會導致混部集羣Pending Pod數量急劇上升,這是因為無論是查詢集羣資源還是Pending數的接口,更新數據都需要一定的週期時間,而且離線任務提交上去到獲取資源也受K8s的調度時間的影響,所以獲取集羣運行情況總會滯後於任務提交。例如03:00查詢集羣是有資源的並且是健康的,由於任務開啓了動態分配所以不能確定需要多少資源,此時集中提交了1000個任務,這1000個任務首先會創建1000個Driver Pod,集羣資源還是能滿足的並且優先創建,假如每個Driver需要創建100個Executor,如果集羣沒有這麼多資源,那就會產生大量的Penging Pod,嚴重影響集羣的性能和穩定以及任務的執行效率,因此需要對彈性調度的穩定性進行優化。

短時提交限制:避免集中提交任務的直接方案就是根據各混部集羣的資源規模設置短時提交的任務數量限制,例如1分鐘內只能提交100個任務,集羣短時間內Pending Pod數量會增加但仍在可以承受範圍內,集羣和任務都會穩定運行。短時提交限制相當於攔截並捨棄了部分某個時間點集中提交的任務,這裏相當於捨棄了900個任務,那麼提交的總任務量就減少了。

延遲打散提交:為解決短時提交限制導致捨棄部分任務的問題,增加了短時延遲打散提交,例如03:00提交的1000個任務,隨機打散到03:00到03:03的3分鐘內,即使有短時提交限制,這3分鐘內也可以提交300個任務。理論上將集中提交的任務延遲更久,能提交到混部的任務會更多,但是增加延遲時長就等於增加任務的執行時長,會影響到業務數據產出的及時性,因此延遲打散提交策略只能是短時的,進一步的優化是執行時長更久的任務延遲更久一點,但根本解決方案還是用户能將調度時間儘量打散。

集羣反饋限制:短時提交限制和延遲打散提交都屬於靜態限制,需要人為地根據各個混部集羣的情況去判斷和設置限制值,因此需要做到動態限制,就需要獲取集羣的運行情況並根據運行情況進行限制。事實上K8s的調度性能相比於Yarn還是有差距的,從提交的Spark任務到獲取到資源運行Pod有一定的滯後時間差,這段時間查詢內還是有剩餘資源,但如果還繼續提交新任務就會產生更多Pending Pod,因此需要做集羣運行情況的反饋控制,例如查詢Pending Pod數、等待的SparkApp數,當數量達到一定數量就不再提交新任務。

集羣反饋限制雖然是動態的能根據混部集羣情況進行反饋調節,但是查詢集羣狀態是滯後的,這種滯後的控制就容易被集中提交給打垮,所以要加上短時提交限制來上一道保險,為緩解短時提交限制造成的任務損失,就引入了延遲打散提交,而在延時打散的過程中集羣能逐步消化任務,查詢集羣狀態逐步接近真實情況,這時又可以交給集羣反饋限制來動態調節,逐步從突增恢復到穩定,三個調度穩定優化策略相輔相成。

4.2.2 集羣分配均勻優化

離線任務會調度到多個混部集羣,每個集羣的資源總量和可用資源量,以及集羣運行狀況都不相同,為保證離線任務的運行穩定和執行效率,需要在多個混部集羣中選擇一個最合適的集羣。各個集羣會按一定的規則進行排序,離線任務會按這個排序依次輪詢各個集羣,只要集羣剩餘資源滿足且沒有被短時提交限制、集羣反饋限制等拒絕,離線任務就提交到該集羣。集羣排序的演化順序如下:

①初始方案

排隊隊列+輪詢

剩餘資源量多的優先

圖片

優點

離線任務優先提交到資源最多的集羣,保證離線任務運行穩定

缺點

對於小集羣剩餘資源量很小一直分配不到任務容易“餓死”(事實上有的小集羣全部資源量都達不到一個大集羣的20%)

② 優化方案

隨機隊列+排序隊列+輪詢

將資源使用量超過一定比例的集羣放到排序隊列,剩餘的集羣放到隨機隊列

圖片

優點

離線任務優先提交到資源較多的集羣,即保證任務的運行穩定,隨機的方式也能均勻“餵飽”每個集羣

缺點

隨機分配在大任務量時相當於是平均分配,每個集羣都會調度差不多的任務量,當前情況會存在整點集中提交大量任務,小集羣接收和大集羣同樣任務量會抗不住,影響任務執行穩定和效率,小集羣容易“撐死”

③再優化方案

加權隨機隊列+排序隊列+輪詢

按剩餘資源進行加權隨機,剩餘資源多的集羣有更多概率分配到任務

圖片

優點

離線任務優先提交到資源較多的集羣,“大集羣多吃,小集羣少吃”,每個集羣都能填充同時保證任務的運行穩定

④ 最終方案

優先隊列(排序)+加權隨機隊列+排序隊列+輪詢

考慮優先隊列,無視其他排序規則,優先隊列裏的集羣將最優先,在優先隊列中的集羣再按資源排序

圖片

優點

繼承上一方案的優點,同時對於特定項目或機房的離線任務,能優先調度到某些特定的集羣

目前只以內存作為資源水位線的衡量標準,這裏的資源量指的是內存量。最開始方案是按集羣的剩餘資源排序,內存資源剩餘多的集羣優先,缺點是小集羣一直分配不到任務容易“餓死”,然後使用隨機的方式也能均勻“餵飽”每個集羣,但小集羣接收同樣任務量時容易“撐死”,於是隨機隊列按剩餘資源進行加權隨機,剩餘資源多的集羣有更多概率分配到任務,這樣離線任務優先提交到資源較多的集羣,“大集羣多吃,小集羣少吃”,每個集羣都能填充同時保證任務的運行穩定,在此基礎上增加優先隊列,無視其他排序規則,優先隊列裏的集羣將最優先,在優先隊列中的集羣再按資源排序,能優先調度到某些特定的集羣,形成最終集羣選擇排序方案。

五、混部的效果與未來規劃

經過以上的對Spark組件、K8s混部系統、大數據平台以及彈性調度系統的改造和優化,目前混部集羣及提交混部的離線任務運行持續穩定,每天任務調度到混部的次數達10+萬次,在凌晨的高峯期通過混部能為離線任務額外增加數百TB內存的計算資源,部分混部集羣的CPU利用率提升至30%左右,整體收益也是可觀的。

雖然目前vivo的在離線混部達到了一定的規模,但未來要繼續提高混部的規模和收益,還有規劃一些改進工作。

1、提高離線任務混部規模。

離線任務混部的節點是在線業務提供的,節點規模取決於在線業務峯值,峯值越高那麼在業務低峯期能提供給離線混部資源就越多,因此提高混部規模的重要因素是提交更多的離線任務。然而目前採用的Spark Operator方案能提交的離線任務只有標準的SparkSql和SparkJar任務,而對於非標準的任務如腳本任務,腳本中除了調用spark-submit提交Spark作業還有額外的處理邏輯,這類任務還不能直接以Spark Operator的方式提交。事實上Spark作業更多是來自腳本任務的非標準任務,如果要繼續增加離線任務的量,就必須把非標準任務也提交到混部,因此後續是選擇改造spark-submit客户端支持Spark Operator,或是選擇使用Yarn on K8s,還需要綜合評估。

2、提高離線任務混部收益。

目前混部節點CPU的平均利用率達到30%,但仍有提升空間。從離線任務的角度來看,一方面是要增加錯峯互補的時間段,例如離線任務的高峯期是02:00到08:00,在線業務的高峯期是06:00到23:00,在06:00後在線業務逐步上量開始回收資源,所以離線任務能顯著提高混部集羣CPU利用率的黃金時間是有02:00到06:00這4個小時,因此如果能把離線任務高峯期提前到00:00到06:00,混部提效的黃金時間就能達到6小時。所以需要推動離線任務高峯期的前移,對於有依賴鏈路的任務,儘量減少調度時間的間隔,上游任務完成後能儘快調起下游任務,而對於沒有依賴的任務,可以儘量提前調度時間,不過這兩種調整都需要推動業務方來調整,平台也可以給予一定的計算成本優惠作為激勵。另一方面是要提高混部資源的填充率,Spark任務需要創建大量的Executor Pod,目前混部集羣的調度器為了保證調度效率就沒有開啓預選、優先策略,事實上Spark的資源粒度比較小更適合填充資源碎片,所以在不影響K8s調度效率的情況下優化資源調配策略,把合適的資源粒度的Pod分配到合適的混部節點,也是提高混部收益的方向。

user avatar zzd41 頭像 markerhub 頭像 dadegongjian 頭像 lele_5d46516329d27 頭像 u_17483758 頭像
5 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.