Iceberg 剖析 | Iceberg 是如何提高查詢效能的

語言: CN / TW / HK

在上週介紹Iceberg原理的文章中,我有提到Iceberg的設計初衷是為了解決Hive數倉所遇到的問題,主要有4點:

  1. 沒有ACID保證

  2. 只能支援partition粒度的謂詞下推

  3. 確定掃描的檔案需要使用檔案系統的list操作

  4. partition欄位必須顯式出現在query裡面

其中2和3都是直接影響query效能的問題, 而Iceberg的主打賣點正是更快的查詢速度 。作者Ryan Blue在DataWorks Summit上宣講Iceberg的ppt上就有列出Netflix在使用了Iceberg之後的效能提升情況

Hive光是確定查詢計劃就要9.6分鐘,而Iceberg確定查詢計劃只要42秒

在騰訊關於Iceberg的《 速度提升10倍,騰訊基於Iceberg的資料治理優化實踐 》這篇文章中,也提到了騰訊採用Iceberg的一部分原因正是看中了其對查詢效能的提升。所以這篇文章的主題,就是來 講一講Iceberg是如何減少查詢的資料量,從而提高查詢效能的

首先,對於儲存層的資料湖系統(Delta,Hudi,Iceberg)來說,query在儲存層發生的事情粗略地可分為兩個階段:

  1. 確定需要讀取的檔案列表( planning階段

  2. 讀取資料並傳遞給計算引擎( execution階段

對於execution階段,相信比較符合直覺,是所有人都認可的儲存層需要做的事情。但是對於planning階段,或許有些朋友會有疑問:Spark等計算引擎需要產生執行計劃,我還能理解, 為什麼儲存層也需要執行計劃

其實儲存層的執行計劃沒有計算層的那麼複雜,但確實也有一些需要做計劃的事情,主要是以下2點:

  1. 需要讀取哪些partition

  2. 每個partition下有哪些檔案

這兩件事情的內容一目瞭然,我就不再贅述了。

講完了planning階段需要解決的問題,回到我們最初的主題,來講講Iceberg所做的優化。其實Iceberg在這兩個階段都有做優化,優化機制總的來說主要有2種:

  1. Partition Pruning(分割槽剪枝)

  2. Predicate Pushdown(謂詞下推)

接下來就會講一講Iceberg是如何實現這兩個功能的。

Partition Pruning(分割槽剪枝) ,主要針對的是planning階段中的第一個問題: 需要讀取哪些partition

分割槽剪枝並不是一個新鮮事物。比如Hive就會根據查詢條件來決定是否使用分割槽查詢,以及具體查詢哪個或哪幾個分割槽,其實就是一種分割槽剪枝。

Hive會根據查詢條件確定讀取哪個分割槽

Iceberg沒有沿用Hive相對簡單的分割槽規則,而是自己實現了一套更為複雜的分割槽系統及分割槽剪枝演算法,名為Hidden Partition。 Iceberg選擇自己實現,目的是為了克服Hive的分割槽功能在使用上的不方便以及容易出錯。在Iceberg裡面,分割槽是儲存系統的一個實現細節,使用者無需理解分割槽和檔案系統的路徑是如何對應的。(關於Hive的分割槽功能的問題,請見上一篇文章)

接下來具體講一講Iceberg的分割槽剪枝演算法。

首先在Iceberg中我們用下面的CREATE語句來建立分割槽表

CREATE TABLE foobar (id bigint, data string) USING iceberg PARTITIONED BY (truncate(id, 3))

可以看到建表語法和Hive是一樣的

和Hive不同的是, Iceberg實現分割槽剪枝不是依賴檔案所在的目錄,而是利用了Iceberg特有的manifest檔案 。上一篇文章中有提到,Iceberg每次寫入都會產生一個新的snapshot,而一個snapshot在檔案系統上就對應一個manifest檔案。

manifest檔案的內容類似如下

{
...
"snapshot_id": {
"long": 4370069137697126000
},
"data_file": {
"file_path": ".../table/data/id_trunc=0/00000-0-1c1865ee-a812-465c-87cb-7588478c2d8f-00001.parquet",
"file_format": "PARQUET",
"partition": {
"id_trunc": {
"long": 0
}
},
...
}
}
{
...
"snapshot_id": {
"long": 4370069137697126000
},
"data_file": {
"file_path": ".../table/data/id_trunc=3/00001-1-b18c1240-86bb-48da-85e9-4a0e41a82b26-00001.parquet",
"file_format": "PARQUET",
"partition": {
"id_trunc": {
"long": 3
}
},
...
}
}

其中的data_file欄位記錄的是這個snapshot裡包含的資料檔案的資訊,注意到data_file欄位裡有一個欄位是partition,裡面記錄的就是這個data_file所在的partition資訊。 Iceberg正是使用這些元資料確定每個分割槽裡包含哪些檔案的。

相比於Hive,Iceberg的這種partition實現方式有以下好處:

  1. 直接定位到parquet檔案 ,無需呼叫檔案系統的list操作。

  2. partition的儲存方式對使用者透明 ,使用者在修改partition定義時Iceberg可以自動地修改儲存佈局而無需使用者操作。

講完了分割槽剪枝,接下來再講一講 Predicate Pushdown(謂詞下推)

前幾篇文章中也有提到過,謂詞下推是計算引擎(Spark等)把查詢的過濾條件(where條件)下推到儲存層,在儲存層面就把一部分必然不滿足條件的資料過濾掉,從而減少儲存層返回給計算引擎的資料量。

在講Iceberg如何實現謂詞下推之前,我先講一講 Spark是如何實現謂詞下推的

Spark作為計算層的系統,自己並不實現謂詞下推,而是交給檔案格式的reader來解決。例如,對於parquet檔案,Spark使用下面這個類讀取parquet檔案

/**
* @param readSupport Object which helps reads files of the given type, e.g. Thrift, Avro.
* @param filter for filtering individual records
*/
public ParquetRecordReader(ReadSupport<T> readSupport, Filter filter) {
internalReader = new InternalParquetRecordReader<T>(readSupport, filter);
}

注意ParquetRecordReader的第二個引數filter, 就是parquet對計算引擎提供的介面,用於傳入過濾條件 。而ParquetRecordReader從parquet檔案中讀取資料後,會首先使用filter過濾掉不滿足的record,然後再交給計算引擎。

相比於Spark,Iceberg做得更多一些。 Iceberg會在兩個層面實現謂詞下推:

1. 在snapshot層面,過濾掉不滿足條件的data file

2. 在data file層面,過濾掉不滿足條件的資料

其中第一點是Iceberg特有的,也是利用了manifest檔案裡儲存的元資料。

Iceberg在manifest檔案裡記錄了每個欄位的上界和下界。 所以在planning階段,Iceberg就可以利用這個資訊,過濾掉不滿足條件的檔案,進一步減少檔案的掃描量。

{
...
"data_file": {
"file_path": ".../table/data/id_trunc=0/00000-0-1c1865ee-a812-465c-87cb-7588478c2d8f-00001.parquet",
...
"lower_bounds": {
"array": [
{
"key": 1,
"value": 1
},
{
"key": 2,
"value": "a"
}
]
},
"upper_bounds": {
"array": [
{
"key": 1,
"value": 2
},
{
"key": 2,
"value": "b"
}
]
},
...
}
}

這個檔案包含兩條資料,(1, “a”)和(2, “b”)。可以看到上界和下界分別是1和2,a和b

第二點則和Spark類似,也是在讀取檔案時,過濾掉不滿足條件的資料。不過和Spark不同的是,Iceberg作為儲存層的系統,使用的是parquet更偏底層的ParquetFileReader介面,自己實現了過濾邏輯。和Spark的ParquetRecordReader有什麼不同呢?Iceberg的這種實現方式可以直接跳過整個row group,更進一步地減少io量,不過礙於篇幅,細節我就不展開講了。

以上就是Iceberg在優化查詢效能方面所實現的優化機制,本質都是為了減少資料查詢量。可以看到manifest檔案在Iceberg的優化邏輯中起到非常關鍵的作用,正是因為Iceberg利用了雲原生資料倉庫的檔案大多不可變的特性,收集了非常多關於資料分佈的元資料的關係。相信未來儲存層會在這方面有更多的發展,然後和計算引擎的結合更為緊密,從而進一步提高查詢的效能。

最後你或許會問,既然Iceberg有這些query優化機制,那Hudi有同樣的功能嗎?就我從Hudi的原始碼看來, 現階段Hudi實現了一半 。Hudi實現了分割槽剪枝功能,但是謂詞下推功能目前似乎還沒有實現。

總結下這篇文章知識點:

  1. Iceberg對減少資料查詢量提供了兩種優化機制:分割槽剪枝和謂詞下推。

  2. Iceberg收集了關於資料分佈的元資料,並利用這些元資料實現更高效的資料裁剪,減少了資料的查詢量。