天天看點

圖文帶你了解 Apache Iceberg 時間旅行是如何實作的?

為了更好的使用 Apache Iceberg,了解其時間旅行是很有必要的,這個其實也會對 Iceberg 表的讀取過程有個大緻了解。不過在介紹 Apache Iceberg 的時間旅行(Time travel)之前,我們需要了解 Apache Iceberg 的底層資料組織結構。

Apache Iceberg 的底層資料組織

我們需要先了解一下 Apache Iceberg 關于底層資料結構的一些術語。

Apache Iceberg 用到的一些術語:

資料檔案(data files)

資料檔案(data files)是 Apache Iceberg 表真實存儲資料的檔案,一般是在表的資料存儲目錄的 data 目錄下。如果我們的檔案格式選擇的是 parquet,那麼檔案是以 .parquet 結尾,比如 00000-0-0eca9076-9c03-4077-baa9-e68769e15c58-00001.parquet 就是一個資料檔案。

每次更新會産生多個資料檔案。

清單檔案(Manifest file)

清單檔案其實是中繼資料檔案,其裡面列出了組成某個快照(snapshot)的資料檔案清單。每行都是每個資料檔案的較長的描述,包括資料檔案的狀态、檔案路徑、分區資訊、列級别的統計資訊(比如每列的最大最小值、空值數等)、檔案的大小以及檔案裡面資料的行數等資訊。其中列級别的統計資訊在 Scan 的時候可以為算子下推提供資料,以便可以過濾掉不必要的檔案。

清單檔案是以 avro 格式進行存儲的,是以是以 .avro 字尾結尾的,比如 d5ba704c-1453-4f18-9077-6944baa1b3f2-m0.avro。

每次更新會産生多個清單檔案。

清單清單(Manifest list)

清單清單也是中繼資料檔案,其裡面存儲的是清單檔案的清單,每個清單檔案占據一行。每行中存儲了清單檔案的路徑、清單檔案裡面存儲資料檔案的分區範圍、增加了幾個資料檔案、删除了幾個資料檔案等資訊。這些資訊可以用來在查詢時提供過濾。

清單清單也是 avro 格式進行存儲的,是以是以 .avro 字尾結尾的;而且這個檔案是以 snap- 開頭的,比如 snap-7389540589641972921-1-aa90f6ed-aee2-49c7-a61c-bd13ed411c66.avro,其中 7389540589641972921 這串數字是代表快照 id(snapshot_id)。

每次更新都會産生一個清單清單檔案。

快照(Snapshot)

快照代表一張表在某個時刻的狀态。每個快照裡面會列出表在某個時刻的所有資料檔案清單。Data files 是存儲在不同的 manifest files 裡面, manifest files 是存儲在一個 Manifest list 檔案裡面,而一個 Manifest list 檔案代表一個快照。

Apache Iceberg 表的資料組織

前面我們已經介紹了 Apache Iceberg 的常用術語,有了這些知識,我們就可以看懂一張 Iceberg 表的底層資料結構組織。比如下面的目錄結構是某一張表在某個時刻的狀态:

[email protected] :/data/hive/warehouse/iteblog|
⇒  tree
.
├── data
│   └── ts_year=2020
│       └── id_bucket=0
│           ├── 00000-0-0eca9076-9c03-4077-baa9-e68769e15c58-00001.parquet
│           ├── 00000-0-2b289c49-c807-4187-ac8c-8d74c674d577-00001.parquet
│           ├── 00000-0-e114f45e-5431-409e-9c00-67bb39f21918-00001.parquet
│           ├── 00001-1-259dfb2b-10f5-41dc-a0fb-6bc7f890a28a-00001.parquet
│           ├── 00001-1-7353eecc-2306-4eea-b6fe-bce9ef650837-00001.parquet
│           └── 00001-1-e11d79c5-a4b1-4178-a4a2-1a5787d6ff18-00001.parquet
└── metadata
    ├── 00000-fc69176c-1ad7-4a66-99f7-a53f4ec0cb4d.metadata.json
    ├── 00001-e83023da-46d5-42fb-9652-fd88faa4b536.metadata.json
    ├── 00002-32cd214b-1ce8-4642-ab70-4e1951f35fed.metadata.json
    ├── 00003-8b83e7d6-83e7-4589-9a20-1e02f1488a2d.metadata.json
    ├── aa90f6ed-aee2-49c7-a61c-bd13ed411c66-m0.avro
    ├── d20a2213-469f-4652-803f-f149741e9a6e-m0.avro
    ├── d20a2213-469f-4652-803f-f149741e9a6e-m1.avro
    ├── d20a2213-469f-4652-803f-f149741e9a6e-m2.avro
    ├── d5ba704c-1453-4f18-9077-6944baa1b3f2-m0.avro
    ├── snap-2875980136834144366-1-d20a2213-469f-4652-803f-f149741e9a6e.avro
    ├── snap-7112118703100649335-1-d5ba704c-1453-4f18-9077-6944baa1b3f2.avro
    └── snap-7389540589641972921-1-aa90f6ed-aee2-49c7-a61c-bd13ed411c66.avro
4 directories, 18 files
           

可以看到,這裡面有三個清單清單檔案,分别是 snap-2875980136834144366-1-d20a2213-469f-4652-803f-f149741e9a6e.avro、snap-7112118703100649335-1-d5ba704c-1453-4f18-9077-6944baa1b3f2.avro 以及 snap-7389540589641972921-1-aa90f6ed-aee2-49c7-a61c-bd13ed411c66.avro。這三個清單清單檔案其實對于的就是表的三個快照。

Apache Iceberg 時間旅行的實作

現在是時候介紹時間旅行的實作了。時間旅行其實對應的是如何讀取 Iceberg 表中的資料,為了更加清晰的說明這些,我畫了一張圖以便大家可以更好的了解。

圖文帶你了解 Apache Iceberg 時間旅行是如何實作的?

假設我們的表是存儲在 Hive 的 MetaStore 裡面的,表名為 iteblog,并且資料的組織結構如上如所示。

查詢最新快照的資料

如果想查詢表的最新快照資料,在 Iceberg 中是這麼進行的:

•通過資料庫名和表名,從 Hive 的 MetaStore 裡面拿到表的資訊。從表的屬性裡面其實可以拿到 metadata_location 屬性,通過這個屬性可以拿到 iteblog 表的 Iceberg 的 metadata 相關路徑,這個也就是上圖步驟①的 /user/iteblog/metadata/2.metadata.json。

•解析 /user/iteblog/metadata/2.metadata.json 檔案,裡面可以拿到目前表的快照 id(current-snapshot-id),以及這張表的所有快照資訊,也就是 JOSN 資訊裡面的 snapshots 數組對應的值。從上圖可以看出,目前表有兩個快照,id 分别為 1 和 2。快照 1 對應的清單清單檔案為 /user/iteblog/metastore/snap-1.avro;快照 2 對應的清單清單檔案為 /user/iteblog/metastore/snap-2.avro。

•如果我們想讀取表的最新快照資料,從 current-snapshot-id 可知,目前最新快照的 ID 等于 2,是以我們隻需要解析 /user/iteblog/metastore/snap-2.avro 清單清單檔案即可。從上圖可以看出,snap-2.avro 這個清單清單檔案裡面有兩個清單檔案,分别為 /user/iteblog/metadata/3.avro 和 /user/iteblog/metadata/2.avro。注意,除了清單檔案的路徑資訊,還有 added_data_files_count、existing_data_files_count 以及 deleted_data_files_count 三個屬性。Iceberg 其實是根據 deleted_data_files_count 大于 0 來判斷對應的清單檔案裡面是不是被删除的資料。由于上圖 /user/iteblog/metadata/2.avro 清單檔案的 deleted_data_files_count 大于 0 ,是以讀資料的時候就無需讀這個清單檔案裡面對應的資料檔案。在這個場景下,讀取最新快照資料隻需要看下 /user/iteblog/metadata/3.avro 清單檔案裡面對應的資料檔案即可。

•這時候 Iceberg 會解析 /user/iteblog/metadata/3.avro 清單檔案,裡面其實就隻有一行資料,也就是 /user/iteblog/data/4.parquet,是以我們讀 iteblog 最新的資料其實隻需要讀 /user/iteblog/data/4.parquet 資料檔案就可以了。注意,上面 /user/iteblog/metadata/2.avro 檔案裡面對應的内容為

{"status":2,"data_file":{"file_path":"/user/iteblog/data/3.parquet"}}
{"status":2,"data_file":{"file_path":"/user/iteblog/data/2.parquet"}}
{“status":2,"data_file":{"file_path":"/user/iteblog/data/1.parquet"}}
           

其中的 status = 2 代表 DELETED,也就是删除,也印證了讀最新快照的資料其實不用讀 /user/iteblog/data/2.avro 清單檔案的資料檔案。而 /user/iteblog/data/3.avro 清單檔案裡面存儲的内容為 {"status":1,"data_file":{"file_path":"/user/iteblog/data/4.parquet"}},其 status = 1,代表 ADDED,也就是新增的檔案,是以得讀取。

查詢某個快照的資料

Apache Iceberg 支援查詢曆史上任何時刻的快照,在查詢的時候隻需要指定 snapshot-id 屬性即可,比如我們想查詢上面 snapshot-id 為 1 的資料,可以在 Spark 中這麼寫:

 spark.read .option("snapshot-id", 1L) .format("iceberg") .load("path/to/table") 下面是讀取指定快照的圖示:

圖文帶你了解 Apache Iceberg 時間旅行是如何實作的?

從上圖可以看出,和讀取最新快照資料不一樣的地方是上圖中的第三步。由于我們指定了 snapshot-id = 1,是以 Iceberg 會讀取上面第二步白色的部分,可以知道,snapshot-id = 1 對應的清單清單檔案為 /user/iteblog/metastore/snap-1.avro。這時候讀出清單清單裡面的檔案,其實就隻有一行資料,對應的清單檔案為 /user/iteblog/metadata/1.avro,其中 added_data_files_count 為 3。

下一步我們讀取 /user/iteblog/metadata/1.avro 清單檔案,可以看到裡面有三個資料檔案路徑,這些資料檔案就是 snapshot-id = 1 的資料。

根據時間戳檢視某個快照的資料

Iceberg 還支援通過 as-of-timestamp 參數指定時間戳來讀取某個快照的資料。如下所示:

spark.read
    .option("as-of-timestamp", "12346")
    .format("iceberg")
    .load("path/to/table")
           

 在上面例子中,我們指定 as-of-timestamp = 123456。那 Iceberg 是如何處理這個查詢呢?其查詢邏輯如下圖:

圖文帶你了解 Apache Iceberg 時間旅行是如何實作的?

 我們注意上面圖中第二步裡面的 JSON 資料裡面有個 snapshot-log 數組,如下:

"snapshot-log" : [ {
  "timestamp-ms" : 12345,
  "snapshot-id" : 1
}, {
  "timestamp-ms" : 23456,
  "snapshot-id" : 2
}]
           

每個清單裡面都有個 timestamp-ms 屬性和 snapshot-id 屬性,并且是按照 timestamp-ms 升序的。在 Iceberg 内部實作中,它會将 as-of-timestamp 指定的時間和 snapshot-log 數組裡面每個元素的 timestamp-ms 進行比較,找出最後一個滿足 timestamp-ms <= as-of-timestamp 對應的 snapshot-id。

由于 as-of-timestamp=12346 比 12345 時間戳大,但是比 23456 小,是以會取 snapshot-id = 1,也就是拿到 snapshot-id = 1 的快照資料。剩下的資料查詢步驟和在查詢中指定 snapshot-id 是一緻的,我就不再介紹了。

好了,到這裡我們已經基本清楚 Iceberg 是如何根據查詢條件擷取對應哪個快照,并且映射到資料檔案,完成一次查詢。