Apache Kyuubi 在B站大數據場景下的應用實踐

01 背景介紹

近幾年隨着B站業務高速發展,數據量不斷增加,離線計算集羣規模從最初的兩百臺發展到目前近萬臺,從單機房發展到多機房架構。在離線計算引擎上目前我們主要使用Spark、Presto、Hive。架構圖如下所示,我們的BI、ADHOC以及DQC服務都是通過自研的Dispatcher路由服務來實現統一SQL調度,Dispatcher會結合查詢SQL語法特徵、讀HDFS量以及當前引擎的負載情況,動態地選擇當前最佳計算引擎執行任務。如果用戶SQL失敗了會做引擎自動降級,降低用戶使用門檻;其中對於Spark查詢早期我們都是走STS,但是STS本身有很多性能和可用性上的問題,因此我們引入了Kyuubi,通過Kyuubi提供的多租戶、多引擎代理以及完全兼容Hive Thrift協議能力,實現各個部門Adhoc任務的資源隔離和權限驗證。

Query查詢情況

目前在Adhoc查詢場景下,SparkSQL佔比接近一半,依賴Kyuubi對於Scala語法的支持,目前已經有部分高級用戶使用scala語法提交語句執行,並且可以在SQL和Scala模式做自由切換,這大大豐富了adhoc的使用場景。

02 Kyuubi應用

Kyuubi 是網易數帆大數據團隊貢獻給 Apache 社區的開源項目。Kyuubi 主要應用在大數據領域場景,包括大數據離線計算、adhoc、BI等方向。Kyuubi 是一個分佈式、支持多用戶、兼容 JDBC 或 ODBC 的大數據處理服務。

爲目前熱門的計算引擎(例如Spark、Presto或Flink等)提供SQL等查詢服務。

我們選擇Kyuubi的原因:

1. 完全兼容Hive thrift 協議,符合B站已有的技術選型。

2. 高可用和資源隔離,對於大規模的生產環境必不可少。

3. 靈活可擴展,基於kyuubi可以做更多適配性開發。

4. 支持多引擎代理,爲未來統一計算入口打下基礎。

5. 社區高質量實現以及社區活躍。

Kyuubi 的架構可以分成三個部分:

1.客戶端: 用戶使用jdbc或者restful協議來提交作業獲取結果。

2.kyuubi server: 接收、管理和調度與客戶端建立的Kyuubi Session,Kyuubi Session最終被路由到實際的引擎執行。

3.kyuubi engine: 接受處理 kyuubi server發送過來的任務,不同engine有着不同的實現方式。

03 基於Kyuubi的改進

Kyuubi已經在B站生產環境穩定運行一年以上,目前所有的Adhoc查詢都通過kyuubi來接入大數據計算引擎。 在這一年中我們經歷了兩次大版本的演進過程,從最初kyuubi 1.3到kyuubi 1.4版本,再從kyuubi 1.4升級kyuubi 1.6版本。與之前的STS相比,kyuubi在穩定性和查詢性能方面有着更好的表現。在此演進過程中,我們結合B站業務以及kyuubi功能特點,對kyuubi進行部分改造。

3.1 增加QUEUE模式

Kyuubi Engine原生提供了CONNECTION、USER、GROUP和SERVER多種隔離級別。在B站大數據計算資源容量按照部門劃分,不同部門在Yarn上對應不同的隊列。我們基於GROUP模式進行了改造,實現Queue級別的資源隔離和權限控制。

用戶信息和隊列的映射由上層工具平臺統一配置和管理,Kyuubi只需關心上游Dispatcher提交過來user和queue信息,進行調度並分發到對應隊列的spark engine上進行計算。目前我們有20+個adhoc隊列,每個隊列都對應一個或者多個Engine實例(Engine pool)。

3.2 在QUEUE模式下支持多租戶

kyuubi server端由超級用戶Hive啓動,在spark場景下driver和executor共享同一個的用戶名。不同的用戶提交不同的sql, driver端和executor端無法區分當前的任務是由誰提交的,在數據安全、資源申請和權限訪問控制方面都存在着問題。

針對該問題,我們對以下幾個方面進行了改造。

3.2.1 kyuubi server端

1. kyuubi server以hive principal身份啓動。

2. Dispatcher以username proxyUser身份提交SQL。

3.2.2 spark engine端

1. Driver和Executor以hive身份啓動。

2. Driver以username proxyUser身份提交SQL。

3. Executor啓動Task線程需要以username proxyUser身份執行Task。

4. 同時需要保證所有的公共線程池,綁定的UGI信息正確。如ORC Split線程池上,當Orc文件達到一定數量會啓用線程池進行split計算,線程池是全局共享,永久綁定的是第一次觸發調用的用戶UGI信息,會導致用戶UGI信息錯亂。

3.3 kyuubi engine UI 展示功能

在日常使用中我們發現 kyuubi 1.3 Engine UI頁面展示不夠友好。不同的用戶執行不同的SQL無法區分的開,session 、job、stage、task無法關聯的起來。

導致排查定位用戶問題比較困難,我們借鑑STS拓展了kyuubi Engine UI頁面。我們對以下幾個方面進行了改造。

1. 自定義kyuubi Listener監聽Spark Job、Stage、Task相關事件以及SparkSQL相關事件:SessionCreate、SessionClose、executionStart、executionRunning、executionEnd等

2. Engine執行SQL相關操作時,綁定併發送相關SQL Event,構造SQL相關狀態事件,將採集的Event進行狀態分析、彙總以及存儲。

3. 自定義Kyuubi Page進行Session以及SQL相關狀態實時展示。

Session Statistics信息展示

SQL Statistics信息展示

3.4 kyuubi支持配置中心加載Engine參數

爲了解決隊列之間計算資源需求的差異性,如任務量大的隊列需要更多計算資源(Memory、Cores),任務量小的隊列需要少量資源,每個隊列需求的差異,我們將所有隊列的Engine相關資源參數統一到配置中心管理。每個隊列第一次啓動Engine前,將查詢自己所屬隊列的參數並追加到啓動命令中,進行參數的覆蓋。

3.5 Engine執行任務的進度顯示與消耗資源上報功能

任務在執行過程中,用戶最關心的就是自己任務的進度以及健康狀況,平臺比較關心的是任務所消耗的計算資源成本。我們在Engine端,基於事件採集user、session、job、stage信息並進行存儲,啓動定時任務將收集的user、session、job、stage信息進行關聯並進行資源消耗成本計算,並將結果注入對應operation log中, 回傳給前端日誌展示。

任務進度信息展示

查詢消耗資源上報展示

04 Kyuubi穩定性建設

4.1 大結果集溢寫到磁盤

在adhoc 場景中用戶通常會拉取大量結果到 driver 中,同一時間大量的用戶同時拉取結果集,會造成大量的內存消耗,導致spark engine內存緊張,driver性能下降問題,直接影響着用戶的查詢體驗,爲此專門優化了driver fetch result 的過程,在獲取結果時會實時監測driver內存使用情況,當driver內存使用量超過閾值後會先將拉取到的結果直接寫出到本地磁盤文件中,在用戶請求結果時再從文件中分批讀出返回,增加driver的穩定性。

4.2 單個 SQL 的task併發數、執行時間和 task 數量的限制

在生產過程中,我們經常性的遇到單個大作業直接佔用了整個Engine的全部計算資源,導致短作業長時間得不到計算資源,一直 pending的情況,爲了解決這種問題我們對以下幾個方面進行優化。

  • Task併發數方面:默認情況下Task調度時只要有資源就會全部調度分配出去,後續SQL過來就面臨着完全無資源可用的情況,我們對單個SQL參與調度的task數進行了限制,具體的限制數隨着可用資源大小進行動態調整。
  • 單個 SQL 執行時間方面:上層Dispatcher和下層Engine都做了超時限制,規定adhoc任務超過1小時,就會將該任務kill掉。
  • 單個Stage task數量方面:同時我們也對單個stage的task數進行限制,一個stage最大允許30W個task。

4.3 單次 table scan 的文件數和大小的限制

爲保障kyuubi的穩定性,我們對查詢數據量過大的SQL進行限制。通過自定義外部optimization rule(TableScanLimit)來達到目的。TableScanLimit匹配LocalLimit,收集子節點project、filter。匹配葉子結點HiveTableRelation和HadoopFsRelation。即匹配Hive表和DataSource表的Logical relation,針對不同的表採取不同的計算方式。

1. HiveTableRelation:

  • 非分區表, 通過table meta 拿到表的totalSize、numFiles、numRows值。
  • 分區表,判斷是否有下推下來的分區。若有,則拿對應分區的數據 totalSize、numFiles、numRows。若沒有,則拿全表的數據。

2. HadoopFsRelation:判斷partitionFilter是否存在動態filter

  • 不存在,則通過partitionFilter得到需要掃描的分區
  • 存在,則對partitionFilter掃描出來的分區進一步過濾得到最終需要掃描的分區

獲取到SQL查詢的dataSize、numFiles、numRows後, 還需要根據表存儲類型、不同字段的類型、是否存在limit、在根據下推來的project、filter 得出最終需要掃描的列,估算出需要table scan size,如果table scan size超過制定閾值則拒絕查詢並告知原因。

4.4 危險join condition發現&Join膨脹率的限制

4.4.1 危險join condition發現

爲保障kyuubi的穩定性,我們也對影響Engine性能的SQL進行限制。用戶在寫sql時可能並不瞭解spark對於join的底層實現,可能會導致程序運行的非常慢甚至OOM,這個時候如果可以爲用戶提供哪些join condition可能是導致engine運行慢的原因,並提醒用戶改進和方便定位問題,甚至可以拒絕這些危險的query提交。

在選擇 join 方式的時候如果是等值 join 則按照 BHJ,SHJ,SMJ 的順序選擇,如果還沒有選擇join type則判定爲 Cartesian Join,如果 join 類型是InnerType的就使用 Cartesian Join,Cartesian Join會產生笛卡爾積比較慢,如果不是 InnerType,則使用 BNLJ,在判斷 BHJ 時,表的大小就超過了broadcast 閾值,因此將表broadcast出去可能會對driver內存造成壓力,性能比較差甚至可能會 OOM,因此將這兩種 join 類型定義爲危險 join。

如果是非等值 join 則只能使用 BNLJ 或者 Cartesian Join,如果在第一次 BNLJ 時選不出 build side 說明兩個表的大小都超過了broadcast閾值,則使用Cartesian Join,如果Join Type不是 InnerType 則只能使用 BNLJ,因此Join策略中選擇Cartesian Join和第二次選擇 BNLJ 時爲危險 join。

4.4.2 Join膨脹率的限制

在shareState 中的 statusScheduler 用於收集 Execution 的狀態和指標,這其中的指標就是按照 nodes 彙總了各個 task 彙報上來的 metrics,我們啓動了一個 join檢測的線程定時的監控 Join 節點的 "number of output rows"及Join 的2個父節點的 "number of output rows" 算出該 Join 節點的膨脹率。

Join 節點的膨脹檢測:

05 kyuubi 新應用場景

5.1 大查詢connection&scala模式的使用

5.1.1 connection模式的使用

adhoc大任務和複雜的SQL會導致kyuubi engine在一定時間內性能下降,嚴重影響了其他正常的adhoc任務的執行效率。我們在adhoc前端開放了大查詢模式,讓這些複雜、查詢量大的任務走kyuubi connection模式。在kyuubi connection模式下一個用戶任務單獨享有自己申請的資源,獨立的Driver,任務的大小快慢都由自身的SQL特徵決定,不會影響到其他用戶的SQL任務,同時我們也會適當放開前面一些限制條件。

connection 模式在B站的使用場景:

  1. table scan判定該adhoc任務爲大任務,執行時間超過1個小時。
  2. 複雜的SQL任務, 該任務存在笛卡爾積或Join膨脹超過閾值。
  3. 單個SQL單個stage的task數超過30W。
  4. 用戶自行選擇connection模式。

5.1.2 scala模式的使用

SQL模式可以解決大數據80%的業務問題,SQL模式加上Scala模式編程可以解決99%的業務問題;SQL是一種非常用戶友好的語言,用戶不用瞭解Spark內部的原理,就可以使用SQL進行復雜的數據處理,但是它也有一定的侷限性。

SQL模式不夠靈活,無法以dataset以及rdd兩種方式進行數據處理操作。無法處理更加複雜的業務,特別是非數據處理相關的需求。另一方面,用戶執行scala code項目時必須打包項目並提交到計算集羣,如果code出錯了就需來回打包上傳,非常的耗時。

Scala模式可以直接提交code,類似Spark交互式Shell,簡化流程。針對這些問題, 我們將SQL模式、Scala模式的優點結合起來,兩者進行混合編程,這樣基本上可以解決數據分析場景下大部分的case。

5.2 Presto on spark

Presto爲了保證集羣的穩定性,每個Query的最大內存進行了限制,超過配置內存的Query會被Presto oom kill掉。部分ETL任務會出現隨着業務增長,數據量增大,佔用內存也會增多,當超過閾值後,流程就會出現失敗。

爲了解決這個問題,prestodb社區開發了一個presto on spark的項目,通過將query提交到Spark來解決query的內存佔用過大導致的擴張性問題,但是社區方案對於已經存在的查詢並不是很友好,用戶的提交方式有presto-cli、pyhive等方式,而要使用Presto on spark項目,則必須通過spark-submit方式將query提交到yarn。

爲了讓用戶無感知的執行presto on spark查詢,我們在presto gateway上做了一些改造,同時藉助kyuubi restulful的接口,和service + engine的調度能力,在kyuubi內開發了Presto-Spark Engine,該engine能夠比較友好的來提交查詢到Yarn。

主要實現細節如下:

1. presto gateway將query的執行歷史進行保存,包括query的資源使用情況、報錯信息等。

2. presto gateway請求HBO服務,判斷當前query是否需要通過presto on spark提交查詢。

3. presto gateway通過zk獲取可用的kyuubi server列表,隨機選擇一臺,通過http向kyuubi open一個session。

4. presto gateway根據獲取到的sessionHandle信息,再提交語句。

5. kyuubi server接收到query後,會啓動一個獨立的Presto-Spark Engine,構建啓動命令,執行命令提交spark-submit 到yarn。

6. Presto gateway根據返回的OperatorHandle信息, 通過http不斷獲取operation status。

7. 作業成功,則通過fetch result請求將結果獲取並返回給客戶端。

06 kyuubi部署方式

6.1 Kyuubi server接入K8S

整合 Engine on yarn label的實踐

生產實踐中遇到的問題:

1.目前kyuubi server/engine部署在混部集羣上,環境複雜,各組件環境相互依賴、發佈過程中難免會存在環境不一致、誤操作等問題,從而導致服務運行出錯。

2.資源管理問題。最初engine使用的是client模式,不同的隊列的engine driver使用的都是大內存50g-100g不等 ,同時AM、NM 、DN、kyuubi server都共享着同一臺物理機器上的資源,當AM啓動過多, 佔滿整個機器的資源,導致機器內存不足,engine無法啓動。

針對於該問題,我們研發了一套基於Queue模式資源分配調度實現:每個kyuubi server 和 spark engine在znode上都記錄着當前資源使用情況。每個kyuubi server znode信息:當前kyuubi註冊SparkEngine數量、當前kyuubi server註冊SparkEngine實例、kyuubi server內存總大小以及當前kyuubi server剩餘內存總大小等。

每個engine znode信息:所屬kyuubi server IP/端口、當前SparkEngine內存、當前SparkEngine所屬隊列等 。每次Spark engine的啓動/退出,都會獲取該隊列的目錄鎖,然後對其所屬的kyuubi server進行資源更新操作。kyuubi server如果宕機,在啓動時,遍歷獲取所有engine在znode的信息,進行資源和狀態的快速恢復。

3.針對資源管理功能也存在着一些問題: 資源碎片化問題、新功能的拓展不友好以及維護成本大。Engine使用的是client模式,過多大內存的AM會佔用客戶端的過多計算資源,導致engine水平拓展受限。

針對以上提出的問題,我們做了對應的解決方案:

1. kyuubi server接入k8s

我們指定了一批機器作爲kyuubi server在k8s上調度資源池,實現kyuubi server環境、資源的隔離。實現了kyuubi server快速部署、提高kyuubi server水平擴展能力,降低了運維成本。

2. Engine on yarn label

我們將kyuubi engine資源管理交給yarn,由yarn負責engine的分配和調度。我們採用了cluster模式以防engine在水平拓展時受到資源限制。採用cluster模式後,我們遇到了新的問題:在queue模式下engine driver使用的都是50g-100g不等的大內存,但是由於yarn集羣的配置限制,能夠申請的最大Container資源量爲<28G, 10vCore>。爲了在cluster模式的情況下保證Driver能夠獲取到足夠的資源,我們改造了yarn以適應此類場景。我們將需求拆分爲以下三項:

  • 將kyuubi Driver放置於獨立的Node Label中,該Node Label中的服務器由kyuubi driver獨立使用;
  • kyuubi Executor仍然放置在Default Label的各對應隊列的adhoc葉子隊列內,承接adhoc任務處理工作;
  • Driver申請的資源需要大於MaxAllocation,即上文所述的<28G, 10vCore>。希望能夠根據Node Label動態設置Queue級別的MaxAllocation,使得kyuubi Driver能夠獲得較大資源量。

首先,我們在yarn上建立了kyuubi_label,並在label內與Default Label映射建立kyuubi隊列,以供所有的Driver統一提交在kyuubi隊列上。並通過“spark.yarn.am.nodeLabelExpression=kyuubi_label”指定Driver提交至kyuubi_label,通過“spark.yarn.executor.nodeLabelExpression= ”指定Executor提交至default label,實現如下的效果:

其次,我們將yarn的資源最大值由原先的“集羣”級別管控下放至“隊列+Label”級別管控,通過調整"queue name + kyuubi_label"的Conf,我們能夠將Driver的Container資源量最大值提高至<200G, 72vCore>,且保證其他Container的最大值仍爲<28G, 10vCore>。同樣申請50G的Driver,在default集羣中會出現失敗提示:

而在kyuubi_lable的同隊列下則能夠成功運行, 這樣我們既藉助了yarn的資源管控能力,又保證了kyuubi driver獲得的資源量。

07 未來規劃

1. 小的ETL任務接入kyuubi,減少ETL任務資源申請時間

2. Kyuubi Engine(Spark和Flink)雲原生,接入K8S統一調度

3. Spark jar 任務也統一接入Kyuubi

以上是今天的分享內容,如果你有什麼想法或疑問,歡迎大家在留言區與我們互動,如果喜歡本期內容的話,請給我們點個贊吧!

本文轉載自:Apache Kyuubi 在 B 站大數據場景下的應用實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章