【Hudi】原始碼解讀——Archive 流程

發表於2023-09-23

簡介

在資料不斷寫入 Hudi 期間,Hudi 會不斷生成 commit、deltacommit、clean 等 Instant 記錄每一次操作型別、狀態及詳細的後設資料,這些 Instant 最終都會存到 .hoodie 後設資料目錄下,為了避免後設資料檔案數量過多,ActiveTimeline 越來越長,需要對比較久遠的操作進行歸檔(archive),將這部分操作移到 .hoodie/archive 目錄下,單獨形成一個 ArchivedTimeline。

由此可知,archive 是 Hudi Timeline 上的操作,操作的資料是 instant 粒度。

相關配置

// org.apache.hudi.config.HoodieArchivalConfig
public static final ConfigProperty<String> ASYNC_ARCHIVE = ConfigProperty
    .key("hoodie.archive.async")
    .defaultValue("false")
    .sinceVersion("0.11.0")
    .withDocumentation("Only applies when " + AUTO_ARCHIVE.key() + " is turned on. "
          + "When turned on runs archiver async with writing, which can speed up overall write performance.");

public static final ConfigProperty<String> MAX_COMMITS_TO_KEEP_PROP = ConfigProperty
    .key("hoodie.keep.max.commits")
    .defaultValue("150")
    .withDocumentation("Archiving service moves older entries from timeline into an archived log after each write, to "
        + " keep the metadata overhead constant, even as the table size grows."
        + "This config controls the maximum number of instants to retain in the active timeline. ");

public static final ConfigProperty<String> MIN_COMMITS_TO_KEEP_PROP = ConfigProperty
    .key("hoodie.keep.min.commits")
    .defaultValue("145")
    .withDocumentation("Similar to " + MAX_COMMITS_TO_KEEP_PROP.key() + ", but controls the minimum number of"
        + "instants to retain in the active timeline.");
        
public static final ConfigProperty<String> COMMITS_ARCHIVAL_BATCH_SIZE_PROP = ConfigProperty
  .key("hoodie.commits.archival.batch")
  .defaultValue(String.valueOf(10))
  .withDocumentation("Archiving of instants is batched in best-effort manner, to pack more instants into a single"
      + " archive log. This config controls such archival batch size.");

MAX_COMMITS_TO_KEEP_PROP:ActiveTimeLine 最多保留的 instant 個數

MIN_COMMITS_TO_KEEP_PROP:ActiveTimeLine 最少保留的 instant 個數

COMMITS_ARCHIVAL_BATCH_SIZE_PROP:每次 archive 最多的 instant 個數

注:archive 配置與 clean 配置存在關係:hoodie.cleaner.commits.retained <= hoodie,keep.min.commits

入口

checkpoint 的時候提交完 writeStat 後(org.apache.hudi.client.BaseHoodieWriteClient#commitStats),會嘗試進行 archive:

// HoodieFlinkWriteClient
protected void mayBeCleanAndArchive(HoodieTable table) {
    autoArchiveOnCommit(table);
}

根據是否配置了 hoodie.archive.async 決定是同步/非同步歸檔。

歸檔

HoodieTimelineArchiver

public boolean archiveIfRequired(HoodieEngineContext context) throws IOException {
try {
  List<HoodieInstant> instantsToArchive = getInstantsToArchive().collect(Collectors.toList());

  boolean success = true;
  if (!instantsToArchive.isEmpty() && instantsToArchive.size() >= this.config.getCommitArchivalBatchSize()) {
    this.writer = openWriter();
    LOG.info("Archiving instants " + instantsToArchive);
    archive(context, instantsToArchive);
    LOG.info("Deleting archived instants " + instantsToArchive);
    success = deleteArchivedInstants(instantsToArchive);
  } else {
    LOG.info("No Instants to archive");
  }

  return success;
} finally {
  close();
}
}

該方法定義了整體 archive 的流程:

  1. 找出所有可歸檔的 instant;
  2. 執行歸檔,即寫到 archive 目錄下;
  3. 刪除歸檔檔案,即刪除 .hoodie 下所有對應的 instant,以及 .aux 目錄下所有的 compaction.requested。

找出所有可歸檔的 instant

獲取所有已完成的 clean、rollback instant,按操作型別分組,如果某類操作 instant 超過了最大保留的 instant,則觸發歸檔,只留下最少需保留的 instant。

getCommitInstantsToArchive:

  1. 獲取最老的未完成的 compaction、replace commit instant;
  2. 獲取最老的處於 inflight 狀態的 commit、delta commit instant;
  3. 找到該表的第一個 savepoint instant;
  4. 獲取 completed commit timline(COW 表為 commit、replace commit,MOR 為 commit、delta commit、replace commit),判斷其 instant 個數是否超過 maxInstantsToKeep;
  5. 如果超過,針對 MOR 表找到尚未參與 compaction 的最早的 delta commit instant;
  6. 獲取 clutering 場景下需保留的最早的 instant:

    1. 找到最近一次已完成的 clean instant,讀取後設資料中的 earliestInstantToRetain,取比 earliestInstantToRetain 新的 instants 中最老的 replace commit instant;
    2. 找到最老的處於 inflight 狀態的 replace commit instant,如果存在,找到在此之前最近一次 commit/delta commit/replace commit;
    3. 取 1 和 2 中更老的 instant。
    不能歸檔 clustering instant,除非其對應的資料檔案已被清理,不然 fs view 可能會重複讀資料(替換前後檔案都存在的情況下)
  7. 如果開啟 hoodie.archive.beyond.savepoint,則歸檔的 instant 只跳過 savepoint instant,否則則跳過第一個 sapoveint 及之後所有的 instant;
  8. 篩選 instant time 比上述 instant 小的 compleated commit/delat commit/replace commit,且滿足保留 instant 個數大於 minInstantsToKeep。

getCleanInstantsToArchive:

  1. 獲取所有已完成的 clean、rollback instant;
  2. 如果 clean 或 rollback 的 instant 個數超過 maxInstantsToKeep,則去掉最新的 minInstantsToKeep 個數的 instants,否則返回空;

透過這兩個把需要 archive 的 completed instants 篩選出來後,再結合 timeline 填充這些 instant 對應的 requested/inflight instants。

執行歸檔

刪除這些 instant 對應的 markfile 目錄,.hoodie/.temp/\<instantTs\>

將每個要歸檔的 instant 轉成一個 IndexedRecord,對應的具體實現為 HoodieArchivedMetaEntry,主要包含四部分資訊:

  • commitTime:instant 對應時間戳;
  • actionState:操作狀態;
  • actionType:操作型別;
  • metadata:操作的後設資料;

最後將這些 record 組織成 HoodieAvroDataBlock 按 log 格式落盤,落盤的檔名格式與 logfile 一致,需要將 .log字尾改為 .archive,比如 .hoodie/archived/commits_.archive.1。

刪除已歸檔的 Instant

刪除需要先刪 requested/inflight 再刪 completed。如果先刪 completed 的檔案,則其他基於 timeline 的服務(比如 compaction、clustering)可能會錯誤的將某次操作狀態認為是 pending,從而造成 bug。

如果 archive 的 instants 中包含 commit/delta commit,則刪除對應 aux 目錄下的後設資料檔案。

合併歸檔

從0.11版本開始,Hudi 支援對歸檔的結果檔案進行合併HoodieTimelineArchiver#mergeArchiveFilesIfNecessary。雖然每次歸檔都將多個 Instant 生成單個 .commit_.arvhive 檔案,但歸檔的次數多後也會有大量的歸檔結果檔案,所以對於較小的歸檔結果檔案進行合併。
當啟用歸檔合併時(hoodie.archive.merge.enable)且檔案系統支援append(StorageSchemas,例如:file://),如果arvhive目錄下存在大小小於 hoodie.archive.merge.small.file.limit.bytes 的檔案數量達到 hoodie.archive.merge.files.batch.size,即觸發合併

  1. 從新到舊獲取連續所有大小小於 hoodie.archive.merge.small.file.limit.bytes 的候選檔案 candidateFiles;
  2. 在這次歸檔生成的檔案的基礎上 rollover,作為合併歸檔用的資料檔案;
  3. 生成合並計劃,記錄要合併的檔案、合併後的檔名等資訊,將這些內容儲存在 arvhive 目錄下的 mergeArchivePlan 檔案;
  4. 合併檔案,將小檔案的內容都讀出來寫到新的合併檔案中去;
  5. 成功後刪除候選檔案。

相關文章