導讀 本次分享主題為 B 站 Presto 叢集查詢效能的最佳化,首先會簡單介紹 Presto以及 B 站內部 Presto 叢集的架構。接下來講解針對 Presto 做的改造,主要是 Presto 搭配 Alluxio 和 Presto 搭配 Alluxio local cache 的使用。最後會對後續計劃開展的工作做簡要說明。
本次介紹的六個部分為:
1. 叢集架構
2. Presto 簡介
3. Presto 改造
4. Presto on Alluxio
5. Presto Local Cache
6. 後續工作
分享嘉賓|楊洋 bilibili 大資料開發工程師
編輯整理|陳昱彤 紐約大學
出品社群|DataFun
01
下圖為 B 站 Presto 叢集的架構圖,最上層是內部的一些資料服務,所有服務統一接入 Dispatcher。Dispatcher 是一套內部自研的服務,根據查詢 HDFS 的資料量、目前引擎的負載情況等將使用者提交的 query 路由到相應的引擎進行執行。對於 presto 的查詢語法和 hive/spark 語法可能出現的不相容問題,我們引進了 Linkedin 的開源軟體 Coral,將 hive 和 spark 的語法轉換為 presto 的語法進行執行。計算引擎的路由邏輯:對於查詢量比較小的 sql,會優先路由到 presto 進行執行,如果失敗則降級到 spark 進行執行,再失敗則降級到 hive 進行執行。排程平臺(ETL):使用者可以透過排程平臺編寫排程作業來執行排程任務,具體是由 yarn 來排程。Ranger:目前 HDFS 和 hive/presto/spark 都已經接入到 Ranger 進行統一的許可權控制。許可權控制包括表欄位級別的控制以及 column masking 和 row filter 的控制。目前 Presto 包括了四套叢集,分為兩個機房(IDC1 和 IDC2)。除了Cluster3 之外的叢集都實現了跨機房功能,目前叢集數最多的為 IDC2 的 Cluster1(441 為叢集數量,2 為 coordinator 數量)。Presto 叢集平均每月作業數量 500w,每日作業數約 16w~17w。平均每月讀 HDFS 資料量約 300PB,每日 10PB。Presto 叢集的架構大致如下,使用者作業提交後,會先透過一個開源元件Presto-Gateway,將 sql 作業路由到對應的 IDC 機房中相對應的 presto 叢集中的 coordinator 節點進行執行。02
Presto 是 2013 年 11 月份由 Facebook 開源的一個分散式 sql 查詢引擎,設計之初是為了進行 OLAP 資料查詢,支援標準的 ANSI SQL,也支援多資料來源。在 2019 年 1 月時 Presto 內部分裂出了兩個分支:PrestoSQL(trino)和 PrestoDB。PrestoSQL 相對於 PrestoDB 來說社群活躍度較高且更加註重 OLAP 的方向,而後者相對更注重 ETL 的方向。Presto 是一個典型的主從架構,它由一臺 coordinator 和多個 worker 構成,Presto worker 啟動時會在 coordinator 進行註冊。Presto 叢集作業的執行流程是:1.coordinator 收到作業後透過 sql 解析器對作業進行解析生成語法樹, LogicPlanner 再對語法樹進行語義分析,將 AST 轉為邏輯執行計劃,同時使用最佳化器進行最佳化。接著透過 DistributedPlanner 將計劃進行切分生成多個 stage,stage 內部劃分為多個 task, 透過 scheduler 將多個 task 分發到不同的 worker 上執行。由於Presto 本身不儲存資料,需要透過多個connector 來訪問不同資料來源的資料。我們對 Presto 做的改造主要從可用性、穩定性和效能提升三個角度出發。Presto在B站的實踐:https://mp.weixin.qq.com/s/9_lSIFSw5o8sFC8foEtA7w ① Presto 和 hive 語法之間相容性的操作在 Ranger 社群中,不同的引擎有不同的 policy 進行管理,我們使用了hive 的 policy 來統一進行管理。Presto 的主從架構存在單點故障的問題,也就是當 Coordinator 發生故障的時候會影響到整個叢集的查詢。多活改造保證了當某一個 Coordinator 發生故障的時候,另一個 Coordinator 會繼續對外進行服務。此外,多活也可以減輕單臺 Coordinator 的作業壓力。由於多個部門同時使用 Presto 叢集進行查詢,其中有一些部門對於作業的實時性要求比較高,有些部門會提交查詢資料量較大的 query 語句,這就會導致大 query 擠壓 Presto 的查詢效能,使實時性要求高的查詢無法完成。因此我們對 worker 進行了標籤的改造,同時在使用者提交任務時也給任務打上標籤,這樣 scheduler 在分發任務的時候就會根據任務標籤和 worker 標籤將任務分配到相應的 worker 上去執行。label 改造實現了不同部門之間 worker 資源的隔離,但對於同一個部門不同的業務還是會存在大 query 擠壓資源的現象。因此我們引進實時懲罰策略,對於提交的大語句判斷對應的 resource group 並分配 CPU 的閾值。如果超過閾值,Presto worker 會進行 split 並下發懲罰資訊暫停執行,等整體 resource group 資源佔用量低於閾值後再恢復排程。在 presto Gateway 對 bad sql 進行攔截。包括短時間內重複提交、查詢HDFS 資料量較大(超過 30TB)的查詢語句。效能提升主要圍繞 Presto 快取來進行,快取有三部分,分別為資料來源/後設資料/結果集的快取。此部分感興趣的同學可以讀下面這篇文章:Presto 在 B 站的實踐(點選可跳轉閱讀文章)04
前面幾章主要講解了我們內部對 presto 的改造,接下來介紹 presto 對資料來源和後設資料的快取。首先是對於 Alluxio 做快取的介紹。和傳統的 MySQL(存算一體)的資料庫不同,Presto 是一個存算分離的資料庫。Presto 本身只做計算不做儲存,它透過多個 connector 實現遠端獲取資料,也可以實現聯邦查詢。但從遠端獲取資料必然會帶來網路上的效能開銷。② 容易受慢 rpc (Remote Procedure Call) 或熱 dn 影響,查詢效能不穩定我們 Presto 主要的場景是查詢 hive 表為主,需要去底層查詢 HDFS 的資料。由下圖可見,Presto 查詢 HDFS 時每隔一段時間就會有較長時間的慢 rpc 請求,進而導致 Presto 查詢效能的不穩定。③ 缺少 data locality,效能方面有待提升上部分從引擎的角度介紹了 Presto 的一些痛點,此部分從業務資料的角度介紹為什麼需要 Alluxio 做儲存。下圖為對 Presto query 血源資訊做的 UI 展示,我們發現有些表/分割槽存在重複訪問的情況,比如圖中第一張表的訪問熱度(tableheat)達到了 866。我們可以對經常被訪問的表進行快取,從而提升 presto 引擎的查詢效能。 架構上的變化主要有兩個,一是對於血緣變化的處理,二是新增了 Alluxio worker。原先 Presto worker 直接從 HDFS 訪問資料, 現在則是先透過 Alluxio worker 獲取資料,如果 Alluxio 中沒有對應資料再去最訪問 HDFS 獲取資料。Presto Coordinator 對 sql 進行解析時,會將血緣資訊吐到 Kafka 中,再透過消費程式將相應的血緣資訊落到資料庫中,最後透過自研的血緣分析服務對熱資料打上標識。下一次 Coordinator 從 Hive Metastore 訪問分割槽時會判斷分割槽的引數是否有熱資料標識,如果有標識,接下來就會走 Alluxio 的邏輯。4. Presto on Alluxio 的實現細節① Alluxio 與 HDFS 的 scheme 不同當 Presto 去 Hive Metastore 查詢的時候,如果想要訪問 Alluxio 的資料時,比較簡單的做法是將 Hive Metastore 裡相應的 scheme 轉換為 Alluxio 的 scheme,但這會帶來的問題是對於其他的引擎(比如 spark),因為其本來就沒有接入 Alluxio,會導致查詢不可用。對於這個問題,社群的解決方案是在高版本 Presto 中支援 Alluxio 聯結器。原先 Presto 需要透過訪問 Hive Metastore 去獲取表資訊,現在只需要訪問 Alluxio 就可以獲取資訊。Alluxio 內的 SDS 模組有和 Hive Metastore 的通訊功能,SDS 模組會在 Alluxio 中將相應的邏輯進行封裝,再返還給 Presto 進行處理。其他網際網路公司方案:維護一套新的 Hive Metastore 來用於 adhoc 的場景,並定期將新 HMS 和原先的 HMS 保持同步。同時依靠自己開發的白名單來確定哪些表是需要 Alluxio 快取的。- 我們希望透過自己的方式來掌管需要把哪些表存到 Alluxio 中
所以我們團隊沒有使用上述兩種方案,而是透過打 Tag 的方式來控制哪些表走 Alluxio。我們的解決方案:改造 hive connector。因為我們需要透過 hive connector 來獲取 HMS 中的 parameter 資訊,再透過識別分割槽引數裡面 Alluxio 的 tag 來判斷是否走 Alluxio 的邏輯並且透過程式碼將 scheme 替換成 Alluxio 的 scheme。由於 Alluxio 快取空間的有限性,沒有必要將所有資料進行快取,因此我們會對熱資料打上標識,只把熱資料儲存到 Alluxio 上。首先,在 Presto 端把 Presto query 血緣資訊吐到 Kafka上,再透過 Kafka 消費程式分析血緣資訊並落到 Tidb 上。血緣資訊主要包括 query 語句,涉及到的 queryid 以及查詢表和分割槽。接下來透過快取策略服務判斷資料熱度,並對熱度高的資料打上 tag。訪問熱度判斷:計算表一週平均訪問次數,再根據全量表一週內的被訪問頻率確定劃分閾值,高於閾值的為熱表。計算 TTL(離當前最遠的熱分割槽的時間跨度)資料:使用滑動視窗的方式實現對離當前時間點最近的熱分割槽的時間跨度計算,在 Alluxio 中剔除超過最遠時間點熱度分割槽並將最近 logdate 的分割槽新增進快取中。當底層 HDFS 資料發生變更的時候,Alluxio 中便可能出現快取了舊資料和髒資料的情況。針對這一問題,社群普遍的解決方案是透過配置引數達到和 HDFS 的後設資料同步。但我們實踐過程中由於存在慢 rpc 的情況,所以無法使用社群的解決方案。為此我們自己開發了一套快取失效服務來監聽 Hive Meta Event,當監聽到 alter partition 或者 drop partition 的事件時,服務會自動剔除 Alluxio 中存在的相應分割槽。同時,我們也會監聽 add partition事件。當 add partition 事件並且表的熱度較高時,我們也會將相應分割槽快取入 Alluxio。下面是 Presto on Alluxio 和 Presto on HDFS 的效能對比,查詢 Alluxio 對比查詢 HDFS 大概可以節省 20% 的查詢時間。目前大約 30% 的 BI 業務已接入到 Alluxio 的快取中,已快取 20w 分割槽(約45TB)。改造後 Presto 讀 HDFS 的穩定性有大幅提升,基本控制在 2.5ms 以內。問題:RocksDB 做後設資料儲存的時候,線上 Master 程式偶發 crash。主要背景:Alluxio 原本是放在容器中的,Alluxio 主程式突然發生 crash,拉起 Alluxio 容器後出現了日誌丟失的問題。為了排查 crash 原因,我們將 Alluxio 部署到物理機上,在物理機上透過新增一些 JVM 的引數,等待問題的再次發生。下圖為 JVM 崩潰時列印出來的錯誤日誌,整個異常棧的呼叫過程為 client 端向 Alluxio 發請求獲取檔案狀態時,會透過 Rocksdb getlocation,再透過 blockid 獲得其對應的資訊。在操作的過程中,rocks object 物件發生了 GC 被 JVM 回收了,但 rockesdb 是 c++ 的 jni 裡面還有該引用,所以會產生 segment fault, 記憶體地址越界,最後導致了 JVM 的崩潰。此問題已經在社群中有了相應的修復。具體可見以下連結:
1. RaptorX 背景
由於 Presto 在執行計劃階段需要訪問 HMS 獲取表和分割槽的資訊,而HMS 的響應受單點 mysql 的吞吐影響,存在慢查詢。Presto 在構建 split 以及讀資料的情況下需要訪問 HDFS。HDFS 作為底層儲存對接了許多計算引擎,如 Hive、spark 等,在 RPC 請求穩定性方面經常存在 slow rpc,在讀 datanode 資料時,存在 slow dn。因此 RaptorX 應運而生,它透過對後設資料與資料來源進行全方面快取來解決上述問題。在 Presto 端的 Coordinator 側對 hive 端的 meta 資訊做了快取,但是考慮到後設資料會出現變更,於是我們新增了版本號。每次請求後設資料時,我們會將版本號和 HMS 中的版本號做匹配,判斷快取的是否是一個新的 hive meta 資料。在 Presto 的 coordinator 側對 HDFS 的後設資料做一些快取,避免長時間的 list status 操作。在 Presto worker 節點對部分查詢結果做快取,避免重複計算。對 orc 或者 parquet 格式的檔案的 foote r做快取,提升 presto 查詢的效能。Raptorx 相關文章:https://prestodb.io/blog/2021/02/04/raptorx3. Presto Local Cache-Alluxio Local 模式在 local 模式中,我們把 Alluxio 透過架構的形式嵌入到 Presto worker的程式中,對 Presto 叢集進行統一的管理。這種模式相對於 Presto on Alluxio 來說更加輕量以及便於維護。4. Presto Local Cache-Soft Affinity scheduling搭配 Alluxio Data Cache 使用,它可以使同一個 split 儘可能分發到同一個 worker 節點,保證快取的命中率。實現方式有兩種,第一種是基於 Hash & Mod,第二種是基於一致性 Hash。Hash & Mod 是透過 Hash 計算,把 split 分發到對應的 worker 節點。它的缺陷是當 worker 節點數發生變動時(如故障等),總的 worker 節點數就會發生變化,會導致 split 重新分發到別的 worker 節點。一致性 hash 就是為了解決這個問題而提出的,其基本思路是透過一個雜湊環(0-max(value)),首先將 Presto worker 給 Hash 到這個雜湊環上,然後再將 split 雜湊到環上。在選定方向後,在環上離 split 最近的一個 worker 會對 split 進行處理。這麼做的好處是當 Presto worker 發生故障的時候,只有原先分發到這個 worker 的 split 才會重新分發到別的節點處進行執行,此外還有一點最佳化,單臺 worker 可能會出現負載較高的情況,因此我們對一致性 Hash 提出了虛擬節點的概念。比如將一臺 worker 對映到三臺 worker 分配到雜湊環上,再重新進行 split 的分發,這樣可以做到 split 更加均勻的分發到不同的 worker 節點上。5. Presto Local Cache – Alluxio Cluster6. Presto Local Cache – Local vs Cluster接下來我們看 presto on local cache 和 presto on alluxio cluster 的區別。Local 是以 jar 包的形式嵌入到 presto 的程式中,cluster 則是需要維護一套 Alluxio 叢集,因此 cluster 的運維成本更高。第二點是快取的粒度不同,local 快取粒度更加細,做到了 page 級別的快取,cluster 是做檔案級別的快取。第三點是 local 離計算節點資源更近,而 cluster 需要額外的計算機器資源來部署 Alluxio 叢集。7. Presto Local Cache – 改造點- Local cache 與底層資料一致性(presto 端改造)
當底層 hdfs 檔案發生變動時,Alluxio 中快取的可能是舊的資料。這時候Presto 引擎查詢時,返回的可能是不準確的髒資料。改造的思路是基於檔案的 LastModifiedTime(最後更改時間)來判斷,對 Presto 來說最初是獲取 HDFS 的後設資料檔案時同時獲取,然後將相應的資訊封裝到 split 中,Presto 透過 scheduler 將相應的 split 排程到 Presto worker 節點,在節點處將相關的資訊封裝到HiveFileContext 中。在具體構建 pagesource 時,將 HiveFileContext 傳到本地檔案系統中。核心方法是openFile,它不同於傳統方法直接傳一個 path 路徑,openFile 方法是傳一個 HiveFileContext,因此它不是一個標準的 hadoop api。openFile 會透過 HiveFileContext 來判斷是否走 Alluxio。(1)cacheable:搭配前文提到的 soft affinixity scheduling 使用,當一臺worker 的負載達到上限,不得不排程到其他 worker 進行執行時,由於這臺 worker 只是臨時支援的,我們會將 cacheable 置 false。(2)ModificationTime:用於在 Alluxio 中判斷快取的是否是新的資料。- Local cache 與底層資料一致性(Alluxio 端改造)
(1)當 presto 去底層讀取資料時,透過 localcache.manage.get 得到對應的 page,這時我們需要透過比較檔案的 LastModifiedTime 和 Alluxio 記憶體檔案中的 LastModifiedTime 來判斷檔案是否一致。(3)持久化資訊(可用於在 restore 中恢復)8. Presto Local Cache – Local Cache 啟動問題(1)Local Cache restore 時間點Local cache 會去指定的 path 路徑載入 page,當 page 較多時就會出現載入耗時高的情況。如下圖所示,get file system 時,我們會在 AlluxioCachingFileSystem 建立 local cache,再對 localcache 進行非同步載入。但對於 Presto worker 來說,它第一次 get file system 是在 get split 的時候。這種時候可能 local cache 還沒有非同步載入完畢,此時便會導致快取命中率的下降。對此進行的改造是:在 Presto worker 啟動時,構建一個空路徑,透過空路徑構建一個 getfilesystem,同步對 local file 進行載入,然後再對外提供服務。local cache 主要是寫在 SSD 盤上,可能會遇到磁碟損壞,或者我們先新增一個新 path 路徑用於儲存 page。但我們新新增的 path 路徑對應的 SSD 可能存在磁碟容量身偏高的場景,於是我們設定了一個 local cache 的開關,在以上情況發生時會將 local cache 的開關關閉。9. Presto Local Cache – Local Cache 支援 HDFS 檔案系統社群對 local cache 實現的 scheme 要求為 Alluxio 和 ws 的 scheme,但我們的線上生產環境主要資料還是以 HDFS 為主,以 Alluxio 為輔。因此我們對 Alluxio 程式碼做了一些改造,使其能夠支援 hdfs 和 viewfs 的scheme。10. Presto Local Cache — Local Cache 支援多磁碟社群解決方法:透過 Hash&Mod 寫入多磁碟,這種方法的缺陷是沒有考慮到每個磁碟本身容量的情況。我們的改造:借鑑 HDFS,基於 AvailableSpace 來做磁碟選擇。基本思路是對多塊磁碟給到一個閾值,超過這個閾值的就認為是容量比較高的,將其放到一個高容量的 list 中,反之亦然。然後給一個機率值,比如 0.75 的機率寫到高容量磁碟,0.25 的機率寫到低容量磁碟。為什麼不直接把資料優先寫入高容量磁碟呢,原因有二:(1)磁碟容量只是其中一個考量因素,不能單純根據容量高就優先寫;(2)假設線上把所有 page 都寫入磁碟容量比較高的盤裡面,會造成這個盤的 io 壓力過大的情況。11. Presto Local Cache – Local Cache 測試效果單併發場景下開啟 local cache 快取可以減少 20% 左右的查詢時間,大大提升了查詢效率。四併發場景下測試查詢時間有一定提升,但相比單併發場景下,效能有一定的損失。12. Presto Local Cache — Local Cache 線上效果目前上線了 3 個 presto 叢集,快取命中率約 40%。13. Presto Local Cache — 社群 PR ( Merged)
( Merged)
( Open)
( Closed)
相對於 Presto on Alluxio, local cache 更加穩定也更加輕量。於是我們後續將繼續推廣 Local 模式。社群實現只支援 orc、parquet、rcFile,但線上有很多表用的是 textFile 的格式,所以我們需要開發 textFile 的快取。線上的一些 Presto 節點會存在慢節點的情況,需要對有問題的節點進行隔離。原先 soft affinity 會將同一個檔案的 split 分發到同一個節點,會造成單臺worker 節點壓力比較大的情況,後續計劃使用 path+start 作為 key 來雜湊,分散大檔案分到單個 worker split 的壓力。5. 改進 soft-affinity 排除不開啟 cache 的節點 對於沒有 local cache 的 presto worker 進行排除。A1:由於業務發展,原本在一個 idc 機房的機位已經達到上限,需要開闢新的機房來儲存資料。在新機房部署 Hadoop 和 Presto 叢集,大家比較容易想到的跨機房難點是資源緊張,因此我們設計了一套 Presto 跨機房的功能。內部是透過使用者作業提交到 Presto Getaway,再由 Presto Getaway 分發到不同叢集,對 Presto Getaway 進行改造:1. 分析 sql 語句,看是查詢了哪些表和分割槽,然後對 nnproxy 做了改造,看這些表和分割槽在哪個機房下,資料量有多少,然後 Presto Getaway 會根據以上資訊判斷這條 sql 語句會路由到哪個叢集執行。比如如果使用者提交來的 query 涉及到很多個表,首先對涉及到的表進行分析,最後排程到佔資料量最大的機房執行。2. 此外還有一些最佳化工作。我們知道有個概念叫做移動資料不如移動計算,原先透過 hive connector 去訪問資料的話會出現頻寬資源比較緊張的現象,因此我們做了一個計算的下推。主要是實現了一個叫 IDC connector 的聯結器。我們可以將第二個機房看作是一個 connector,將跨機房的相應邏輯拋到第二個機房的 presto coordinator 進行處理。IDC2 的 Presto coordinator 獲取底層 HDFS 的資料並處理完成後會將結果返還給第一個機房做一個結果合併。此外我們還使用了 Alluxio 來快取跨機房的資料,比如我們的 query 語句被推到 IDC1 機房進行執行,但很多資料需要透過 IDC2 獲取,這時 Alluxio 可以將熱度較高的資料跨機房進行快取,下次就只需要透過 Alluxio 來進行跨機房資料的獲取。這兩個方法的好處都是可以減少跨機房的流量壓力。Q2:Local Cache 需要部署叢集服務嗎?A2:不需要。對於 Alluxio 叢集模式需要單獨部署 Alluxio 叢集,然後 presto 訪問 Alluxio 叢集。但如果使用 presto local cache 則不需要部署 alluxio 叢集,Alluxio 是透過 jar 包的形式嵌入 presto 的程式中,它跟原先的 presto cluster 是共用同一套 presto 叢集的,因此相對於 presto on alluxio 來說它會更加的輕量級。A3:社群實現主要是對 worker 端的 metrics進行統計,單臺 worker 在生產環境中的命中率達到了 90% 以上。我們自己更加關注 coordinator 端的命中率。前面有提到 hive connector 讀取 HMS 中 split 的資訊時會帶上引數來判斷是否走 Alluxio,我們會統計 query 走 Alluxio 的次數,除以整個 presto 叢集 query 的次數來得到快取命中率。目前線上的快取命中率基本可以達到 40% 左右。Q4:怎麼解決 local cache 的高併發效能問題?A4:在我們的實驗環境中。四併發查詢的響應時間明顯不如單併發,原因可能是因為資料存在 SSD 盤上,在多次讀資料的時候存在磁碟 io 的限制。Q5:local cache 的記憶體佔用大概是多少?A5:我們發現存相同大小的檔案,local cache 的記憶體佔用相對較低。這是因為 cluster 是基於檔案級別儲存的,並且當別的 Alluxio worker 參加執行時,別的 worker 也會快取對應檔案。對 local cache 來說,首先是快取粒度更細(基於 page),只會精準快取某些 page。另外是 soft affinity scheduling 的策略,它會盡可能將檔案對應的 split 分發到 worker 執行。就算遇到單 worker 負載高,分發到其他的 worker 上進行執行的情況時,我們也會透過把 cacheable 引數置 false,讓臨時 worker 不快取檔案。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2934960/,如需轉載,請註明出處,否則將追究法律責任。