使用部分寫時複製提升Lakehouse的 ACID Upserts性能

使用部分寫時複製提升Lakehouse的 ACID Upserts性能

譯自:Fast Copy-On-Write within Apache Parquet for Data Lakehouse ACID Upserts

傳統的寫時複製會直接讀取並處理(解壓解碼等)整個文件,然後更新相關數據頁並保存爲新的文件,但大部分場景下,upsert並不會更新所有數據頁,這就導致其做了很多無用功。

文章中引入了一種新的寫時複製,它會創建指向Apache Parquet文件的數據頁的索引,並跳過不相關的數據頁(不會對這部分數據進行解壓解碼等操作),以此來加速數據的處理。

術語

  • copy-on-write:寫時複製
  • merge-on-write:讀時合併

概述

隨着存儲表格式的發展,越來越多的公司正在基於Apache HudiApache IcebergDelta Lake等工具來構建lakehouse,以滿足多種使用場景,如增量處理。但隨着數據卷的增加,upsert的運行速度可能會帶來一定的影響。

在各種存儲表中,Apache Parquet是其中最主要的文件格式。下面我們將討論如何通過構建二級索引並對Apache Parquet進行一些創新來提升在Parquet文件中upsert數據的速度。我們還會通過性能測試來展示相較傳統的Delta Lake和Hudi寫時複製的速度(提升3x~20x倍)。

起因

高效的 ACID upsert 對於今天的lakehouse至關重要,一些重要的使用場景,如數據保存和Change Data Capture (CDC)嚴重依賴ACID upsert。雖然 Apache Hudi, Apache Iceberg 和 Delta Lake中已經大規模採用了upsert,但隨着數據卷的增加,其運行速度也在降低(特別是寫時複製模式)。有時較慢的upsert會成爲消耗時間和資源的點,甚至會阻塞任務的執行。

爲了提升upsert的速度,我們在具有行級索引的Apache Parquet文件中引入了部分寫時複製,以此來跳過那些不必要的數據頁(Apache Parquet中的最小存儲單元)。術語"部分"指文件中與upsert相關的數據頁。一般場景中只需要更新一小部分文件,而大部分數據頁都可以被跳過。通過觀察,發現相比Delta Lake和Hudi的傳統寫時複製,這種方式提升了3~20倍的速度。

Lakehouse中的寫時複製

本文中我們使用Apache Hudi作爲例子,但同樣適用於Delta Lake和Apache Iceberg。Apache Hudi支持兩種類型的upserts操作:寫時複製和讀時合併。通過寫時複製,所有具有更新範圍內記錄的文件都將被重寫爲新文件,然後創建包含新文件的新snapshot元數據。相比之下,讀時合併會創建增量更新文件,並由讀取器(reader)進行合併。

下圖給出了一個數據表更新單個字段的例子。從邏輯的角度看,對User ID1的email字段進行了更新,其他字段都沒變。從物理角度看,表數據存儲在磁盤中的單獨文件中,大多數情況下,這些文件會基於時間或其他分區機制進行分組(分區)。Apache Hudi使用索引系統在每個分區中定位所需的文件,然後再完整地進行讀取,更新內存中的email字段,最後寫入磁盤並形成新的文件。下圖中紅色的部分表示重寫產生的新文件。

image

圖1:表upsert的邏輯和物理文件視角

使用Apache Hudi構建大型事務數據湖一文中提到,一些表更新可能會涉及到90%的文件,進而導致重寫數據湖中的特定大型表中約100TB的數據。因此寫時複製對於很多使用場景至關重要。較慢的寫時複製不僅會導致任務運行時間變長,還會消耗更多的計算資源。在一些使用場景中可以觀察到使用了相當數量的vCore,等同於花費了上百萬美元。

引入行級別的二級索引

在討論如何在Apache 中提升寫時複製之前,我們打算引入Parquet 行級別的二級索引,用於幫助在Parquet中定位數據頁,進而提升寫時複製。

當首次寫入一個Parquet文件或通過離線讀取Parquet文件時會構建行級別的二級索引,它會將record映射爲[file, row-id],而不是[file]。例如,可以使用RECORD_ID作爲索引key,FILERow_IDs分別指向文件和每個文件的偏移量。

image

圖2:Apache Parquet中行級別的索引

Apache Parquet內部,數據被分爲多個row groups。每個row group由一個或多個column chunks構成(column chunk對應數據集中的一列),然後每個column chunk 會被寫成數據頁格式。一個block包含多個頁,它是訪問單個record前必須讀取的最小單元。在頁內部,除了編碼的目錄頁,每個字段都追加了值、重複級別和定義級別。

如上圖所示,每個索引都指向頁中record所在的行。使用行級別的索引時,當接收到更新時,我們不僅僅可以快速定位哪個文件,還可以定位需要更新的數據頁。使用這種方式可以幫助我們跳過不需要更新的頁,並節省大量計算資源,加速寫時複製的過程。

Apache Parquet中的寫時複製

我們在Apache Parquet中引入了一種新的寫時複製方式來加速lakehouse的upserts。我們只對Parquet文件中相關的數據頁執行寫時複製更新,而對於無關的頁,只是將其複製爲字節緩存而沒有做任何更改。這減少了在更新操作期間需要更新的數據量,並提高了性能。

image

圖3:Apache Hudi傳統寫時複製和新寫時複製的比較

上面展示了新的寫時複製和傳統的寫時複製的處理過程。在傳統的Apache Hudi upsert中,Hudi會使用record 索引 來定位需要修改的文件,然後一個record一個record地將文件讀取到內存中,然後查找需要修改的record。在應用變更之後,它會將數據寫入一個全新的文件中。在讀取-修改-寫入的過程中,會產生消耗大量CPU週期和內存的任務(如壓縮/解壓縮,編碼/解碼,組裝/拆分record等)。

爲了處理所需的時間和資源消耗,我們使用行級別的索引和Parquet元數據來定位需要修改的頁,對於不在修改範圍的頁,只需要將其作爲字節緩存拷貝到新文件即可,無需壓縮/解壓縮,編碼/解碼,組裝/拆分record等。我們將該過程稱爲"拷貝&更新"。下圖描述了更多細節:

image

圖4:Parquet文件中的新寫時複製

性能測試

我們使用傳統的TPC-DS 數據方式測試比較了新的寫時複製的性能。

我們採用具有相同vCore數量的TPC-DS銷售數據和Spark作業的內存設置,並用開箱即用的配置進行了測試。我們對5%~50%的數據進行了更新,然後比較Delta Lake和新的寫時複製所花費的時間。對於真實的使用場景來說,50%的數據更新已經足夠了。

測試結果表明,新方法的更新速度更快。不同百分比數據的更新場景下都能保證其性能優勢。

image

總結

總之,高效的ACID upserts對今天的lakehouse至關重要。隨着Apache Hudi, Delta Lake 和 Apache Iceberg 的廣泛採納,upserts的慢操作也面臨挑戰,特別是在數據卷不斷擴展的情況下。爲了解決這個問題,我們在具有行級索引的Apache Parquet文件中引入了部分寫時複製,以此來跳過對不需要的數據頁的讀寫。在性能測試中展現了明顯的性能優勢。該方法使公司能夠高效地執行數據刪除和CDC,並適用於其他依賴於lakehouse中高效表更新的場景。

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