火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

字节跳动数据平台發表於2024-07-30

更多技術交流、求職機會,歡迎關注位元組跳動資料平臺微信公眾號,回覆【1】進入官方交流群

作者:王恩策、徐慶
火山引擎 LAS 團隊

火山引擎數智平臺 VeDI 是火山引擎推出的新一代企業資料智慧平臺,基於位元組跳動資料平臺多年的“資料驅動”實踐經驗,為企業提供端到端的數智產品、場景化的行業解決方案和專業的數智轉型諮詢。

Parquet 在位元組跳動內部已廣泛使用和深度最佳化,結合小檔案合併、列級 TTL 兩個應用場景,我們已經實現兼顧高查詢效能下的儲存成本最佳化。

基於 Parquet 的降本增效最佳化和應用,目前已經透過火山引擎數智平臺 VeDI 旗下的湖倉一體 LAS 對外輸出。

Parquet 在位元組跳動的使用

位元組跳動離線數倉預設使用 Parquet 格式進行資料儲存。Parquet 作為一種列式儲存的開原始檔格式,在大資料領域被廣泛應用,它所提供的一系列特性,如高壓縮率、高查詢效能等都非常契合大資料領域。

另外在資料安全方面,它提供的模組化加密功能在對資料進行保護的同時也兼顧了高查詢效能。除了社群提供的一些基礎能力,位元組跳動也基於 Parquet 格式進行了深度最佳化和應用,其中包括 LocalSort/PreWhere 等功能,進一步提升了 Parquet 的儲存和查詢效能。另外在資料安全方面,也基於 Parquet 構建了透明加密系統,對底層資料進行加密保護的同時不影響使用者的正常使用。

在實際的生產過程中,隨著海量資料的持續增長,也遇到了一些問題。其中比較典型的就是小檔案問題和儲存成本問題。小檔案問題指的是在儲存系統中存在大量小檔案,由於位元組跳動離線儲存採用的是 HDFS,大量小檔案的存在會嚴重影響 HDFS 叢集的穩定性以及資料訪問的效率。

經過分析,我們發現 HDFS 中大部分資料來源於 Hive,因此治理的目標將主要針對於 Hive 資料。而在儲存成本方面,海量資料帶來了高額的儲存成本,如何安全高效地控制儲存成本也是降本增效的一大難點。

小檔案合併

首先介紹在小檔案治理方面的一些技術實踐,主要包括小檔案問題的產生原因以及小檔案問題的技術解決方案。

小檔案問題是怎麼產生的

小檔案問題的產生可能是由於資料來源本身的問題,比如一些流式任務天然地就會按照一定時間週期產出一些小檔案。

另外比較常見的是,使用者在使用 Spark 等分散式引擎對資料進行處理的過程中使用了過高的併發,也會產出大量小檔案,如果同時又用到了動態分割槽,還會進一步加劇檔案數量的放大。

類似於下圖中所示的例子,最終產出的檔案數量是併發數乘上分割槽數,一個作業很容易就會產出上千甚至上萬個小檔案。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

如何解決小檔案問題

針對上文中提到的小檔案問題,當下已經存在一些常見的解決方法,比如用 repartition 控制輸出的併發;或者用 distribute by 控制資料的分佈形式,每個分割槽只輸出一個檔案;一些情況下甚至還需要把作業拆成 2 個單獨處理來應對不同的資料場景。

以上這些方法總的來說都不夠靈活,對業務的侵入性較大,並且往往還涉及到繁瑣的調參工作,影響工作效率。為此我們提出了一套自動化、宣告式的小檔案合併方案。使用者只需要透過引數開啟小檔案合併,並設定目標檔案大小,就能讓作業自動輸出合適的檔案大小。

這種方式除了簡單直接,而且合併效率也很高,這部分內容會在後文的原理中詳細展開介紹。此外,該方案對靜態分割槽和動態分割槽也都能很好地支援。

下面來介紹一下這個功能的實現原理。假設不考慮小檔案問題,對於一個普通的 Spark ETL 作業來說,資料經過計算後會先寫到目標表的各個分割槽目錄下,然後 Spark 會觸發 Hive 表元資訊的更新,這時候資料就對下游正式可見了。

而當使用者開啟了小檔案合併後,我們會在更新後設資料,也就是資料可見之前插入小檔案合併操作。具體來說,就是檢查各個分割槽下的檔案是否滿足大小要求。如果發現檔案太小,就會在這個分割槽下觸發小檔案合併。

對於那些已經滿足要求的分割槽,則是直接跳過,不做任何操作。這種按需合併的方式是合併效率高的一個原因,而另一個原因則是我們採用了快速合併技術。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

小檔案合併的核心是如何把一個分割槽下的多個 Parquet 小檔案合併成一個,由於 Parquet 格式具有特殊的編碼規則,檔案內部被劃分為多個功能子模組,我們不能直接把 2 個 Parquet 檔案首尾拼接進行合併。

常規的做法是需要用 Spark 讀取這些小檔案,提取出檔案中的一行行記錄,然後再寫成新的檔案。在這個一讀一寫的過程中,會涉及到大量的壓縮反壓縮、編碼反編碼等等操作,這些操作消耗了大量的計算資源。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

為了提高合併速度,我們採用了一種快速合併的方式,這種方式借鑑了 Parquet 社群所提供的 merge 工具,能夠快速地將多個 Parquet 檔案合併成一個,下面介紹一下它的實現原理。

Parquet 檔案的內部有很多內容,例如 Footer、RowGroup 等等。這些內容可以分為 2 類,一類是被壓縮和編碼後的實際資料,而另一類則是記錄了資料是如何被編碼和排列的後設資料。快速合併的基本思路就是:直接 copy 實際資料所對應的原始二進位制 Data(跳過編解碼流程),再基於資料在新檔案中的位置構建出新的元資訊。

元資訊構建過程非常快速,因此整體開銷近似於直接 copy 整個檔案。值得注意的是,這種合併方式並不會合併 RowGroup,因此對壓縮率和查詢效能並不會有明顯提升,但是卻極大地提升了合併效率,而檔案數量的減少最終有效降低了 HDFS 叢集的壓力。

經過效能測試, 快速合併上相比於普通合併有 14 倍左右的提升。根據線上任務的實際執行情況,作業在開啟快速小檔案合併後,平均執行時長只增加了 3.5% 左右,可以看到對業務的影響很小。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

另外,在實際生產環境中,為了資料安全,Parquet 檔案是被加密儲存的,並且為了保證高查詢效能,加密儲存採用的是模組化加密的方式,也就是對檔案中的各個模組分別加密。

這種加密方式保留了 Parquet 檔案的基本結構,從而保留了 Parquet 高效能查詢的能力。但在小檔案合併過程中,這也帶來了新的挑戰。

我們無法像之前的模式那樣直接 copy 二進位制資料,因為各個檔案的資料是基於不同金鑰加密的結果,金鑰資訊儲存在每個檔案的 Footer 中,直接 copy 二進位制模組到目標檔案後,無法用新檔案中的統一金鑰進行解密。

為此需要在原有快速合併的基礎上,在 copy 二進位制模組的同時加上解密和再加密操作,用原檔案中的金鑰解密,然後再用新檔案的金鑰進行加密。整體流程上還是以 copy 二進位制資料為主,跳過了編碼反編碼之類的多餘操作,實現快速合併。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

以上介紹瞭如何在資料產出的同時合併小檔案,在實際情況中,HDFS 叢集上會存在著大量歷史小檔案,為此我們提供了存量小檔案合併工具進行處理,其使用方法也是非常簡單:使用者提交一個 SQL,在 SQL 中制定合併的那張表、分割槽、合併的目標檔案大小。SQL 提交後,系統就會啟動一個 Spark 作業,再結合剛剛提到的工具和流程對存量資料進行快速合併。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

小結:我們在增量和存量場景都提供了對應的小檔案合併能力,以一種簡單高效的方式對小檔案進行綜合治理,提升了整個叢集的健康度和穩定性,最終有效降低了機器成本和人力運維成本。

列級 TTL

上文介紹了在解決小檔案問題時的相關實踐。接下來將介紹 Parquet 格式在位元組跳動另一降本增效實踐——列級 TTL 相關的內容。

列級 TTL 產生的背景

  1. 隨著業務發展,海量資料的儲存成本逐漸成為離線數倉的一大痛點。而目前離線數倉清理儲存只有分割槽級的行級 TTL 方案,類似於使用 alter table drop partition 的 DDL 來完成分割槽資料的整體刪除。對於具有時間跨度較大彙總需求的表,則需要保留較長時間的歷史分割槽,而這些歷史分割槽中很多明細資料在彙總任務中並不會使用,也就是歷史分割槽中存在很多低頻訪問欄位。如果想刪除這些不再使用的欄位資料,目前已有的方式就是透過 Spark 等引擎將資料讀取出來,並將需要刪除的欄位設定為 NULL 的覆寫方式來完成。這種方式有兩個缺點:海量資料的覆寫計算資源開銷很大;

  1. 對於欄位較多的大寬表,使用者需要在 select 中羅列出所有欄位,並對需要刪除的欄位逐個置空,TTL 任務運維成本高。

輕量級列級 TTL 方案

針對以上業務存在的痛點,結合 Parquet 列存的特性,提出了一種如下圖所示的輕量級的列級 TTL 方案。

火山引擎VeDI資料技術分享:兩個步驟,為Parquet降本提效

該方案按照 Column chunck 直接 Copy 每個 RowGroup 中需要保留的列的二進位制資料,跳過編解碼流程。舉一個例子,假如表有 1、2、3 列,現在需要刪除第 2 列資料。

首先要做的是構造新的 schema,從原檔案 schema 中刪除需要 TTL 的列(第 2 列)並作為新檔案的 schema,然後從原檔案中按照 Column Chunk copy 第 1 列和第 3 列的資料到新檔案中。在進行列級 TTL 時,因為刪除了部分列資料,會導致新檔案 size 變小,容易出現小檔案問題,所以我們支援將原來多個檔案的資料合併到同一個檔案中。

這種列級 TTL 的方式,相比於 insert overwrite 覆寫的方式,速度能夠實現提升 14 倍 +。前文還提到 insert overwrite 方式需要在 select 中列舉所有列,為了方便業務方使用這個列級 TTL 功能,我們定義了一種新的語法來支援列級 TTL 功能:alter table ${db.table} partition(${part_name}) drop columns(xxx)。

使用者只需指定需要執行列級 TTL 的庫表分割槽以及需要刪除的列即可,不用再把其他不需要刪除的列也一一列舉,然後往資料引擎提交這個 SQL,就能觸發列級 TTL 任務的執行。

列級 TTL 功能實現到落地還涉及到一個問題,就是如何高效的發現哪些分割槽下的哪些列能夠進行列級 TTL。

LAS 團隊開發了一個列級血緣的分析工具,支援快速分析表歷史分割槽查詢情況,然後自動推薦能夠進行列級 TTL 的 columns 及對應的 TTL 時間,比如某張表 column A 90 天前基本無使用者查詢,那麼 90 天前的歷史分割槽中 column A 可以進行 TTL;Column B 120 天前的資料無使用者查詢,可以進行列級 TTL 等。

列級 TTL 在位元組跳動主要應用於歷史低優資料的清理,大 JSON、大 MAP 型別欄位的清理或者明細日誌的資料清理。該功能上線後已經為公司清理了大量的無用歷史資料,釋放了較大的儲存空間。

結合位元組跳動自身經驗,火山引擎數智平臺 VeDI 不斷對 Parquet 這項開源技術進行最佳化和重構,進一步提升效能,併兼顧儲存成本。未來,火山引擎數智平臺 VeDI 也將憑藉卓越的技術能力和豐富的行業經驗,持續為更多使用者提供了優質的資料技術服務,助力企業實現數字化轉型。

點選跳轉 火山引擎VeDI 瞭解更多

相關文章