Spark on K8s 在 vivo 大數(shù)據(jù)平臺的混部實戰(zhàn)
一、在離線業(yè)務差異
互聯(lián)網(wǎng)數(shù)據(jù)業(yè)務服務一般可以分為在線服務和離線任務兩大類,在線服務是指那些長時間運行、隨時響應對實時性要求高、負載壓力隨著接收流量起伏的服務,如電商、游戲等服務,離線任務是指運行周期短、可執(zhí)行時間提交對實時性要求低、有一定容錯性、負載壓力基本可控的服務,如離線計算任務、模型訓練等。一般在線服務在白天時段繁忙,離線任務在凌晨繁忙,兩者的業(yè)務高峰期存在錯峰現(xiàn)象,如果按傳統(tǒng)方式在線和離線都是分別獨立機器部署,業(yè)務高峰時期需要更多機器來支持,業(yè)務低峰期又存在部分機器空閑,整體資源利用率都不高。因此行業(yè)提出來在離線混部的解決方案,在線和離線業(yè)務通過混部系統(tǒng)部署在同一批機器,實現(xiàn)共享資源并錯峰互補,提高整體的資源利用率。目前業(yè)內利用混部技術可以將數(shù)據(jù)中心的CPU利用率提升至40%左右,vivo在2023年混部平臺投入生產也已經將部分混部集群的CPU利用率提升至30%左右,整體收益也是可觀的。
混部系統(tǒng)需要有強大的隔離能力,絕大部分都是基于容器,所以混部的前提是在線和離線業(yè)務都容器化,對于容器管理工具如K8s來說是更適應于運行時間長、啟停次數(shù)少、容器數(shù)量少的在線服務,在線服務也能比較容易地上容器,而對于運行時間短、啟停頻繁、容器數(shù)量大的離線任務,對K8s來說不是天然地適應,但容器化已是大勢所趨,K8s也推出了性能更好的調度器、用于離線任務的控制器,Spark在2.3版本后也支持容器化,諸多技術的發(fā)展也推動離線任務實現(xiàn)容器化以及在離線混部的落地。
本文將從在離線混部中的離線任務的角度,講述離線任務是如何進行容器化、平臺上的離線任務如何平滑地提交到混部集群、離線任務在混部集群中如何調度的完整實現(xiàn)以及過程中的問題解決。
二、離線任務容器化
2.1 Spark Operator 方案
2.1.1 方案對比
vivo離線任務大部分任務是以Spark作為執(zhí)行引擎,Spark任務運行在K8s上,目前業(yè)界有兩種架構的方案:Spark on K8s及Yarn on K8s。兩者部分優(yōu)缺點對比如下:
Spark on K8s是Spark容器化,由K8s直接創(chuàng)建Driver和Executor的Pod來運行Spark作業(yè),Yarn on K8s是Yarn的容器化,由K8s創(chuàng)建RM和NM的Pod,Spark的Driver和Executor運行在NM Pod的container中,正是由于兩種架構方案的區(qū)別,它們各自也會存在優(yōu)缺點。
Yarn on K8s方案可以支持原生的Hive、Spark、Flink等引擎,它僅需要創(chuàng)建一定數(shù)量的NodeManager Pod來滿足作業(yè)需求,Pod運行相對穩(wěn)定因此對K8s的壓力比較小,本身Yarn支持調度性能和調度策略也是專門為離線任務設計的,調度性能比K8s的強很多。由于NodeManager ESS服務是對磁盤有容量和讀寫性能要求的,混部機器的磁盤一般難以滿足,所以也需要能支持不同引擎的Remote Shuffle Service。在資源利用上,NodeManager需要滿足多個作業(yè)的資源,最小單位是Container,Pod的資源粒度比較大,自身也會占用一些資源,如果資源粒度得不到有效地彈性伸縮,也會造成資源的浪費,因此需要引入額外的組件來協(xié)調,根據(jù)Kubernetes集群節(jié)點的剩余資源,動態(tài)調整NodeManager的CPU和內存,然而這也需要一定的改造成本。在資源緊張的情況下,NodeManager Pod如果被驅逐也就意味著整個NodeManager被銷毀,將會影響多個任務。
Spark on K8s方案目前在Spark 3.1以上版本才正式可用,它需要頻繁的創(chuàng)建、查詢、銷毀大量的Executor Pod,對K8s的ApiServer和ETCD等組件都會造成比較大的壓力,K8s的調度器也不是專門為離線的大批量任務設計的,調度性能也比較弱。另一方面,Spark on K8s雖然只能支持Spark3.X的RSS,不過目前有較多的開源產品可選擇。在資源利用上,最小單位是Driver和Executor的Pod,資源粒度小,可以填充到更多的碎片資源,調度時直接與K8s對接,資源的彈性調度更多由K8s來承擔,不需要額外的組件,改造成本比較低。在資源緊張的情況下,Executor、Driver的Pod將依次逐個被驅逐,任務的穩(wěn)定性會更高。
而對于Spark on K8s方案,還細分2種實現(xiàn)方案:Spark Submit on K8s和Spark Operator on K8s。
SparkOnK8s架構圖
(圖片來源:Spark官網(wǎng))?
Spark Operator架構圖
(圖片來源:Spark Operator開源項目)
以spark-submit方式提交到K8s集群是Spark在2.3版本后提供的原生功能,客戶端通過spark-submit設置K8s的相關參數(shù),內部再調用K8sApi在K8s集群中創(chuàng)建Driver Pod,Driver再調用K8sApi創(chuàng)建需要的Executor Pod,共同組成Spark Application,作業(yè)結束后Executor Pod會被Driver Pod銷毀,而Driver Pod則繼續(xù)存在直到被清理。使用spark-submit方式的最大好處是由spark-submit來與K8s的進行交換,提交作業(yè)的方式幾乎保持一致。但是因為使用的便利性所需要的封裝也會帶來一些缺點,spark-submit是通過K8sApi創(chuàng)建Pod,使用非聲明式的提交接口,如果需要修改K8s配置就需要重新開發(fā)新接口,二次開發(fā)復雜繁瑣,雖然Spark提供了大量的K8s配置參數(shù),但也遠比不了K8s YAML的聲明式的提交方式更加靈活,而且Spark Application和K8s Workload的生命周期還不能較好地對應起來,生命周期不能靈活控制,任務監(jiān)控也比較難接入Prometheus集群監(jiān)控。雖然Spark社區(qū)也不斷地在推出新特性來和K8s集成地更加靈活,不過對于些復雜場景需要定制開發(fā),spark-submit的封裝性也會成為阻礙。
spark-submit還是離線任務提交的思維,而Spark Operator方式就更傾向于K8s作業(yè)的思維,作為K8s的自定義控制器,在集成了原生的Spark on K8s的基礎上利用K8s原生能力提供了更全面管控功能。Spark Operator使用聲明式的YAML提交Spark作業(yè),并提供額外組件來管理Spark作業(yè)的生命周期,SparkApplication控制器,負責SparkApplicationObject的創(chuàng)建、更新和刪除,同時處理各種事件和作業(yè)狀態(tài),Submission Runner, 負責調用spark-submit提交Spark作業(yè),Driver和Executor的運行流程是一致的,Spark Pod Monitor,負責監(jiān)控和同步Spark作業(yè)相關Pod的狀態(tài)。Spark Operator最大的好處是為在K8s中的Spark作業(yè)提供了更好的控制、管理和監(jiān)控的功能,可以更加緊密地與K8s結合并能靈活使用K8s各種特性來滿足復雜場景,例如混部場景,而相對地它也不再像spark-submit那樣方便地提交任務,所以如何使用Spark Operator優(yōu)雅提交任務將是在離線混部中一項重要的工作。
2.1.2 最終選項
在大的架構選型上,我們選擇了Spark on K8s,一方面因為Spark3.X是vivo當前及未來2~3年的主流離線引擎,另一方面vivo有比較完善的K8s生態(tài)體系,內部對K8s研發(fā)也比較深入,環(huán)境和能力都能很好地支持,在應用的小方向上,我們選擇了Spark Operator,因為它在混部這種復雜場景下使用更加靈活、擴展性更強、改造成本更低,我們最終決定使用Spark Operator方案。
2.2 Spark優(yōu)化
2.2.1 Spark鏡像
Spark任務容器化的第一步就是構建具有Spark相關環(huán)境的鏡像,Spark任務類型主要分為sql任務和jar任務,在實踐的過程中我們發(fā)現(xiàn)Spark的鏡像構建需要注意幾個問題:
- Spark環(huán)境的完整性:鏡像中除了打入自研的Spark包以外,還需要打入相應的依賴如Hadoop、ZSTD、RSS等包,對于SparkJar任務還有直接調用Hadoop客戶端的,因此Hadoop客戶端也需要打入鏡像中。
- JDK版本問題:K8s使用的Spark是基于3.2.0版本,鏡像打包工具默認使用JDK11,而自研的Spark用的JDK1.8,由于在Yarn和K8s上使用的JDK版本不同,導致在雙跑驗證數(shù)據(jù)一致性時發(fā)現(xiàn)了hash函數(shù)、時間戳不一致的問題,因此Spark鏡像中的JDK版本需要和Yarn保持一致。
- 環(huán)境變量問題:鏡像生成容器后需要預置如Spark、Hadoop的環(huán)境變量,如果鏡像中相關目錄的位置不能完全和Yarn的提交節(jié)點保持一致,則需要檢查各啟動腳本,如spark-env.sh中的環(huán)境變量的路徑是否存在,發(fā)生沖突時可以修改為絕對路徑。
Spark鏡像構建完成后,區(qū)分SparkSql任務和SparkJar任務實質就是啟動命令的不同,事實上SparkSql任務也就是SparkJar任務的一種,只是啟動的主類是固定的,兩者的啟動參數(shù)如下:
SparkSql任務:
driver --class org.apache.spark.sql.hive.thriftserver.SparkSQLCLIDriver -f {sql文件}
SparkJar任務:
driver --class {jar任務主類} {jar任務jar包} {參數(shù)}
早期不僅構建了Spark鏡像,還構建了Spark日志鏡像,容器組成結構會復雜一些。如圖例如Driver容器,我們將Spark、Hadoop等配置文件構建了configMap,啟動initContainer來拉取從configMap拉取配置文件,然后啟動Driver容器執(zhí)行Spark任務,同時也使用sidecar創(chuàng)建日志上報的容器,在Spark任務運行完成后上報Driver和Executor日志到Spark HistoryServer。這樣的方案看似充分應用了K8s技術,但是在實踐的過程中這些技術卻被一一棄用,轉而逐步地把各種功能集中到了一個Driver容器上。
具體演進如下:
- 移除initContainer,拉取Spark等配置文件步驟寫在啟動命令中,Spark作業(yè)執(zhí)行前執(zhí)行下載配置,原因在多個namespace下不方便統(tǒng)一管理,而且configmap內容較大,會導致Pod啟動時配置加載的延遲增加,影響了Pod創(chuàng)建速度,同時K8s的內存和CPU資源占用增加,對kube-apiserver、ETCD負載有一些影響。去掉initContainer還有個重要的好處就是減小ETCD的存儲壓力,事實上我們在移除initContainer拉取配置的功能后的一段時間內還保留著initContainer,在任務逐漸上量后發(fā)現(xiàn)ETCD的存儲比較滿,分析后發(fā)現(xiàn)Spark作業(yè)中的一個Pod生命周期大約8次更新,其中initContainer更新會占用2次,移除了之后理論上是可以減少1/4的ETCD存儲,實際應用中完全去除了initContainer也確實能減小了ETCD的存儲壓力。
- 移除sidecar創(chuàng)建日志上報的容器,Driver和Executor日志上報步驟寫在啟動命令中,Spark作業(yè)執(zhí)行完后再執(zhí)行腳本上報,原因是sidecar在同一個Pod中與主容器共享相同的生命周期,不使用sidecar方式就能更快創(chuàng)建Pod,Spark任務執(zhí)行完成后能更快釋放資源。
對于Spark作業(yè)會頻繁創(chuàng)建、更新和銷毀大量的Pod,所以去除非必要的容器,提高Pod生命周期流轉速度,就能降低kube-apiserver、ETCD工作負載,也能提高Spark的作業(yè)效率。
2.2.2 Spark改造
Spark任務運行在K8s上,對于一些使用的兼容問題也進行了相關改造。
- HistoryServer改造,因為Spark Operator沒有存儲已結束作業(yè)的日志,因此參考了on Yarn的方式,在Spark作業(yè)結束后,通過日志上傳腳本把Driver和Executor的日志上傳HDFS,與Yarn日志聚合類似,同時也在Spark HistoryServer做了二次開發(fā)工作,增加了on K8s方式的日志查看接口,用戶查看已完成的Executor日志時,不再請求JobHistory Server,而是請求Spark HistoryServer接口。但日志上傳方式需要Executor執(zhí)行完才能查看到日志,為了能實時查看到執(zhí)行中的日志,可以在Executor內部實現(xiàn)一個HTTP服務,根據(jù)Pod以及端口信息拼接出日志請求URL,Executor啟動一個Servlet自動獲取本地日志并返回。日志查看體驗上做到了基本與Yarn一致。
- 主機ip通信,Spark Driver和Executor之間的通信通常是通過主機名進行的,不過隨著Spark任務增多,CoreDNS因為頻繁的域名解釋請求導致壓力增大,甚至會影響到在線服務,因此我們將Hadoop的配置文件改為ip格式、設置Driver和Executor使用ip地址,同時去除了對應的K8s Service,通過訪問ip而不是域名的方式來規(guī)避這個問題。
- 文件參數(shù)兼容,Spark Driver在K8s上是運行在某一個Pod中的,所以文件需要是全局可視的,如HDFS文件,否則就會報文件未找到的錯誤,但Spark作業(yè)運行在大數(shù)據(jù)作業(yè)平臺時有的任務使用的上傳的本地文件,因此對于提交到K8s的任務,第一步是要把上傳到大數(shù)據(jù)作業(yè)平臺的文件再次上傳到HDFS,第二步是改造add jar和--file等命令邏輯,Spark任務在未能讀取本地文件后將再嘗試讀取二次上傳到HDFS的文件,實現(xiàn)任務無需修改成全局可視的文件路徑也能讀取到文件。
- non-daemon線程終止,在K8s上運行的Spark任務是指定Client模式,Client模式下Driver遇到異常時停掉SparkContxet,等所有non-daemon線程結束后,Driver才會退出,但如果存在一直運行的non-daemon線程,那么Driver一直不退出,任務就一直處于執(zhí)行中。因此需要改造成Cluster模式的異常退出機制,即異常時以非0退出碼退出,不再等待其他的non-daemon線程結束,Driver直接終止,以確保Driver Pod的正常結束。
2.3 Spark Operator優(yōu)化
隨著在K8s上運行的Spark任務不斷增加,K8s集群的負載也逐漸顯現(xiàn)。因此,需要對Spark Operator進行一系列優(yōu)化,以減輕K8s集群的壓力。
- 離線使用獨立的kube-apiserver,混部集群中離線容器占了很大一部分,而且離線任務由于生命周期短,容器創(chuàng)建銷毀更加頻繁,這對kube-apiserver造成了很大的壓力,然而在線業(yè)務需要更高的穩(wěn)定性,為了減少離線對在線業(yè)務的影響,我們拆分了kube-apiserver,離線任務通過指定master參數(shù)來使用獨立的kube-apiserver。
- 使用K8s的HostNetwork網(wǎng)絡模式,在K8s上啟動Driver與Executor雖然使用的是獨立ip+固定端口,但頻繁的ip申請和釋放也對kube-apiserver造成了一定的壓力,因此我們改為使用HostNetwork網(wǎng)絡模式,同時不指定端口避免端口沖突。
- 優(yōu)化Spark Operator控制器的隊列,在任務量比較大的情況下,Spark Operator對Pod創(chuàng)建消耗效率會遇到瓶頸,排查后發(fā)現(xiàn)是Spark Operator的事件處理隊列的并發(fā)數(shù)和限速桶的默認配置地太小,因此我們調低Spark maxPendingPods參數(shù),調高schedulerBacklogTimeout、 sustainedSchedulerBacklogTimeout參數(shù),減少Pending Pod個數(shù),使Pod的處理效率符合集群的承載水平。
- 優(yōu)化Spark Driver List Pod接口,使用kube-apiserver緩存,避免對ETCD產生影響,同時修改Spark Driver清理Executor邏輯,直接Delete,減少List Pod對kube-apiserver壓力。
- 存儲emptydir + log lv存儲優(yōu)化,開發(fā)CSI插件,Spark任務的離線日志單獨存儲,避免對在線業(yè)務pod的影響和磁盤負載高等問題。
- Spark Secret標記immutable,減少kubelet watch secret請求,降低kube-apiserver的負載。
三、離線任務提交
3.1 平臺任務提交平滑切換
離線任務容器化方案確定后就要落地到生產,目前有SparkSql和SparkJar兩種離線任務實現(xiàn)了容器化,這里以SparkSql任務為例描述Spark提交到混部K8s集群的流程并達到與傳統(tǒng)客戶端提交任務幾乎無差異的平滑切換。目前vivo的離線任務都是通過大數(shù)據(jù)平臺進行提交和調度的,平臺會把主要的提交流程進行封裝形成簡單操作的功能,例如在平臺上提交SparkSql任務流程一般是編寫sql、提交任務、查看Driver日志或在跳轉到SparkUI、執(zhí)行完成后獲取結果以及更新任務狀態(tài)。
在平臺內部,SparkSql任務使用傳統(tǒng)的spark-submit提交流程是:
- 用戶編寫好的sql上傳到提交節(jié)點生成一個sql文件;
- 在提交節(jié)點使用Spark客戶端執(zhí)行該sql文件啟動SparkSql任務;
- 任務啟動后,通過不斷地tail操作查詢日志轉存到HBase方便在平臺頁面上查詢到Driver日志;
- 任務結束后,再查詢輸出結果轉存到HBase方便在平臺頁面上查詢到執(zhí)行結果;
- 根據(jù)提交sql任務命令的返回碼來更新任務狀態(tài)。
傳統(tǒng)Spark客戶端提交任務大部分只會涉及到提交節(jié)點的客戶端與平臺服務器之間的交互,而SparkSql任務提交到混部K8s集群,從上節(jié)的Spark容器化方案的原理可知最終目的是要將Spark任務的任務參數(shù)按一定的格式封裝好傳入Spark Operator控制器來創(chuàng)建相關的容器,平臺需要通過會調用容器團隊提供的封裝好K8sApi的統(tǒng)一接入層來創(chuàng)建Spark容器。
在平臺內部,SparkSql任務提交到混部K8s集群的完整流程為:
- 用戶編寫好的sql上傳到HDFS生成一個遠程可訪問的HDFS文件;
- SparkSql任務參數(shù)封裝好傳入容器接入層的createSpark接口來調用Spark Operator控制器容器,再由Spark Operator控制器創(chuàng)建Driver Pod,最后由Driver Pod根據(jù)Spark任務需要創(chuàng)建多個Executor Pod,這些Driver、Executor的Pod相當于Driver和Executor的角色,共同配合執(zhí)行Spark作業(yè);
- 任務啟動后,通過容器接入層的getDriverLog接口周期性地查詢Driver日志,實質上是查詢Driver容器的日志,查詢到的Driver日志會轉存到HBase方便在平臺頁面上查詢;
- 任務結束后,一方面通過Spark啟動腳本中的日志上傳命令,把Driver和Executor的日志上傳HDFS,可以在改造后的Spark HistoryServer直接查看,另一方面執(zhí)行結果也會先輸出到HDFS,再從HDFS轉存到HBase方便在平臺頁面上查詢到執(zhí)行結果;
- 通過輪詢接入層的getSpark接口根據(jù)返回的狀態(tài)碼來更新任務狀態(tài),在任務結束后,此時Driver Pod不會主動退出,首先將任務狀態(tài)更新為成功,在日志和結果都存儲完成后,再調用deleteSpark接口主動地殺死Driver Pod釋放資源,完成整個Spark任務流程。
可以看出SparkSql任務提交到混部K8s的執(zhí)行主體是容器,因此需要增加容器接入層來管理Spark相關的容器,同時容器的使用更傾向于存算分離的效果,因此需要使用HDFS作為遠程文件中轉。
大數(shù)據(jù)平臺上傳統(tǒng)使用spark-submit和onK8s使用spark-operator的SparkSql任務執(zhí)行流程對比如下:
3.2 混部任務的資源參數(shù)調整
Spark任務的Driver和Executor,在Yarn上執(zhí)行實質是運行在NodeManager節(jié)點上的,而在K8s上執(zhí)行實質是運行在對應的Pod中的,由于Spark on K8s的提交方式和運行環(huán)境都不同于on Yarn,任務的資源參數(shù)不能直接套用,需要做一些參數(shù)調整才能提交到K8s上。
1、資源參數(shù)提取和轉換
SparkSql任務在Yarn上可以靈活地調整sql中的配置來滿足不同特性的任務,sql中的資源配置會覆蓋客戶端啟動時的全局配置,因為Executor是運行在NodeManager節(jié)點上的,資源會相對充裕能滿足Executor的資源需求,與此不同的是Spark on K8s的Executor是運行在Executor Pod中的,使用的資源會受到Pod資源規(guī)格大小的限制,而spark-operator的提交方式是要先獲取Executor全局資源規(guī)格并生成相應資源規(guī)格大小的Executor Pod,所以在提交Spark任務到K8s前就要準確地獲取任務真正生效的資源參數(shù)。在大數(shù)據(jù)平臺中資源參數(shù)會存在多中類型的參數(shù)中,參數(shù)的優(yōu)先級為:任務配置參數(shù) < 任務模板參數(shù) < sql中設置參數(shù) < HBO優(yōu)化參數(shù) < 平臺統(tǒng)一參數(shù),按此優(yōu)先級順序依次提取最終的資源參數(shù)并傳入容器接入層創(chuàng)建Spark作業(yè)。另外容器接入層對于Spark的arguments和sparkConf參數(shù)都是要求以字符數(shù)組的方式傳入,需要做好對原任務參數(shù)中的單引號、雙引號、反斜杠和回車等符號以及分段落的處理和轉換。
2、overheadMemory的計算
在Yarn上Executor是運行在NodeManager節(jié)點上的,節(jié)點的資源一般都大于并能滿足container申請的資源,所以在Yarn上只需要關心container本身申請的資源即可,而在K8s上Executor運行在對應的Pod中,可以把Pod理解為只一臺獨立的節(jié)點,除了要滿足container申請的資源量,還需要一些Pod容運行時網(wǎng)絡、存儲等基礎設施的自身開銷資源,如果把Spark任務中Driver和Executor申請的資源直接設置為K8s中Driver Pod和Executor Pod的資源規(guī)格,有可能出現(xiàn)OOM情況,另外還要考慮非JVM內存,Spark默認會把申請的Executor內存乘以一個系數(shù)或者至少預留384 MiB內存作為額外的非JVM內存緩沖區(qū),用于堆外內存分配、非JVM任務以及各類系統(tǒng)進程的使用,可以通過設置overheadMemory進行覆蓋。因此K8s的Pod除了要滿足申請的Memory和運行時需要的overheadMemory的資源,還會再添加100M資源用于Pod運行的自身開銷。
pod的資源規(guī)格 = memory + pod overheadMemory
對于overheadMemory也需要先獲取到并加到Pod的資源規(guī)格,如果任務有配置就直接使用配置的overheadMemory,如果沒有配置值則按一定計算公式來計算得到。
有配置:
pod overheadMemory = overheadMemory + 100M
無配置:
pod overheadMemory = (max(384M,0.1*memory))向上取整到512MB的整數(shù)倍 + 100M
不過在實際應用中發(fā)現(xiàn)對于個別任務,即使K8s上配置的overheadMemory比在Yarn的配置多100M,完全一樣的任務在K8s上則有較多的Executor OOM情況,而在Yarn上卻完全沒有,目前排查到的現(xiàn)象是有JVM堆外的內存無法回收,如果任務需要較多的對外內存,堆外內存一直增長最終導致OOM,但哪些內存無法回收的還未排查到。目前對于這些OOM過多且實際影響到運行效率的任務,在原overheadMemory基礎上再增加512M后就沒有OOM情況了,同時也有采用了大數(shù)據(jù)平臺的HBO能力自動調整內存參數(shù)來事后規(guī)避這個問題。
3、CPU超分配置
Spark任務申請的CPU使用一般不會使用完,事實上Executor Pod的CPU利用率也并不是很高,比如Executor申請1個核,通常只能利用0.6個核,存在CPU浪費的現(xiàn)象。Executor Pod的資源規(guī)格是創(chuàng)建的時候分配的,利用容器的能力,可以采取CPU超分的方式提高CPU的利用率,例如Executor申請1核,實際用0.6核,如果Pod分配1核,那利用率就只有60%,但如果Pod只分配0.8核,那利用率就有75%了,所以超分的策略就是申請了1核只給0.8核,但還是要按1核的申請量來運行任務。目前平臺使用的是靜態(tài)的固定比例超分設置為0.8,實施超分配置策略后Pod的實際CPU利用率打到80%以上。
3.3 混部任務的篩選提交
經過上面的任務提交方式的改造和任務資源參數(shù)的調整,原SparkSql和SparkJar任務就可以平滑切換提交到混部K8s上執(zhí)行了,但在大規(guī)模切換之前平臺還做了比較長期的雙跑驗證工作,在執(zhí)行成功率、數(shù)據(jù)一致性和執(zhí)行時效等方案都進行了雙跑比較,雙跑通過的任務才能切換到K8s上執(zhí)行。除了雙跑通過,前期還設置了其他的篩選條件如下。
前期按這些條件篩選出可以提交到K8s的任務,然后分批的進行K8s任務的參數(shù)標記,并把標記的這批任務添加監(jiān)控進行跟蹤。經過雙跑驗證、任務篩選、批量標記、監(jiān)控跟蹤和問題解決這一整套SparkSql任務上量K8s的流程,K8s上的任務運行逐步穩(wěn)定,K8s的兼容問題也基本解決,因此目前取消了雙跑通過的這一條件,主要保留了任務重要性、運行時長和重試次數(shù)這幾個篩選指標。隨著SparkSql任務上量和穩(wěn)定,提交到K8s的任務類型也增加了SparkJar任務,SparkJar任務無法進行雙跑驗證,所以在各種K8s兼容問題解決后再推進會更加穩(wěn)妥。
目前大數(shù)據(jù)平臺會定期篩選和標記一批SparkSql和SparkJar任務允許提交到混部K8s,用戶也可以自行開啟,在任務配置頁面只顯示已開啟混部,則該任務就有機會被提交到混部K8s上執(zhí)行。當然,用戶也可以手動關閉這一開關,并且手動操作的優(yōu)先級最高,手動關閉后平臺的自動開啟功能將不再生效。
四、彈性調度系統(tǒng)
4.1 彈性調度功能矩陣
Spark任務開啟了混部也不是必定能提交到混部,最終能不能在混部集群上執(zhí)行,還要根據(jù)當時混部集群的資源和運行情況等來確定,為了更好地協(xié)調離線任務和混部集群的供需關系,大數(shù)據(jù)平臺構建了離線任務混部彈性調度系統(tǒng)。彈性調度系統(tǒng)的設計目是混部集群有資源了就調度離線任務,但在生產環(huán)境中不管是混部集群還是離線任務都會各自的問題需要解決和優(yōu)化的需求,彈性調度系統(tǒng)也逐步演變成了全面管理離線任務提交到混部以實現(xiàn)混部資源最大化利用的功能矩陣。
4.1.1 資源水位線調度
彈性調度的流程,任務按調度時間以任務流的形式過來,如果任務標記了允許提交到混部,那就會先去查詢K8s的各個集群,如果某一個集群資源充足就直接提交到K8s,如果當時沒有足夠資源就等待資源再判斷,這里分為有三類任務,第一類是一直等K8s資源,永不超時,只會提交到K8s;第二類是長時間等待,超時時間在1到5分鐘,可以等久一點;第三類是短時等待,超時時間為30-60秒,稍微等一下,如果K8s沒有資源就回到Yarn上執(zhí)行,目前平臺標記的任務大部分任務都是第三類短時等待。
混部集群提供給離線任務的資源是呈潮汐波動的,使用百分比的水位線方式才能更好地貼合資源的波動情況?;觳考禾峁┑馁Y源是指CPU和內存,但離線任務一般不能百分之百地獲取到這部分資源,需要設置一個折算比例也就是水位線來計算出離線任務能使用的真正資源是多少,水位線的設置需要考慮幾個因素:
- 混部集群的碎片化率,混部集群中的機器規(guī)格和正在運行的業(yè)務占用量都是不確定的,但一般大規(guī)格的機器多的集群碎片化率較低,所以小規(guī)格的機器多的集群的水位線要設置低一點。
- 資源動態(tài)分配容納率,對于開啟了動態(tài)分配的Spark任務,無法提前知道任務所需的資源,需要留有一部分資源用于動態(tài)分配的消耗,如果同樣的水位線資源規(guī)模大的混部集群容納率會高,所以資源規(guī)模小的集群的水位線要設置低一點。
- 資源配比的均衡性,不同的集群或者同一集群的不同時間段的CPU和內存配比可能會存在很大的差異,例如Spark任務的CPU和內存的平均比例是1核6G,即1:6,如果有CPU和內存比為1:2的,內存會被用完而CPU有剩余,此時為了內存留有部分余量,水位線要設置低一點。
混部資源可用量 = 混部資源提供量 * 資源水位線
資源水位線有CPU水位線和內存水位線,設計時以CPU或內存中的最低水位線為準,哪個資源先分配完就停止提交任務,不過在實際生產中大部分混部集群都是受內存限制較多,個別時段CPU比內存多但通過其他的限制手段即使CPU滿載對任務影響不大,因此目前只開啟了內存資源水位線。以上提到的3點可以當成集群的固有消耗需要保留有一定的余量,為了直觀地控制混部資源使用率和引入優(yōu)先策略,計算方式調整為:
混部資源可用量 = 混部資源提供量 * (1-余量水位線) * 優(yōu)先水位線
余量水位線根據(jù)各個集群來調整,一般為0.05,優(yōu)先水位線的范圍可以在0-1之間。優(yōu)先水位線的作用是對于一些符合優(yōu)先條件的任務可以優(yōu)先提交,但是任務調度是一有任務就要調度的流式調度,不能夠先集中再挑選優(yōu)先任務而是先到先得,所以要為優(yōu)先任務預留一部分資源,例如優(yōu)先水位線為0.8,混部資源使用到0.8以下的時候任何任務都可以調度上來,但使用量超過了0.8,那只有優(yōu)先任務能調上來,也就是為優(yōu)先任務預留了0.2的資源,當然即使資源使用量達到了1,由于余量水位線的存在,實際的使用量為0.95,混部集群仍有資源維持周轉。優(yōu)先水位線是最常用的調整參數(shù),它實質就是控制混部任務提交量,不僅能調整混部資源的使用量,還在灰度測試、壓力測試和問題排查等事項起到了靈活調節(jié)的作用。
4.1.2 其他調度能力
1.多集群管理:混部集群通常會有多個,vivo目前就有多個生產環(huán)境的混部集群,各混部集群由于建設周期、機器規(guī)格和業(yè)務接入的不同,混部資源的規(guī)模和變化趨勢都會呈現(xiàn)比較大的差異,因此每個集群的調度策略配置都需要做到能獨立調整來適應各自的資源特點。
2.分時段控制:每個混部集群上的在線業(yè)務一般是潮汐波動的,給到離線任務的資源也是潮汐波動的,因此每個集群需要做到在每天不同時段可以調整不同的調度策略,尤其在波峰波谷差異較大的時間段各自調整配置的差異會更大。
3.分散namespace:Spark任務的Driver Pod和Executor Pod都會放在一個namespace中管理,如果所有任務都由一個namespace管理,那需要管理的pod數(shù)量會達到數(shù)十萬的級別,會對K8s集群的性能和穩(wěn)定性產生影響。因此需要將Spark任務平均分配到多個namespace,采用的方案是輪詢填充,任務優(yōu)先分配到多個namespace中任務最少namespace。
4.失敗回退Yarn:離線任務混部推進的過程中還有會有Spark兼容問題、混部集群異常和平臺變更等問題導致的離線任務在混部K8s上運行失敗,為了減少失敗對任務的影響,任務在K8s上首次執(zhí)行失敗后就會自動回到Yarn重新執(zhí)行。
5.資源準入粒度:各混部集群的機器規(guī)格和碎片率是不一樣的,如executorMemory=2G這樣較小粒度的Spark任務即使碎片率較高的混部集群可以填充,而對于executorMemory=16G這樣較大粒度的Spark任務,機器規(guī)格大的集群才更容易獲取到資源,因此不同混部集群可以設置不同的準入粒度,小規(guī)格和碎片率高的集群準入粒度可以設置小一些。
6.任務偏好配置:對于一些灰度任務和特殊要求的任務,例如只有在0到8點才允許提交到混部、只提交到某幾個指定的混部集群等調度要求,需要支持任務偏好配置,在任務參數(shù)中調整混部控制參數(shù)實現(xiàn)相應的調度需求。
4.2 彈性調度策略優(yōu)化
彈性調度的核心是通過資源水位線的調節(jié),有混部資源就調度離線任務,但實際生產中還要考慮混部集群的運行情況,是否能穩(wěn)定地接收和消化離線任務,同時在存在多個差異較大的集群時提交到哪個集群最優(yōu)。
4.2.1 任務調度穩(wěn)定優(yōu)化
大數(shù)據(jù)平臺的離線任務提交高峰在凌晨時段而且調度時間集中在整點半點,還有5分和10分這樣的整分,例如03:00調度的任務達1000個,但在03:01調度的任務只有10個,過于集中地提交任務會導致混部集群Pending Pod數(shù)量急劇上升,這是因為無論是查詢集群資源還是Pending數(shù)的接口,更新數(shù)據(jù)都需要一定的周期時間,而且離線任務提交上去到獲取資源也受K8s的調度時間的影響,所以獲取集群運行情況總會滯后于任務提交。例如03:00查詢集群是有資源的并且是健康的,由于任務開啟了動態(tài)分配所以不能確定需要多少資源,此時集中提交了1000個任務,這1000個任務首先會創(chuàng)建1000個Driver Pod,集群資源還是能滿足的并且優(yōu)先創(chuàng)建,假如每個Driver需要創(chuàng)建100個Executor,如果集群沒有這么多資源,那就會產生大量的Penging Pod,嚴重影響集群的性能和穩(wěn)定以及任務的執(zhí)行效率,因此需要對彈性調度的穩(wěn)定性進行優(yōu)化。
短時提交限制:避免集中提交任務的直接方案就是根據(jù)各混部集群的資源規(guī)模設置短時提交的任務數(shù)量限制,例如1分鐘內只能提交100個任務,集群短時間內Pending Pod數(shù)量會增加但仍在可以承受范圍內,集群和任務都會穩(wěn)定運行。短時提交限制相當于攔截并舍棄了部分某個時間點集中提交的任務,這里相當于舍棄了900個任務,那么提交的總任務量就減少了。
延遲打散提交:為解決短時提交限制導致舍棄部分任務的問題,增加了短時延遲打散提交,例如03:00提交的1000個任務,隨機打散到03:00到03:03的3分鐘內,即使有短時提交限制,這3分鐘內也可以提交300個任務。理論上將集中提交的任務延遲更久,能提交到混部的任務會更多,但是增加延遲時長就等于增加任務的執(zhí)行時長,會影響到業(yè)務數(shù)據(jù)產出的及時性,因此延遲打散提交策略只能是短時的,進一步的優(yōu)化是執(zhí)行時長更久的任務延遲更久一點,但根本解決方案還是用戶能將調度時間盡量打散。
集群反饋限制:短時提交限制和延遲打散提交都屬于靜態(tài)限制,需要人為地根據(jù)各個混部集群的情況去判斷和設置限制值,因此需要做到動態(tài)限制,就需要獲取集群的運行情況并根據(jù)運行情況進行限制。事實上K8s的調度性能相比于Yarn還是有差距的,從提交的Spark任務到獲取到資源運行Pod有一定的滯后時間差,這段時間查詢內還是有剩余資源,但如果還繼續(xù)提交新任務就會產生更多Pending Pod,因此需要做集群運行情況的反饋控制,例如查詢Pending Pod數(shù)、等待的SparkApp數(shù),當數(shù)量達到一定數(shù)量就不再提交新任務。
集群反饋限制雖然是動態(tài)的能根據(jù)混部集群情況進行反饋調節(jié),但是查詢集群狀態(tài)是滯后的,這種滯后的控制就容易被集中提交給打垮,所以要加上短時提交限制來上一道保險,為緩解短時提交限制造成的任務損失,就引入了延遲打散提交,而在延時打散的過程中集群能逐步消化任務,查詢集群狀態(tài)逐步接近真實情況,這時又可以交給集群反饋限制來動態(tài)調節(jié),逐步從突增恢復到穩(wěn)定,三個調度穩(wěn)定優(yōu)化策略相輔相成。
4.2.2 集群分配均勻優(yōu)化
離線任務會調度到多個混部集群,每個集群的資源總量和可用資源量,以及集群運行狀況都不相同,為保證離線任務的運行穩(wěn)定和執(zhí)行效率,需要在多個混部集群中選擇一個最合適的集群。各個集群會按一定的規(guī)則進行排序,離線任務會按這個排序依次輪詢各個集群,只要集群剩余資源滿足且沒有被短時提交限制、集群反饋限制等拒絕,離線任務就提交到該集群。集群排序的演化順序如下:
①初始方案
排隊隊列+輪詢
剩余資源量多的優(yōu)先
優(yōu)點
離線任務優(yōu)先提交到資源最多的集群,保證離線任務運行穩(wěn)定
缺點
對于小集群剩余資源量很小一直分配不到任務容易“餓死”(事實上有的小集群全部資源量都達不到一個大集群的20%)
② 優(yōu)化方案
隨機隊列+排序隊列+輪詢
將資源使用量超過一定比例的集群放到排序隊列,剩余的集群放到隨機隊列
優(yōu)點
離線任務優(yōu)先提交到資源較多的集群,即保證任務的運行穩(wěn)定,隨機的方式也能均勻“喂飽”每個集群
缺點
隨機分配在大任務量時相當于是平均分配,每個集群都會調度差不多的任務量,當前情況會存在整點集中提交大量任務,小集群接收和大集群同樣任務量會抗不住,影響任務執(zhí)行穩(wěn)定和效率,小集群容易“撐死”
③再優(yōu)化方案
加權隨機隊列+排序隊列+輪詢
按剩余資源進行加權隨機,剩余資源多的集群有更多概率分配到任務
優(yōu)點
離線任務優(yōu)先提交到資源較多的集群,“大集群多吃,小集群少吃”,每個集群都能填充同時保證任務的運行穩(wěn)定
④ 最終方案
優(yōu)先隊列(排序)+加權隨機隊列+排序隊列+輪詢
考慮優(yōu)先隊列,無視其他排序規(guī)則,優(yōu)先隊列里的集群將最優(yōu)先,在優(yōu)先隊列中的集群再按資源排序
優(yōu)點
繼承上一方案的優(yōu)點,同時對于特定項目或機房的離線任務,能優(yōu)先調度到某些特定的集群
目前只以內存作為資源水位線的衡量標準,這里的資源量指的是內存量。最開始方案是按集群的剩余資源排序,內存資源剩余多的集群優(yōu)先,缺點是小集群一直分配不到任務容易“餓死”,然后使用隨機的方式也能均勻“喂飽”每個集群,但小集群接收同樣任務量時容易“撐死”,于是隨機隊列按剩余資源進行加權隨機,剩余資源多的集群有更多概率分配到任務,這樣離線任務優(yōu)先提交到資源較多的集群,“大集群多吃,小集群少吃”,每個集群都能填充同時保證任務的運行穩(wěn)定,在此基礎上增加優(yōu)先隊列,無視其他排序規(guī)則,優(yōu)先隊列里的集群將最優(yōu)先,在優(yōu)先隊列中的集群再按資源排序,能優(yōu)先調度到某些特定的集群,形成最終集群選擇排序方案。
五、混部的效果與未來規(guī)劃
經過以上的對Spark組件、K8s混部系統(tǒng)、大數(shù)據(jù)平臺以及彈性調度系統(tǒng)的改造和優(yōu)化,目前混部集群及提交混部的離線任務運行持續(xù)穩(wěn)定,每天任務調度到混部的次數(shù)達10+萬次,在凌晨的高峰期通過混部能為離線任務額外增加數(shù)百TB內存的計算資源,部分混部集群的CPU利用率提升至30%左右,整體收益也是可觀的。
雖然目前vivo的在離線混部達到了一定的規(guī)模,但未來要繼續(xù)提高混部的規(guī)模和收益,還有規(guī)劃一些改進工作。
1、提高離線任務混部規(guī)模。
離線任務混部的節(jié)點是在線業(yè)務提供的,節(jié)點規(guī)模取決于在線業(yè)務峰值,峰值越高那么在業(yè)務低峰期能提供給離線混部資源就越多,因此提高混部規(guī)模的重要因素是提交更多的離線任務。然而目前采用的Spark Operator方案能提交的離線任務只有標準的SparkSql和SparkJar任務,而對于非標準的任務如腳本任務,腳本中除了調用spark-submit提交Spark作業(yè)還有額外的處理邏輯,這類任務還不能直接以Spark Operator的方式提交。事實上Spark作業(yè)更多是來自腳本任務的非標準任務,如果要繼續(xù)增加離線任務的量,就必須把非標準任務也提交到混部,因此后續(xù)是選擇改造spark-submit客戶端支持Spark Operator,或是選擇使用Yarn on K8s,還需要綜合評估。
2、提高離線任務混部收益。
目前混部節(jié)點CPU的平均利用率達到30%,但仍有提升空間。從離線任務的角度來看,一方面是要增加錯峰互補的時間段,例如離線任務的高峰期是02:00到08:00,在線業(yè)務的高峰期是06:00到23:00,在06:00后在線業(yè)務逐步上量開始回收資源,所以離線任務能顯著提高混部集群CPU利用率的黃金時間是有02:00到06:00這4個小時,因此如果能把離線任務高峰期提前到00:00到06:00,混部提效的黃金時間就能達到6小時。所以需要推動離線任務高峰期的前移,對于有依賴鏈路的任務,盡量減少調度時間的間隔,上游任務完成后能盡快調起下游任務,而對于沒有依賴的任務,可以盡量提前調度時間,不過這兩種調整都需要推動業(yè)務方來調整,平臺也可以給予一定的計算成本優(yōu)惠作為激勵。另一方面是要提高混部資源的填充率,Spark任務需要創(chuàng)建大量的Executor Pod,目前混部集群的調度器為了保證調度效率就沒有開啟預選、優(yōu)先策略,事實上Spark的資源粒度比較小更適合填充資源碎片,所以在不影響K8s調度效率的情況下優(yōu)化資源調配策略,把合適的資源粒度的Pod分配到合適的混部節(jié)點,也是提高混部收益的方向。