Hudi Timeline簡析

前言

Long time no see(鞠躬

最近終于開始嘗試推廣Hudi在部門內部的應用,作為流批一體計劃的最后一塊拼圖,順便復活許久未更的博客,希望今后至少能保持周更的節奏吧。

在Hudi官方文檔的開頭列舉了四大核心概念,分別是:

  • Timeline
  • File Layout
  • Table Types
  • Query Types

本文就來簡要地談談Timeline。

Timeline作用與結構

官網關于Timeline的頁面洋洋灑灑介紹了很多,但是少了筆者認為最關鍵的、本質的概念:

Timeline就是Hudi的事務日志。

讀者可以回想一下MySQL中的Redo/Undo Log、Kudu中的Redo/Undo File(可參見很久之前寫的解析)。Timeline在Hudi中扮演的角色和它們基本相同(盡管Hudi并不是一個數據庫系統),也就是說,Hudi依靠Timeline提供快照隔離(SI)的事務語義,并使得增量查詢、Time-travel等特性成為可能。

每張Hudi表都有一條Timeline,由許多Instant組成,其中維護了各個時間點在該表上進行的操作。每個Instant又包含以下3個主要field。

  • Time:操作進行的時間戳,單調遞增,格式為yyyyMMddHHmmssSSS;
  • Action:該時間戳進行的具體操作,如commit、compaction等,所有操作都是原子的;
  • State:這個Instant的狀態,包含requestedinflightcompleted三種。

Timeline和Instant的詳細圖示如下。

關于各個Action和State值的含義,可直接參考文檔,這里不再贅述。

Timeline以文件序列的形式存儲,其路徑位于/path/to/table/.hoodie目錄,每個文件的命名方式是[time].[action].[state](處于completed狀態的Instant沒有state后綴),例如:20220822181448272.deltacommit.inflight。不同類型的Action對應的文件格式由不同的Avro Schema定義,以一個已經完成的deltacommit操作為例,它對應的Instant數據節選如下:

{
      "fileId" : "6e0ef835-2474-4182-b085-e64994788729",
      "path" : "2022-08-22/.6e0ef835-2474-4182-b085-e64994788729_20220822181218028.log.1_3-4-0",
      "prevCommit" : "20220822181218028",
      "numWrites" : 179,
      "numDeletes" : 0,
      "numUpdateWrites" : 179,
      "numInserts" : 0,
      "totalWriteBytes" : 60666,
      "totalWriteErrors" : 0,
      "tempPath" : null,
      "partitionPath" : "2022-08-22",
      "totalLogRecords" : 0,
      "totalLogFilesCompacted" : 0,
      "totalLogSizeCompacted" : 0,
      "totalUpdatedRecordsCompacted" : 0,
      "totalLogBlocks" : 0,
      "totalCorruptLogBlock" : 0,
      "totalRollbackBlocks" : 0,
      "fileSizeInBytes" : 199309,
      "minEventTime" : null,
      "maxEventTime" : null,
      "logVersion" : 1,
      "logOffset" : 0,
      "baseFile" : "6e0ef835-2474-4182-b085-e64994788729_0-4-0_20220822181218028.parquet",
      "logFiles" : [ ".6e0ef835-2474-4182-b085-e64994788729_20220822181218028.log.1_3-4-0" ],
      "recordsStats" : {
        "val" : null,
        "present" : false
      },
      "columnStats" : {
        "val" : null,
        "present" : false
      }
}

Timeline實現

Timeline的類層次體系如下圖所示。

HoodieTimeline接口定義了所有合法的Action和State的組合(也就是Instant文件的擴展名組合),以及Instant的獲取、過濾和文件名拼接等規范,主要的實現則位于HoodieDefaultTimeline類。所有的Instant維護在List<HoodieInstant>容器中。

舉個例子,Flink-Hudi Sink配備了生成Inline Compaction計劃的算子CompactionPlanOperator,在每個Checkpoint完畢時負責調度。它需要在Timeline中尋找第一個pending的Compaction操作,就會用到HoodieDefaultTimeline提供的對應方法:

// CompactionPlanOperator
  private void scheduleCompaction(HoodieFlinkTable<?> table, long checkpointId) throws IOException {
    // the first instant takes the highest priority.
    Option<HoodieInstant> firstRequested = table.getActiveTimeline().filterPendingCompactionTimeline()
        .filter(instant -> instant.getState() == HoodieInstant.State.REQUESTED).firstInstant();
    if (!firstRequested.isPresent()) {
      // do nothing.
      LOG.info("No compaction plan for checkpoint " + checkpointId);
      return;
    }
    // ......
  }
// HoodieDefaultTimeline
  @Override
  public HoodieTimeline filterPendingCompactionTimeline() {
    return new HoodieDefaultTimeline(
        instants.stream().filter(s -> s.getAction().equals(HoodieTimeline.COMPACTION_ACTION) && !s.isCompleted()), details);
  }

下面再來看看HoodieDefaultTimeline的兩個實現。

HoodieActiveTimeline

顧名思義,HoodieActiveTimeline維護當前活動的Timeline,它的主要作用是讀寫不同Action、不同State對應的Instant文件,所以大部分操作都是直接對文件操作。以requested狀態到inflight狀態的轉換為例,代碼比較易懂,其他操作都類似:

  public void transitionRequestedToInflight(HoodieInstant requested, Option<byte[]> content,
      boolean allowRedundantTransitions) {
    HoodieInstant inflight = new HoodieInstant(State.INFLIGHT, requested.getAction(), requested.getTimestamp());
    ValidationUtils.checkArgument(requested.isRequested(), "Instant " + requested + " in wrong state");
    transitionState(requested, inflight, content, allowRedundantTransitions);
  }

  private void transitionState(HoodieInstant fromInstant, HoodieInstant toInstant, Option<byte[]> data,
       boolean allowRedundantTransitions) {
    ValidationUtils.checkArgument(fromInstant.getTimestamp().equals(toInstant.getTimestamp()));
    try {
      if (metaClient.getTimelineLayoutVersion().isNullVersion()) {
        // Re-create the .inflight file by opening a new file and write the commit metadata in
        createFileInMetaPath(fromInstant.getFileName(), data, allowRedundantTransitions);
        Path fromInstantPath = getInstantFileNamePath(fromInstant.getFileName());
        Path toInstantPath = getInstantFileNamePath(toInstant.getFileName());
        boolean success = metaClient.getFs().rename(fromInstantPath, toInstantPath);
        if (!success) {
          throw new HoodieIOException("Could not rename " + fromInstantPath + " to " + toInstantPath);
        }
      } else {
        // Ensures old state exists in timeline
        LOG.info("Checking for file exists ?" + getInstantFileNamePath(fromInstant.getFileName()));
        ValidationUtils.checkArgument(metaClient.getFs().exists(getInstantFileNamePath(fromInstant.getFileName())));
        // Use Write Once to create Target File
        if (allowRedundantTransitions) {
          FileIOUtils.createFileInPath(metaClient.getFs(), getInstantFileNamePath(toInstant.getFileName()), data);
        } else {
          createImmutableFileInPath(getInstantFileNamePath(toInstant.getFileName()), data);
        }
        LOG.info("Create new file for toInstant ?" + getInstantFileNamePath(toInstant.getFileName()));
      }
    } catch (IOException e) {
      throw new HoodieIOException("Could not complete " + fromInstant, e);
    }
  }

除此之外,HoodieActiveTimeline還有一個非常重要的功能是生成新的Instant時間戳:

  public static String createNewInstantTime(long milliseconds) {
    return lastInstantTime.updateAndGet((oldVal) -> {
      String newCommitTime;
      do {
        if (commitTimeZone.equals(HoodieTimelineTimeZone.UTC)) {
          LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
          newCommitTime = now.format(MILLIS_INSTANT_TIME_FORMATTER);
        } else {
          Date d = new Date(System.currentTimeMillis() + milliseconds);
          newCommitTime = MILLIS_INSTANT_TIME_FORMATTER.format(convertDateToTemporalAccessor(d));
        }
      } while (HoodieTimeline.compareTimestamps(newCommitTime, HoodieActiveTimeline.LESSER_THAN_OR_EQUALS, oldVal));
      return newCommitTime;
    });
  }

注意最近一個Instant的時間以AtomicReference<String>來維護,這樣就可以通過CAS操作(updateAndGet())來保證Instant的時間戳單調遞增。

活動Timeline中可維護的Commit數目的上下界可由參數hoodie.keep.max.commitshoodie.keep.min.commits來指定,默認值分別為30和20。

HoodieArchivedTimeline

隨著Hudi表不斷寫入,Instant會逐漸增多,為了降低活動Timeline上的文件壓力,需要對比較久遠的Instant進行歸檔,并將這些Instant從活動Timeline移除。這個操作一般是默認執行的(hoodie.archive.automatic默認為true ),歸檔后的Instant就會維護在HoodieArchivedTimeline中,位于/path/to/table/.hoodie/archived目錄下。觸發自動歸檔的Commit數上下界則由參數archive.max_commitsarchive.min_commits指定,默認值分別為50和40。

HoodieArchivedTimeline進行歸檔的邏輯并不在它內部,而位于HoodieTimelineArchiver中,看官可自行參考其源碼。為了進一步減少小文件的影響,在歸檔的同時還可以進行小文件合并,與合并操作相關的參數有:

  • hoodie.archive.merge.enable:是否啟用歸檔合并,默認false;
  • hoodie.archive.merge.small.file.limit.bytes:小文件閾值,默認20971520字節;
  • hoodie.archive.merge.files.batch.size:合并小文件的批次大小,默認為10。

The End

晚安晚安。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容