Apache Hudi 維護在給定表上執行的所有操作的Timeline(時間線),以支援以符合 ACID 的方式高效檢索讀取查詢的資料。 在寫入和表服務期間也會不斷查閱時間線,這是表正常執行的關鍵。 如果任何時間線操作出現混亂(由於多寫入未配置鎖提供程式等),則可能導致資料一致性問題(資料丟失或資料重複)或最終導致不可恢復的錯誤。 因此讓我們深入研究時間線Timeline的細微差別,以幫助操作 Apache Hudi 表。
Instant
在表格上執行的所有操作都表示為 Hudi 時間軸中的Instant(瞬間)。 可以在表基本路徑下找到一個名為“.hoodie”的目錄,其中維護這些Instant。 Hudi instant由以下元件組成:
- Instant操作:在表上執行的操作型別。
- Instant時間:毫秒格式的時間戳,被視為時間線上操作的識別符號。
- 狀態:當前Instant狀態。有 3 種不同的狀態:Requested(請求)、Inflight(執行)和 Completed(完成)。 給定instant將處於任何時間點的狀態之一。 每個操作都從"requested"狀態開始,然後移至"inflight",最後進入 "completed" 狀態,在這種情況下,整個操作被視為已完成。 在操作進入 "completed" 狀態之前,其被視為待處理,並且不允許讀取查詢從任何此類操作中讀取任何資料。
Hudi保證在Timeline時間軸上執行的操作是原子的並且基於Instant時間的時間軸一致。
Action
我們在 Apache Hudi 表上發生了很多不同的操作,每個操作都有不同的目的,例如由常規寫入者攝取資料、壓縮和聚簇、清理和歸檔等表服務。
對於部落格的大部分內容,我們將假設單寫入模型,因為重點是說明時間線事件。 但如果有必要的話,也會討論一些多寫入端的場景。
Commit
Commit(提交) 操作代表寫入 COW 表。 每當新批次被攝取到表中時,就會生成新的 CommtTime 並且操作進入請求狀態。 可以在 Hudi 時間軸中找到"tN.commit.requested"。 例如,20230705155904980.commit.requested(其中"20230705155904980"是該操作的提交時間,請求標誌著規劃階段的完成。對於常規寫入,準備階段沒有太多事情要做,執行階段從新增"inflight"開始,一旦執行完成,在時間軸中看到“已完成”的提交檔案。
| — 20230705155904980.commit.requested
| — 20230705155904980.commit.inflight
| — 20230705155904980.commit
因此在我們看到 20230705155904980.commit 之前,所有查詢都不會讀取此提交部分寫入的任何資料。 一旦透過將 20230705155904980.commit 新增到時間線來標記完成,任何命中表的新讀取都將讀取此感興趣的提交提交的資料。
Delta Commit
Delta Commit(增量提交)表示對 MOR 表的寫入。 這可能會產生日誌檔案或基本Parquet檔案。 但"增量提交"是指定期寫入 MOR 表。 該序列類似於我們上面看到的"提交"。
| — 20230707081934362.deltacommit.requested
| — 20230707081934362.deltacommit.inflight
| — 20230707081934362.deltacommit
提交和增量提交都只會導致新增新檔案。 完成的檔案將列出有關新增的檔案的所有元資訊,以及寫入的位元組數、寫入的記錄、更新的記錄等統計資訊。
Clean
Hudi 在對現有檔案組的任何更新中新增名為FileSlice(檔案切片)的新版本檔案。 舊版本的檔案切片由Cleaner(清理器)根據清理器配置清理(或刪除)。 與常規寫入(提交和增量提交)不同,Cleaner 還將經歷一個計劃階段,最終將導致 tX.clean.requested 包含清理計劃。 它將跟蹤需要在清理過程中刪除的所有檔案。
將計劃序列化到請求檔案中的主要原因是為了確保冪等性。 為了在清理過程中能夠防止中途崩潰,我們希望確保清理計劃一旦完成就能夠順利完成而不會失敗。 此外完成的清理準確顯示了哪些檔案作為清理提交的一部分被刪除,而不僅僅是部分檔案列表,無論重新嘗試清理多少次。 同樣的原理也適用於聚簇計劃、壓縮計劃和恢復計劃。
| — 20230708091954360.clean.requested
| — 20230708091954360.clean.inflight
| — 20230708091954360.clean
可以在完成的"20230708091954360.clean"檔案中找到有關清理器刪除的所有檔案的資訊。 讓我們透過一個簡單的示例來瞭解 Cleaner 的作用。
t1.commit:
- 插入新資料
- 新增新檔案fg1_fs1(fg指檔案組,fs指檔案切片)
t2.commit:
- 更新同一組資料。
- 將新檔案片 fg1_fs2 新增到現有檔案組 fg1。
t3.commit:
- 更新同一組資料。
- 將新檔案片 fg1_fs3 新增到現有檔案組 fg1。
現在Cleaner被觸發,Cleaner配置設定為“2”,以保留要保留的提交數。 因此任何早於最近 2 次提交建立的檔案切片都會被清理。 因此 Cleaner 會將 fg1_fs1 新增到 clean 計劃中,然後在執行過程中將其刪除。 因此儲存中僅留下 fg1_fs2 和 fg1_fs3。
t4.clean
- 清理fg1_fs1
這個迴圈將會重複。 例如 t5.commit 將新增 fg1_fs4,t6.clean 將刪除 fg1_fs2 等等。 可以在此處閱讀有關Cleaner的更多資訊。
Replace Commit
與提交和增量提交不同,某些操作可能會導致替換某些資料檔案。 例如,對於Clustering(聚簇),insert_overwrite 操作會新增新的資料檔案,但也會替換某些資料檔案。 其中大多數都是非同步的,因為替換的檔案不會同步刪除,而只是標記為替換。 在稍後的某個時間點,由清理器負責刪除檔案。
| — 20230707081954360.replacecommit.requested
| — 20230707081954360.replacecommit.inflight
| — 20230707081954360.replacecommit
比方說,使用 4 個提交 t1.commit(file1)、t2.commit(file2)、t3.commit(file3) 和 t4.commit(file4) 將 4 個資料檔案寫入表中。 這裡的每個檔案代表Hudi中的一個不同的檔案組。 假設我們觸發將小檔案批處理為大檔案。
t1.commit:
- 插入新資料 fg1_fs1
t2.commit:
- 插入新資料 fg2_fs1
t3.commit:
- 插入新資料 fg3_fs1
t4.commit:
- 插入新資料 fg4_fs1
t5.replacecommit 將建立一個新檔案,file5 替換先前提交建立的 4 個檔案。
t5.replacecommit
- 透過替換檔案組(1 至 4)建立新檔案組 fg5_fs1
在將 t5.replacecommit(已完成的時間線檔案)新增到時間線之前,讀取查詢將從 4 個檔案中讀取資料,一旦將完成的 t5.replacecommit 新增到時間線,任何新的讀取查詢將僅讀取 file5 並忽略 file1 到 file4。 完成的 t5.replacecommit 將包含有關新增哪些檔案和替換哪些檔案的所有資訊。
此外,Commit和Replace Commit之間的另一個區別是,常規提交的規劃階段沒有太多涉及。 但在Replace Commit情況下,規劃涉及遍歷現有檔案組,並根據聚簇計劃策略和配置,Hudi 將確定要考慮聚簇的檔案組以及如何將它們打包到不同的聚簇操作中。 因此對於非常大的表,即使是計劃也可能需要一些不小的時間。 此外在規劃階段結束時,有可能不會生成任何聚簇計劃,因此我們可能看不到任何".replacecommit.requested"檔案。 這意味著此時沒有任何東西可以聚簇,並且聚簇計劃將在稍後的某個時間再次重新嘗試。 可以在此處閱讀有關聚簇的更多資訊。
聚簇就是這樣的一個例子。 但還有其他操作會導致Replace Commit操作,其中包括insert_overwrite、insert_overwrite_table 和 delete_partition 操作。
Compaction Commit
Compaction(壓縮)是指將 MOR 表中的基礎檔案和關聯日誌檔案壓縮為新的基礎檔案的過程。 可以在此處閱讀有關壓縮的更多資訊。 與聚簇類似,這也將經歷一個規劃階段,並基於壓縮策略,可選地生成一個壓縮計劃,跟蹤日誌檔案列表和要壓縮的基本檔案。 如果生成了計劃,它將在時間線中生成一個compaction.requested 檔案。 這標誌著規劃階段的結束。 然後在執行階段,將建立一個inflight檔案,最終一旦壓縮完成,一個完成的檔案將被新增到時間線中以標記感興趣的壓縮的完成。
| — 20230707091954370.compaction.requested
| — 20230707091954370.compaction.inflight
| — 20230707091954370.commit
同樣與 Clean 和 Clustering 類似,計劃一旦序列化(換句話說,一旦requested檔案寫出),Hudi 就可以適應任意數量的崩潰和重新嘗試,最終 Hudi 一定會完成它,確保所有部分失敗的嘗試都得到正確清理,並且只有最終成功嘗試的資料檔案完好無損。 當操作一個非常大的表並且必須壓縮大量檔案組時,這一點非常關鍵。 此外假設計劃的壓縮最終完成,表中的其他操作也將繼續進行。 因此我們永遠無法恢復計劃的壓縮。 如果表中有更多寫入端,則必須不惜一切代價完成它,這是Hudi支援非同步壓縮的關鍵設計之一。 如果看到具有以下序列的時間線,則它是有效的事件序列。
| — t100.compaction.requested
| — t110.deltacommit.requested
| — t110.deltacommit.inflight
| — t100.compaction.inflight
| — t110.deltacommit
| — t100.commit
如果在連續模式下使用 Deltastreamer,這是通常看到的時間線事件序列。
Rollback
使用Rollback(回滾)操作回滾任何部分失敗的寫入。 在單寫入端模式下,回滾是急切的,即每當開始新的提交時,Hudi 都會檢查任何待處理的提交併觸發回滾。 在Hudi支援的所有不同操作中,只有Clean、Rollback和Restore會刪除檔案,其他操作都不會刪除任何資料檔案,Replace Commit可以將某些檔案標記為已替換,但不會刪除它們。
回滾計劃階段包括查詢作為部分失敗提交的一部分新增的所有檔案並將其新增到回滾計劃中。正如我們之前所看到的,計劃被序列化到 rollback.requested 檔案中。 執行首先在時間線中建立一個執行中的檔案,最終當回滾完成時,完成的回滾檔案將被新增到時間線中。
假設這是崩潰之前的時間線。
| — t10.commit.requestet
| — t10.commit.inflight
| — t10.commit
| — t20.commit.requested
| — t20.commit.inflight
就在這之後,程式崩潰了。 因此使用者重新啟動管道並將觸發回滾,因為 t20 被推斷為待處理。
| — t10.commit.requested
| — t10.commit.inflight
| — t10.commit
| — t20.commit.requested
| — t20.commit.inflight
| — t25.rollback.requested
回滾結束時,Hudi 會刪除正在回滾的提交的提交元檔案。 在這種情況下,與提交 t20 相關的所有時間線檔案都將被刪除。 因此回滾完成後的時間線可能如下所示。
| — t10.commit.requested
| — t10.commit.inflight
| — t10.commit
| — t25.rollback.requested。
| — t25.rollback.inflight
| — t25.rollback
對於多寫入端,Hudi 還引入了延遲迴滾,即它使用基於心跳的回滾機制,我們會在未來的部落格中更深入地瞭解回滾演演算法。
與聚簇、壓縮類似,回滾也被設計成冪等的。我們在請求檔案中序列化計劃,因此即使回滾中途崩潰,我們也可以重新嘗試,不會出現任何問題。 Hudi 確保重複使用相同的回滾即時時間來回滾給定的提交。 完成的回滾檔案將列出在回滾過程中刪除的所有檔案。 COW中的回滾將刪除部分寫入的檔案,但在MOR的情況下,如果部分失敗的提交新增了一個日誌檔案,則回滾將新增另一個帶有回滾塊的日誌檔案,並且不會刪除原始日誌檔案。 這是 MOR 表的關鍵設計之一,以將任何寫入保留為追加。 我們還可以在以後的一些部落格中檢視日誌檔案設計。
Savepoint
為了在災難和恢復場景中提供幫助,Hudi 引入了兩種操作,稱為Savepoint(儲存點)和Restore(恢復)。 將儲存點新增到提交可確保清理和歸檔不會觸及與儲存點提交相關的任何內容。 這意味著使用者可以根據需要將表恢復到感興趣的儲存點提交。 僅當儲存點尚未清理時才允許將其新增到提交中。
Savepoint 只有兩種狀態:正在執行和已完成。 由於沒有計劃階段,因此沒有儲存點請求。 在執行階段,Hudi 會查詢截至感興趣的提交時間提供讀取查詢所需的所有檔案。 這些檔案將新增到 tX.savepoint.inflight 檔案中。 並立即將完整的儲存點檔案新增到時間線中。
| — t10.commit.requested
| — t10.commit.inflight
| — t10.commit
| — t10.savepoint.inflight
| — t10.savepoint
也可以在稍後階段新增儲存點,只是清理程式不應該清理檔案。 例如表可能有從 t10 到 200 的提交(每 10 秒一次)。 因此在時間 t210,如果 Cleaner 清理 t30 之前的資料檔案,則允許為t50新增儲存點。
Restore
Restore(恢復)用於將整個表恢復到某個較舊的時間點。 萬一表中出現了一些壞資料,或者資料損壞或其他正當原因,如果使用者希望將表恢復到 10 小時前的狀態,恢復操作就會派上用場。 使用者可以將儲存點新增到 10 小時前的提交之一併觸發恢復。 從技術上講,恢復意味著按時間倒序回滾 N 個提交。 例如如果表有提交 t10、t20、t30、t40、t50、t60、t70、t80、t90 和 t100。 使用者更願意將表恢復到 t40。 Hudi 將回滾 t100,然後回滾 t90,然後回滾 t80,依此類推。直到 t50 回滾開始。
Hudi 將像其他表服務一樣經歷類似的狀態轉換。 將生成請求的計劃來跟蹤需要回滾的所有提交,然後在執行過程中,將建立一個執行中的檔案,最終完成後,完整的恢復檔案將新增到時間線中。
| — t10.commit.requested
| — t10.commit.inflight
| — t10.commit
| — t10.savepoint.inflight
| — t10.savepoint
| — t20.commit.requested
| — t20.commit.inflight
| — t20.commit
| - ..
| - ..
| — t100.commit.requested
| — t100.commit.inflight
| — t100.commit
恢復後時間線可能如下所示
| — t10.commit.requested
| — t10.commit.inflight
| — t10.commit
| — t10.savepoint.inflight
| — t10.savepoint
| — t120.restore.requested
| — t120.restore.inflight|
| — t120.restore
Index
Hudi支援新增各種索引來輔助讀寫延遲,此類分割槽包括列統計分割槽和布隆過濾器分割槽,要首次為大型表初始化這些索引,我們不能阻止攝取寫入器,因為它可能會佔用大量時間。 因此 Hudi 引入了 AsyncIndexer 來協助非同步初始化這些分割槽。
| — t200.indexing.requested
| — t200.indexing.inflight
| — t200.indexing
與任何其他操作一樣,這會經歷典型的狀態轉換,我們將在單獨的部落格中詳細介紹非同步索引。
Active/Archive Timeline
Hudi 將整個時間線剖析為Active Timeline(活動時間線)和Archive Timeline(存檔時間線)。 在".hoodie"目錄下看到的任何Instant均指活動時間線,而存檔的那些Instant將進入".hoodie/archived"目錄。 可以在此處閱讀有關存檔時間表的更多資訊。區分Ative/Archive Timeline背後的基本原理是確保我們對後設資料(時間線)有最大限制,防止隨著時間線越來越長,讀取出現延遲增加的情況。
Hudi CLI
Hudi CLI 有檢視錶時間線的命令。我們將在其他一些部落格中透過示例詳細介紹它們,如果想嘗試一下,命令是"timeline"。
結論
時間線在提供符合 ACID 的正確資料方面發揮著非常重要的作用。 瞭解不同的時間線事件對於管理任何組織中的 Apache Hudi 表都非常有益,並且還有助於根據需要進行問題排查。