作者:公尺哈游大資料開發。
近年來,隨著容器、微服務、Kubernetes 等各種雲原生技術的成熟,越來越多的企業開始擁抱雲原生,開始在雲原生上部署和執行人工智慧、大資料等企業應用。 以Spark為例,在雲上執行Spark可以充分享受公有雲的彈性資源、運維控制、儲存服務等,業界湧現出許多Spark on Kubernetes的最佳實踐。
在剛剛結束的2024年雲棲大會上,來自公尺哈遊資料平台集團的大資料技術專家杜安明分享了公尺哈游大資料架構向雲原生公升級過程中的目標、探索和實踐,以及如何通過基於阿里雲容器服務(ACK)的Spark on K8S架構,在彈性計算、節約成本、存算分離等方面獲得價值。
隨著公尺哈遊業務的快速發展,大資料離線資料儲存和計算任務量快速增長,早期的大資料離線架構已無法滿足新的場景和需求。
為了解決原有架構彈性不足、運維複雜、資源利用率低等問題,2024年下半年開始研究大資料基礎設施的云原生化,最終在阿里雲上實現了Spark on K8s + OSS-HDFS解決方案。
1.彈性計算
由於遊戲業務會進行週期性的版本更新、上線活動、新遊戲的上線,離線計算資源的需求和消耗波動較大,可能是平時水位的幾十倍或幾百倍。 利用 K8S 集群的天然彈性,可以將 Spark 計算任務定為在 K8S 上執行,可以輕鬆解決此類場景下的資源消耗高峰問題。
2.節省成本
依託阿里雲容器服務Kubernetes版(ACK)集群的強彈性,所有計算資源均按量付費進行請求和釋放,再加上我們對Spark元件的定製化改造,以及ECI Spot例項的充分利用,在承載相同計算任務和資源消耗的情況下,成本最高可降低50%。
3.儲存和計算分離
Spark執行在K8S上,充分利用K8S集群的計算資源,接入資料逐步從HDFS和OSS切換到OSS-HDFS,中間隨機資料的讀寫為Celeborn,實現了計算和儲存的解耦,易於維護和擴充套件。
眾所周知,Spark 引擎可以支援並執行在多種資源管理器之上,例如 yarn、k8s、mesos 等。 在大資料場景下,國內大部分企業仍然在YARN集群上執行Spark任務,Spark在2版本 3 首次支援 K8s,Spark3 於 2021 年 3 月發布版本 1 正式正式發布。
相較於 YARN,Spark 在 K8S 上起步較晚,雖然在成熟度和穩定性上還存在一些不足,但 Spark on K8s 能夠實現彈性計算、節約成本等非常突出的好處,所以各大公司也在不斷嘗試和探索,在這個過程中,Spark on K8s 的運營架構也在不斷迭代地向前演進。
1.離線混合零件
目前,大多數公司仍然使用混合部署方式在 K8S 上執行 Spark 任務。 該架構的設計原則是,不同的業務系統會有不同的高峰時段。 大資料離線業務系統的典型高峰期為凌晨0:00-9:00,各類應用微服務、Web提供的BI系統等業務的常見高峰期為白天時間,在此時間之外的其他時間,可以將業務系統的機器節點新增到Spark使用的K8S命名空間中。 如下圖所示,Spark 等應用服務部署在一組 K8s 集群上。
這種架構的優點是可以通過離線服務的混合部署和非高峰運營,提高機器資源的利用率,降低成本,但缺點也很明顯,即架構實現複雜,維護成本相對較高,難以實現嚴格的資源隔離, 尤其是網路層面的隔離,業務之間難免會有一定的互動。
2. spark on k8s + oss-hdfs
考慮到離線混合部署的弊端,我們設計並採用了更符合雲原生的全新實現架構:底層儲存採用OSS-HDFS(Jindofs),計算集群採用阿里雲容器服務(ACK),Spark功能相對豐富,相對穩定2.3個版本。
OSS-HDFS完全相容HDFS協議,提供無限的OSS容量、冷熱資料儲存、目錄原子性和毫秒級重新命名操作。
阿里雲ACK集群提供高效能、可擴充套件的容器應用管理服務,可支援企業級Kubernetes容器化應用的生命週期管理。
架構簡單易維護,底層利用ECI彈性,讓Spark任務輕鬆應對峰值流量,排程Spark的執行器執行在ECI節點上,可以最大限度提公升計算任務的彈性,達到最佳降本效果。
1.理由
在開始實現之前,我們先簡單解釋一下 Spark 如何在 K8S 上執行的基本原理。 Pod 是 K8S 中最小的排程單元,Spark 任務的驅動和執行器是乙個獨立的 Pod,每個 Pod 被分配乙個唯一的 IP 位址,Pod 可以包含乙個或多個容器,無論是驅動還是執行器 JVM 程序,在容器中啟動、執行和銷毀。
將 Spark 任務提交到 K8S 集群後,首先啟動驅動 pod,然後驅動會按需向 apiserver 申請執行器,執行器會執行具體任務,作業完成後,驅動負責清理所有執行器 pod。
2.執行過程
下圖顯示了完整的作業執行流程,使用者完成 Spark 作業開發後,使用者將任務發布到排程系統並配置相關執行引數,排程系統會定期將任務提交到自研的啟動器中介軟體,中介軟體會呼叫 spark-k8s-cli,最後 CLI 將任務提交到 K8S 集群。 任務提交成功後,Spark Driver Pod 首先啟動並申請到集群中分配乙個 Executor Pod,在執行特定任務時,該 Pod 會訪問外部 Hive、Iceberg、Olap 資料庫、OSS-HDFS 等多個大資料元件並與之互動,而 Spark Executor 之間的資料洗牌則由 Celeborn 實現。
3.任務提交
至於如何將 Spark 任務提交到 K8S 集群,不同的公司有不同的做法,下面我們簡單介紹一下目前比較常見的做法,然後介紹一下目前線上提交和管理任務的方法。
3.1 使用原生 spark-submit
Spark原生支援這種通過spark-submit命令直接提交的方式,整合起來比較簡單,符合使用者的習慣,但不適合用於跟蹤和管理作業狀態,自動配置Spark UI的服務和入口,以及任務完成後自動清理資源。
3.2 使用 spark-on-k8s-operator
這是提交作業的常用方式,其中 K8S 集群需要提前安裝 spark-operator,客戶端通過 kubectl 提交 YAML 檔案來執行 spark 作業。 實質上這是原生模式的擴充套件,最終的作業提交仍是 spark-submit 模式,擴充套件功能包括:作業管理、服務入口建立和清洗、任務監控、Pod 增強等。 這種方法可以在生產環境中使用,但與大資料排程平台整合得不到很好的,使用起來比較複雜,對於不熟悉K8S的使用者來說門檻也比較高。
3.3 使用 spark-k8s-cli
在生產環境中,我們使用 spark-k8s-cli 提交任務。 spark-k8s-cli本質上是乙個可執行檔案,基於阿里雲的emr-spark-ack提交工具進行了重構、增強和深度定製。
spark-k8s-cli 結合了 spark-submit 和 spark-operator 作業提交方式的優點,使得所有作業都可以通過 spark-operator 進行管理,支援執行互動式 spark-shell 和本地依賴提交,使用與原生 spark-submit 相同的語法。
線上使用之初,我們所有任務的 Spark Submit JVM 程序都是在 Gateway Pod 中啟動的,使用一段時間後,我們發現這個方法不夠穩定,一旦 Gateway Pod 出現異常,上面的所有 Spark 任務都會失敗,Spark 任務的日誌輸出也不好管理。 針對這種情況,我們將 spark-k8s-cli 改為對每個任務使用單獨的提交 pod,提交 pod 適用於啟動任務的驅動,提交 pod 和驅動 pod 一樣執行在固定的 ecs 節點上,提交 pod 之間是完全獨立的,任務結束後會自動釋放提交 pod。 spark-k8s-cli 的工作原理如下圖所示。
關於 spark-k8s-cli,除了上面提到的基本任務提交外,我們還做了一些其他的增強和自定義。
支援將任務提交到同一地域的多個不同 k8s 集群,實現集群間的負載均衡和故障轉移,在資源不足時實現類似 yarn 的自動排隊功能(k8s 如果設定了資源配額,當配額達到上限時,任務會直接失敗),增加與 k8s 進行網路通訊等異常處理, 建立或啟動失敗等重試,對偶發的集群抖動做出響應, 網路異常容錯支援根據不同部門或業務線對大規模補體任務進行限流和管理,並內嵌任務提交失敗、容器建立或啟動失敗、執行超時等告警功能4.日誌收集和顯示
K8S 集群本身並不像 yarn 那樣提供日誌自動聚合和顯示的功能,驅動和執行器的日誌採集需要使用者自己完成。 目前比較常見的解決方法是在每個 k8s 節點上部署 agent,通過 agent,將日誌收集並丟棄在第三方儲存上,如 ES、SLS 等,但這些方法對於習慣在 yarn 頁面點選檢視日誌的使用者和開發者來說非常不方便,使用者不得不跳轉到第三方系統來檢索和檢視日誌。
為了方便檢視 K8S Spark 任務日誌,我們修改了 Spark **,使驅動和執行器日誌最終都輸出到 OSS,使用者可以直接點選檢視 Spark UI 和 Spark JobHistory 上的日誌檔案。
當Spark任務啟動時,驅動和執行器都會註冊乙個關機鉤子,當任務結束,JVM退出時,呼叫鉤子方法將完整的日誌上傳到OSS。 另外,如果想要完整檢視日誌,需要對 Spark 的作業歷史記錄進行一些修改,需要在歷史記錄頁面顯示 stdout 和 stderr,點選日誌時從 OSS 中拉取對應驅動程式或執行器的日誌檔案,最後瀏覽器會渲染檢視。 此外,對於正在執行的任務,我們會向使用者提供乙個 Spark Running Web UI,任務提交成功後,spark-operator 會自動生成服務和 ingress,供使用者檢視執行詳情,可以通過訪問 K8s API 拉取對應 pod 的執行日誌來獲取日誌。
5.靈活性和成本降低
基於ACK集群提供的彈性伸縮能力,以及ECI的充分利用,在K8S中同等規模執行Spark任務的總成本明顯低於YARN固定集群,資源利用率大幅提公升。
ECI和ECS最大的區別在於ECI是按秒計費的,應用和發布速度也是秒級的,所以ECI非常適合Spark這樣的計算場景,負載波峰和波谷都很明顯。
困境是集群中安裝了ack-virtual-node元件,配置了vswitch等資訊,任務執行時,執行器被排程到虛擬節點,虛擬節點申請建立和管理ECI。
ECI分為普通例項和搶占式例項,搶占式例項是低成本的競價型例項,預設保護期為1小時,適用於大多數Spark批處理場景。 為了進一步提高降本效果,充分利用搶占式例項的優勢,我們對Spark進行了改造,實現了ECI例項型別的自動轉換。 當例項不足或由於庫存不足或其他原因無法建立搶占式例項時,系統會自動切換到通用ECI例項,以保證任務的正常執行。 實現原理和轉換邏輯如下圖所示。
6. celeborn
由於 k8s 節點的磁碟容量較小,且節點用完後申請和釋放,導致無法儲存大量 Spark shuffle 資料。 如果將雲盤掛載到 Executor Pod 上,掛載的磁碟大小很難確定,並且由於資料傾斜等因素,磁碟使用率會較低,使用起來更加複雜。 此外,雖然 Spark 社群在 32 提供重複使用pvc等功能,但經過調查發現功能不齊全,穩定性不足。
為了解決 K8S 上 Spark 資料洗牌的問題,在對多款開源產品進行充分調研對比後,最終採用了阿里巴巴開源的 Celeborn 解決方案。 Celeborn 是乙個獨立的服務,專門用於儲存 Spark 的中間洗牌資料,使 executor 不再依賴本地磁碟,並且該服務可以被 k8s 和 yarn 使用。 Celeborn 使用 Push Shaffle 模式,隨機播放過程為追加寫入和順序讀取,以提高資料讀寫效能和效率。
基於開源的 Celeborn 專案,我們還在資料網路傳輸方面做了一些內部工作,如功能增強、指標豐富、監控告警改進、bug修復等,現已形成內部穩定版本。
7. kyuubi on k8s
Kyuubi 是乙個分布式的多租戶閘道器,可以為 Spark、Flink 或 Trino 等提供 SQL 等查詢服務。 在早期,我們的 Spark Adhoc 查詢被傳送到 Kyuubi 執行。 為了解決 YARN 佇列資源不足,使用者查詢 SQL 無法提交和執行的問題,我們也支援 Kyuubi Server 在 K8S 上的部署和執行,當 YARN 資源不足時,Spark 查詢會自動切換到 K8s 執行。 鑑於 YARN 集群規模不斷縮小,無法保證查詢資源,以及相同的使用者查詢體驗,我們已將所有 SparkSQL Adhoc 查詢提交到 K8S 執行。
為了讓你的 adhoc 查詢在 K8S 上流暢執行,我們還對 Kyuubi 做了一些原始碼修改,包括 docker-image-toolsh、deployment.重寫YAML和Dockfile檔案,將日誌重定向到OSS,支援Spark Operator管理,許可權控制,輕鬆檢視任務執行UI。
8. k8s manager
在 Spark on K8S 場景中,雖然 K8S 有集群級別的監控和告警,但並不能完全滿足我們的需求。 在生產中,我們更關注集群上的 Spark 任務、Pod 狀態、資源消耗和 ECI。 利用 K8S 的 watch 機制,我們實現了自己的監控告警服務 K8S Manager,如下圖所示。
K8sManager 是乙個內部實現的比較輕量級的 Spring Boot 服務,實現的功能是監聽和彙總每個 k8s 集群上的 pod、quota、service、configmap、ingress、role等各種資源資訊,從而生成自定義的指標指標,並對指標進行展示和告警,包括集群的總 CPU 和記憶體使用率, 當前執行的Spark任務數量、Spark任務記憶體資源消耗和執行時長排名靠前的統計、單日Spark任務彙總、集群中的Pod總數、Pod狀態、ECI機器型號和可用區的分布統計、過期資源監控等。
9.其他工作
9.1、排程任務自動切換。
在我們的排程系統中,Spark 任務可以配置三種執行策略:yarn、k8s 和 auto。 如果 user 任務指示需要執行的資源管理器,則該任務只會在 yarn 或 k8s 上執行,如果使用者選擇 auto,則任務將在 ** 中執行,具體取決於當前 yarn 佇列的資源使用情況,如下圖所示。 由於任務總數較大,且 Hive 任務不斷遷移到 Spark,部分任務仍在 yarn 集群上執行,但最終形態下所有任務都將由 k8s 託管。
9.2 多區域、多交換機支援。
ECI在Spark任務執行過程中被廣泛使用,成功建立ECI有兩個前提條件: 1.可以申請IP位址2.當前區域有庫存。 實際上,單個交換機提供的可用 IP 數量是有限的,單個可用區擁有的搶占式例項總數也是有限的。
9.3 成本核算。
由於在提交 Spark 任務時已經明確指定了每個執行器的 CPU、記憶體等模型資訊,因此我們可以在關閉 SparkContxt 之前,從任務關閉前從任務中獲取每個執行器的實際執行時間,然後結合單價計算出 Spark 任務的大致成本。 由於 ECI Spot 例項會隨市場和庫存水平而波動,因此以這種方式計算的每次作業成本是乙個上限值,主要用於反映趨勢。
9.4. 優化 Spark 運算元
但隨著任務數量的增加,Operator 對各種事件的處理速度越來越慢,甚至無法及時清理 configmap、ingress、service 等任務執行過程中產生的大量資源,導致新提交的 Spark 任務的 Web UI 堆積。 發現問題後,我們調整了運算元的協程數量,實現了 Pod 事件的批處理、不相關事件的過濾、TTL 刪除等功能,解決了 Spark Operator 效能不足的問題。
9.5 公升級 Spark K8S 客戶端
spark3.2.2 Fabric8(Kubernetes j**a 客戶端)用於訪問和操作 K8s 集群中的資源,預設客戶端版本為 54.1. 在這個版本中,當任務結束時釋放執行器時,驅動程式會向 K8s APISer 傳送大量刪除 Pod 的 API 請求,這將給集群 APISer 和 etcd 帶來更大的壓力,APISer 的 CPU 會瞬間飆公升。
我們當前的內部 Spark 版本已將 kubernetes-client 公升級到 62.0. 支援批量刪除 Pod,解決了 Spark 任務成套發布時大量刪除 API 請求導致的集群抖動問題。
在 K8S 上設計和實現 Spark 的整個過程中,我們也遇到了各種各樣的問題、瓶頸和挑戰。
1.彈性網絡卡發布緩慢
彈性網絡卡發布速度慢是大規模ECI應用場景下的效能瓶頸,導致交換機上的IP位址被大幅消耗,最終導致Spark任務卡頓或提交失敗,如下圖所示。 目前,阿里雲團隊已經通過技術公升級解決了這個問題,大大提高了發布速度和整體效能。
2.觀察程式無效
當 Spark 任務啟動驅動時,會在執行器 *** 上建立乙個事件,實時獲取所有執行器的執行狀態,對於一些長時間執行的 Spark 任務,這個 *** 經常會因為資源過期、網路異常等原因而失敗,所以在這種情況下,需要重置 watcher,否則任務可能會跑掉。 此問題是 Spark 中的乙個 bug,已在我們的構建中修復,並且 PR 可供 Spark 社群使用。
3.任務卡住了
如上圖所示,驅動程式通過 list 和 watch 兩種方式獲取執行器的健康狀態。 Watch 採用被動監聽機制,但由於網路問題等問題,可能會遺漏或處理事件,但這種概率相對較低。 List 是乙個主動請求,例如,每隔 3 分鐘,驅動就可以從 apiserver 請求當前完全執行自己任務的資訊。
由於 list 請求任務的所有 pod 資訊,當任務較多時,頻繁的 list 會給 k8s 的 apiserver 和 etcd 帶來很大的壓力。 當 Spark 任務執行異常時,例如有很多執行器 OOM,有一定概率是驅動手錶的資訊會不正確,雖然任務還沒有執行,但是驅動會不再申請執行器執行任務,任務會卡住。 我們的解決方案如下:
在開啟 watch 機制的同時,也開啟了 list 機制,並延長了 list 時間間隔,每 5 分鐘請求修改一次 executorpodspollingsnapshotsource,允許 apiserver 伺服器快取並從快取中獲取完整的 pod 資訊,減輕集群上列表的壓力4.Celeborn 讀寫超時,失敗
ApacheceleBorn是阿里巴巴的開源產品,前身為RSS(Remote Shuffle Service)。 前期還略顯缺乏成熟度,對網路延遲、丟包異常處理等的處理,導致部分 Spark 任務出現大量洗牌資料執行時間甚至任務失敗。
優化 Celeborn,形成內部版本,改進網路資料包傳輸,優化 Celeborn master 和 worker 的相關引數,提公升 shuffle 資料的讀寫效能,公升級 ECI 底層映象版本,修復 ECI Linux 核心 bug5.批量提交任務時,配額鎖衝突
為了防止資源被無限期使用,我們為每個 k8s 集群設定了配額上限。 在 k8s 中,配額也是一種資源,每個 Pod 應用和發布都會修改配額的內容(cpu 記憶體值),當多個任務併發提交時,可能會發生配額鎖定衝突,從而影響任務驅動的建立,無法啟動任務。
針對這種情況導致的任務啟動失敗,我們修改了 Spark Driver Pod 的建立邏輯,增加了可配置的重試引數,當檢測到 Driver Pod 建立是配額鎖衝突導致時重試建立。 執行器 pod 的建立也可能因為配額鎖衝突而失敗,可以忽略,驅動程式會自動申請新建乙個,相當於自動重試。
6.批量提交任務時,unknownhost報錯
當大量任務瞬間批量提交到集群時,會同時啟動多個提交 Pod,同時申請 Terway 元件繫結彈性網絡卡的 IP 位址,Pod 啟動有一定概率, 並且彈性網絡卡會繫結成功但未完全就緒,Pod的網路通訊功能將無法正常使用,任務訪問核心DNS時無法傳送請求,Spark任務會報錯unknownhost並執行失敗。我們可以通過以下兩種措施來避免和解決這個問題:
對於每個 ECS 節點,分配乙個 Terway Pod 開啟 Terway 快取功能,提前分配 IP 位址和彈性網絡卡,直接從快取池中獲取新的 Pod,使用後返回快取池7.可用區間網路丟包
為了保證充足的庫存,每個K8S集群都配置了多個可用區,但跨可用區網路通訊的穩定性略差於同一可用區之間,即使用可用區之間存在一定的丟包概率,表現為任務執行時間不穩定。
如果跨可用區出現網路丟包,可以嘗試將ECI排程策略設定為vSwitchOrdered,這樣乙個任務的所有執行器基本都在乙個可用區,避免了不同可用區執行器之間通訊異常導致的任務執行時間不穩定的問題。
最後,感謝阿里雲容器、ECI、EMR等相關團隊的同學們,在技術解決方案的實施和實際遷移過程中,給予了寶貴的建議和專業的技術支援。
目前,新的雲原生架構已經在生產環境中穩定執行了近一年左右,未來我們將繼續優化和完善整體架構,主要集中在以下幾個方面:
1.我們將持續優化整體雲原生解決方案,進一步提公升系統承載和容災能力。
2.雲原生架構公升級,更多大資料元件容器化,整體架構更加雲原生。
3.更細粒度的資源管理,精準的成本控制。