導讀:在日常線上生產環境中有大量的資料需要被訪問,為了保證資料同步以及查詢效率,需要耗費較大的資源,同時,很多被查詢和訪問的資料是重複的,這對資料庫系統造成極大壓力。
為了解決這一問題,B 站採用 Presto 搭配 Alluxio 的模式來提高系統效能,本次分享從以下四方面展開講解:分享嘉賓|楊洋 bilibili 大資料開發工程師
編輯整理|張瑋
出品社群|DataFun
01
B 站離線叢集架構
B 站 SQL On Hadoop 的架構,整體由 5 個部分組成:- 最上層服務端,包括 BI 報表、資料質量校驗、ADHOC 查詢等
- 服務端由 Dispatcher 引擎接入,並提供路由服務
- 支援多種形式提交 ETL 作業到 Yarn 來進行排程的排程平臺
Dispatcher 是 B 站團隊(下稱團隊)自研的路由服務。透過 Dispatcher,使用者提交Query 作業不需要指定使用哪些執行引擎,Dispatcher 會根據提交的 Query 語句來分析需要讀取的 HDFS 的資料量,或結合計算引擎的負載情況,路由給相應的計算引擎執行。大多數情況下的 SQL 的語法是相容的,但 Presto 語法和 Hive,Spark 衝突,這時如果使用者編寫 Hive 語句到 Presto 上執行會由於不相容而導致執行失敗。對此,團隊採用了 Linkedin 開源的 Coral 元件對語句進行轉換。查詢量比較小的 SQL,優先轉給 Presto 去執行,如果 Presto 執行失敗,再依次降級到 Spark 和 Hive 上進行執行。目前該架構的 Presto、Hive、Spark 引擎均接入 Ranger 來實現許可權控制,Ranger 可以對計算引擎做表、欄位級別的許可權控制,以及 Column Masking 和 Row Filter 的許可權控制。使用者透過 Client 端提交的查詢語句,由 Gateway 來進行路由。Presto-Gateway 是開源專案,它可以為 Presto 提供基礎路由服務,我們對其改造使查詢路由到不同機房下不同 Presto 叢集的 Coordinator 上。此外,團隊對 Presto 進行改造,使 Coordinator 支援多活,解決了 Coordinator 的單點問題。目前共有 7 個叢集,分佈在兩個機房,且實現了跨機房功能,單個叢集節點最大可達441。平均下來Presto一個月執行約 500 萬作業,每天執行作業數約 16 萬,讀取 HDFS 的資料量約 10PB。02
Presto 是基於記憶體做運算,因此查詢效率較高,但在使用過程中,仍存在一些問題:- 計算儲存分離架構帶來網路開銷。一般的資料庫,如 MySQL,計算和儲存都在同一臺節點上,而 Presto 是存算分離的架構,本身不儲存資料,只做計算,Presto 透過內部實現多個 Connector 獲取遠端資料,因此 Presto 可以做聯邦查詢,但從遠端獲取資料勢必會有網路的效能開銷。
- 查詢效能不穩定,容易受慢 RPC 或熱 DN 影響。Presto 在我們的場景中主要用於查詢 Hive 表,需要從 HDFS 獲取資料,HDFS 會存在慢 RPC 或者熱點 DN 的情況,這會導致 Presto 查詢性不穩定,如圖,每隔一段時間就有一個比較長時間的 RPC 請求。
- Presto 讀 HDFS 缺少 Data Locality,效能方面還待提升。
同時,透過 Presto 收集的血緣資訊可以發現,有的表和分割槽是反覆被訪問的熱資料,而有的表和分割槽卻基本不會被訪問,如果能將這些熱資料提前進行快取,查詢效率將會大大提升。因此,團隊引入 Alluxio 來快取這些熱資料。整合 Alluxio,需要解決以下 3 個問題: - Alluxio 與 HDFS 的 Scheme 不同
1. Alluxio 與 HDFS 的 Scheme 不同HDFS 的 Scheme 通常是 HDFS 開頭,Alluxio Scheme 是 Alluxio 開頭,Presto查詢時遠端到 HiveMetastore 獲取相應的分割槽資訊時,如果要到 Alluxio 裡面獲取資訊,需要將 HMS 裡分割槽的 Schema 資訊替換成 Alluxio 的 Scheme,但有些查詢引擎,比如 Spark,本身不透過 Alluxio 查詢,這種情況會造成線上查詢不可用。對於這個問題,Alluxio 社群(下稱社群)的做法是在高版本中的 Presto 支援 Alluxio聯結器,這個聯結器可以直接從 Alluxio 中獲取後設資料,無需再訪問 HiveMetastore 裡面獲取 Scheme 資訊。同時,Alluxio 中有 SDS 模組,會與底層的 HiveMetastore 做一個通訊,然後在 Alluxio 中把響應的邏輯封裝好返還給 Presto。其他的一些網際網路公司也採取類似的做法,透過維護新的一套 HiveMetastore,比如專門用於 ADHOC 的場景,會使新的 HiveMetastore 與原先的 HiveMetastore 保持同步,同時開發了白名單,透過白名單來確定哪些表和分割槽需要透過 Alluxio來進行快取。但維護新的一套 HiveMetastore 運維成本高,而且,不同的表和分割槽被訪問的頻率差別很大,有的表資料幾乎不被訪問,Alluxio 的快取空間是有限的,如果將這些很少被訪問的表資料也進行快取,勢必要佔用快取資源。因此 B 站團隊沒有采用上述的兩種做法,而是希望透過打 Tag 方式來控制哪些表走Alluxio 哪些走原來的邏輯。團隊對 Hive Connector 進行了改造,透過 HMS 來獲取 Partition 資訊後,根據 Partition 資訊來判斷是否走 Alluxio。Alluxio 的快取空間是有限的,不可能也沒必要將所有資料都快取在 Alluxio 中。那麼哪些資料是需要快取到 Alluxio 中的呢?團隊的做法是將獲取 Presto Query 血緣資訊吐到 Kafka 中進行分析,將符合快取條件的資料快取到 Alluxio 中,透過以下方式可以確定哪些資料是需要快取的:- 計算訪問熱度:計算表一週內的訪問頻率均值,將均值和設定的閾值(比如 10)作比較,如超過閾值,則認為是訪問頻率較高的熱資料,對計算獲得的熱資料打上 Tag 做標識,然後透過 Kafka 消費程式把血緣資訊落地到 Tidb 中。
- 計算 TTL:計算離當前最遠的熱分割槽的時間跨度,第二天視窗移動,將超過 TTL 的分割槽剔除,把最新的分割槽載入到快取中。
3. Alluxio 與 HDFS 資料一致性保證當底層 HDFS 發生變動的時候,Alluxio 中快取的資料就成了舊資料,這時候計算引擎到 Alluxio 中查詢資料,獲得的計算結果是不準確的。社群對於資料同步已給出瞭解決辦法:透過引數來控制 Alluxio 與 HDFS 的後設資料同步。 考慮到查詢慢 RPC 的情況,團隊並沒有線上上生產環境使用這個功能,而是開發了一套快取失效服務,透過監聽 Hive Meta Event 事件來做快取更新。當監聽到事件是 Drop Partition 或 Alter Partition 時,並且這個執行了 Alter 和 Drop 的分割槽剛好在 Alluxio 快取中,就會觸發快取失效,Invalidate 這個分割槽的資料;當監聽到的是 Add Partition 事件,這個新增資料的表剛好是 Alluxio 中快取的熱資料,服務會新增一個分割槽,將新分割槽的資料 Distribute Load 到 Alluxio 中。團隊發現線上的 Alluxio 叢集 Master 程式會發生偶發的 Crash。當時 Alluxio 是部署在容器裡面,容器重啟後相關的日誌會被消除掉,因此把 Alluxio 重新部署到物理機上,方便排查問題,當下一次 JVM 崩潰時就可以看到 JVM 列印出來的日誌。如圖,可以看到具體的呼叫棧過程:當 Client 端向 Alluxio 傳送請求獲取 File 的 GetStatus,由於我們線上 Alluxio 叢集對後設資料的儲存開啟了 RocksDB,因此會走到 RocksDB中,透過傳入的 BlockID 資訊獲取相關 Location,在這過程中,有 Rocks Object 物件發生 GC 被 JVM 回收了,但 RocksDB 是 C++ 的 JNI 裡面還有相關引用,導致 Segment Fault 報錯,最終導致 JVM 崩潰。社群裡有一個相關的 Issue,地址:
如果有遇到 Alluxio 叢集中的線上 Master 程式偶發的 Crash,並且是用 RocksDB做後設資料儲存,可以看看是否是這個問題導致的 JVM崩潰。如圖,綠色和藍色折線分別是 Presto 查詢 HDFS 和 Presto 查詢 Alluxio 的時間,透過對比可看到 Presto 查詢 Alluxio 花費的時間明顯比查詢 HDFS 的時間要少,平均節省約 20% 的查詢時間。- 引入 Alluxio 之後 Presto 架構的變化
未引入 Alluxio 之前的 Presto 執行流程:① 使用者透過 Client 端將 Query 作業提交到 Presto 叢集;② Coordinator 利用 SQL 解析器對 SQL 進行解析生成相應的語法樹;③ Logicalplanner 對語法樹進行解析,生成邏輯執行計劃,利用最佳化器進行最佳化;④ DistributedPlanner 將執行計劃進行切分,生成多個 Stage,內部生成多個 Task;⑤ 透過 Scheduler 排程器將任務排程到不同的 Worker 上,進行任務執行。引入 Alluxio 之後,Presto 可以直接透過 Alluxio 讀取資料,不需要每次都訪問HDFS,只有當 Alluxio 裡面沒有這個資料的快取時,再到 HDFS 中獲取資料。目前已有約 30% 的線上 BI 業務接入了 Alluxio 快取,快取了 20w 分割槽,約 45TB的資料量,讀 HDFS 的穩定性明顯提升。Presto 搭配 Alluxio Local 的使用Presto 在執行計劃階段需要訪問 HMS 獲取表和分割槽的資訊,HMS 的響應受單點mysql的吞吐影響,存在慢查詢。同時,Presto 在構建 Split 以及讀資料的情況下需要訪問 HDFS。HDFS 作為底層儲存對接了許多計算引擎,對 RPC 請求存在 Slow RPC 情況。RaptorX 是 Prestodb 透過資料快取進行查詢加速的專案(https://prestodb.io/blog/2021/02/04/raptorx),對 HMS 後設資料與 HDFS 資料來源做了全方面快取,能夠很好地解決上面提到的慢查詢等問題,其功能包括:- Hive meta cache:Presto 訪問 HMS 時,在 Coordinator 側採用版本號的方式快取 HMS 的元資訊,在下一次獲取 Hive Meta Cache 時拿 Coordinator 的版本號和 HMS 裡面的版本號做匹配,判斷是需要進行快取的新資料;
- File List Cache:在 Presto中快取 HDFS File 的元資訊,避免長時間的 List Status 操作;
- Fragment Result Cache:在 Presto Worker 節點快取部分查詢結果,避免重複計算;
- Orc/Parquet Footer Cache:儲存 Orc/Parquet 檔案的 Meta 資訊,比如 File Footer,提升快取效率;
- Alluxio Data Cache:Page 級別的快取,可以只快取常訪問的某些資料,減少快取開銷;
- Soft Affinity Scheduling:搭配 Alluxio Data Cache 使用,將同一個檔案的 Split 分發到同一臺 Worker 節點上,提高快取命中率。
這是 Presto on Alluxio 的簡圖,將 Alluxio 嵌入到 Worker 的程式中,使它整體歸於 Presto Cluster 管理,這樣相對於叢集模式更加輕量級。為了保證快取的命中率,要使同一個檔案的 Split 儘可能分到同一臺 Worker 上,實現這一點有兩種方式:一是基於 Hash&Mod 的方式,另外一種是基於一致性 Hash。Hash&Mod 方式,是透過 Hash 進行計算,算出 Split 分發到哪臺 Worker 節點,但當 Worker 節點發生變動,比如有 Worker 突然掛了,這時所有的 Split 會發生偏移,分發到其他 Worker 上。 為了解決這個問題,社群推出了一致性 Hash 策略。在 Presto 場景中,把 Presto Worker 和 Split 雜湊到 Hash 環上,然後選取方向,比如順時針方向,離這些 Split 最近一臺的 Worker 選為需要執行這些 Split 的 Worker。這樣處理的好處是,當 Presto 節點發生變動的時候,只有原先分配到這臺 Worker 上的 Split 需要重新分發到其他 Worker 節點上。此外還有一個需要改造的點,單臺 Worker 會存在負載比較高的情況,因此引入虛擬節點的概念,將單臺 Worker 對映成多臺 Worker,比如說一臺 Worker 對映成 3 臺 Worker 分配到 Hash 環上,然後再重新進行 Split 分發,這樣可以做到 Split 更加均勻的分發到不同節點上。將改造後的 Local 模式與之前的 Cluster 模式做對比,可以發現:- Local 模式以 Jar 包的形式嵌入在 Presto 程式中,更加輕量,Cluster 模式需要單獨維護一套 Alluxio 叢集,有比較大的運維壓力;
- Local 模式快取粒度更加細,可以做到 Page 級別快取,Cluster 是檔案級別的快取;
- Local 模式離計算節點更加近,Cluster 模式需要部署額外的機器資源。
3. 對 Presto Local Cache 的改造我們知道,當底層 HDFS 資料發生變動時,Alluxio 如果沒有及時對新資料進行快取,Presto 查詢使用的就是舊資料,這會影響查詢結果的準確性,對此團隊分別對 Presto 端和 Alluxio 端進行改造,使 Local Cache 與底層資料保持一致性。基於檔案的 LastModifiedTime 來改造。Presto 獲取 HDFS 檔案元資訊的時候可以獲取到檔案的 LastModifiedTime,然後將檔案相關資訊封裝到 Split 中,透過 Scheduler 把 Split 傳送到不同的 Presto Worker 節點中,Presto Worker 節點收到之後將相應的 Split 資訊封裝到 HiveFileContext 的類中,最後在構建 PageSource 時,將 HiveFileContext 中的相應資訊傳到本地的檔案系統。OpenFile 方法不是一個標準的 Hadoop API,透過 HiveFileContext 來判斷之後的邏輯是要走 Alluxio 還是走原先的程式碼邏輯,HiveFileContext 裡面主要有這幾個比較關鍵的引數:Cacheable:搭配 Soft Affinity Scheduling 使用,Soft Affinity Scheduling 會盡最大可能將檔案 Split 分發到一臺 Worker,當這臺 Worker 負載達到它的 MaxSplit 閾值時,Soft Affinity 會把 Split 分發到其他 Worker,但被分發的 Worker 只暫時處理這個 Split,下次不會再處理這個 Split 了,這個時候將 Cacheable 設定為 False 狀態。ModificatitonTime:Presto 從 HDFS 檔案裡面獲取 ModificatitonTime 資訊傳到Alluxio 中,判斷 Alluxio 快取的是否為新的資料。Alluxio 社群實現了基本的快取功能,但沒有對過期的資料進行處理,因此,團隊在Alluxio 端做了幾點改造: - 讀資料時校驗檔案的 LastModifiedTime,透過 CacheManager 裡的 Get 方法獲取相應資料,比較從 Presto 端傳來的 Context 中帶有的檔案 LastModifiedTime 和 Alluxio記憶體中所儲存的 LastModifiedTime 匹配,比較是否一致,如果一致說明快取的是比較新的資料;
- 構建記憶體資料結構,儲存檔案及時間資訊,比如 Map 檔案和相關的時間資訊;
- 持久化檔案資訊,在 Alluxio Page Restore 過程中透過讀 Metadata 檔案將 LastModifiedTime 載入賦到記憶體裡面,並可在 Restore 過程中恢復;
通常 Local Cache 啟動時首先會根據路徑遍歷其下的 Page,將獲取到的 Page 資訊載入到記憶體中,但如果遇到 Page 數比較多的情況,該操作會很耗時。Presto Worker 節點進行 Local Cache Restore 是它第一次 getFileSystem時。然而 Presto Worker 第一次呼叫 getFileSystem 是在處理 Page Source,此時 Local Data 還沒有載入完畢,這時候處理會導致快取命中率下降。因此需改造為 Presto Worker 啟動後同步對 Local Data 進行載入。 (3)Local Cache 支援 HDFS 檔案系統 社群的外部檔案系統要求 Scheme 為 Alluxio 與WS,B 站線上環境的主要資料還是存在 HDFS 中,加上一些歷史原因,HDFS 還有基於 Viewfs 的 Scheme,因此需要對Alluxio 程式碼改造,新增 HDFS 和 Viewfs 的 Scheme 資訊。單個 Disk 空間是有限的,沒辦法儲存較多 Page,同時單磁碟還有 IO 的限制,社群提供的解決方案是透過 Hash&Mod 的方式來寫入多磁碟,但是這個方式沒有考慮磁碟容量的問題。因此團隊借鑑了 HDFS 選擇 Volume 的策略,透過可用空間的大小來做磁碟的選擇。團隊基於 AvailableSpace 來做磁碟選擇改造(借鑑 HDFS)。假設有 5 個 Disk,容量分別為 1g、50g、25g、5g、30g,現在需要基於該策略往某個盤寫資料:第一步,校驗 5 個盤是否處於 Balanced,給定一個平衡態的閾值(預設 10g),用最大的容量減最小容量,如果小於這個閾值,則認為是處於平衡狀態,直接 RoundRobin 進行選擇;第二步,判斷 Disk 的容量是否大於最小容量和平衡態閾值的總和,將 Disk 劃分為HighAvail 與 LowAvail;第三步,給定一個機率值(平衡機率值預設為 75%),選擇某列進行 RoundRobin,若資料大小超過 LowAvail 列最大值,則選擇 HighAvail 進行輪詢。 那麼為什麼不直接把資料都寫到磁碟容量較高的那個 Disk 中呢?如果每次都將資料寫到磁碟容量比較高的 Disk,磁碟就接受了很多的 I/O 就可能會發生磁碟 Hang 住的情況,這種時候就不適合再選擇這個 Disk 寫入 Page 了。在單併發場景下,開啟 Local Cache 快取可以減少 20% 左右的查詢時間,相比之下,4 併發場景下有一定的效能損失,但從總體上來看,無論對於簡單查詢還是複雜查詢開啟 Local Cache 都能夠獲得一定的效能提升。目前 Local Cache 已上線三個 Presto 叢集,整體的 Presto 叢集的快取命中率達到40% 左右。- 改進 Soft-Affinity,用 Path + Start 作為 Key 來 Hash,分散大檔案分到單個 Worker Split 的壓力
- 改進 Soft-Affinity 排除不開啟 Cache 的節點
Q1:Presto SQL 和 Spark SQL 語法不同,如何降級處理?A1:對於使用者提交過來的查詢語句,首先對 SQL 進行分析,看 SQL 執行涉及到的資料量等,資料量比較小的作業優先分發到 Presto 上去執行,由於 Presto 與其他引擎語法存在相容性問題,所以會使用 Coral 做層轉換,如果 Presto 執行失敗,就降級到 Spark 上執行原 SQL,如果 Spark 上再執行失敗則降級到 Hive 上執行,如果都失敗,查詢會直接報錯。Q2:Presto 使用的是 PrestoSQL 還是 Presto db?A2:目前是基於 PrestoSQL(社群叫法 Trino)的 330 版本來進行二次開發,剛剛提到的 RaptorX 的功能是 Presto db 內部研發出來的,所以在 Presto db 這個分支中是已經實現了 RaptorX 的一些基本功能。團隊基於自己內部的 Presto 分支將其進行改造,實現了 RaptorX 的一些基本功能。Q3:目前 B 站走 Alluxio 表的標準是什麼?A3:主要是上文提到的快取策略服務,對 Presto Query 的血緣資訊進行解析,給每個表和分割槽計算熱度,熱度計算公式是計算表或分割槽一週訪問次數的均值是否高於某個給定的閾值,比如給定閾值 10,大於 10 就可以認為是比較熱的表,然後對比較熱的表打 Tag 進行標識,下一次 Presto 執行時透過 HMS 獲取分割槽的 Tag 資訊來判斷是否走 Alluxio。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2924963/,如需轉載,請註明出處,否則將追究法律責任。