Apache Iceberg 中引入索引提升查詢性能
Apache Iceberg 是一種開源數(shù)據(jù) Lakehouse 表格式,提供強(qiáng)大的功能和開放的生態(tài)系統(tǒng),如:Time travel,ACID 事務(wù),partition evolution,schema evolution 等功能。
本文將討論火山引擎EMR團(tuán)隊(duì)針對(duì) Iceberg 組件的優(yōu)化思路,通過引入索引來提高查詢性能。
采用 Iceberg 構(gòu)建數(shù)據(jù)湖倉
火山引擎 E-MapReduce(簡稱 EMR)是火山引擎數(shù)智平臺(tái)(VeDI)旗下的云原生開源大數(shù)據(jù)平臺(tái)產(chǎn)品, 提供了企業(yè)級(jí)的 Hadoop、Spark、Flink、Hive、Presto、Kafka、StarRocks、Doris、Hudi、Iceberg 等大數(shù)據(jù)生態(tài)組件,100% 開源兼容,可以幫助企業(yè)快速構(gòu)建企業(yè)級(jí)大數(shù)據(jù)平臺(tái),降低運(yùn)維門檻。秉承業(yè)界領(lǐng)先的 EMR Stateless 理念,火山引擎 EMR 可以實(shí)現(xiàn)集群級(jí)別的彈性伸縮,即無業(yè)務(wù)需求時(shí)釋放集群,有業(yè)務(wù)需求時(shí)再拉起集群,配合智能化的冷熱數(shù)據(jù)分層存儲(chǔ)能力,助力企業(yè)在大數(shù)據(jù)基建領(lǐng)域進(jìn)一步降本提效。
基于火山引擎 EMR 產(chǎn)品,可以構(gòu)建數(shù)據(jù)湖倉、近實(shí)時(shí)數(shù)倉、實(shí)時(shí)數(shù)倉等場(chǎng)景。例如,使用 Iceberg 構(gòu)建數(shù)據(jù)湖倉,從 ODS 到 DWD 等不同的分層進(jìn)行建模,將數(shù)據(jù) HFDS 或 TOS(火山引擎對(duì)象存儲(chǔ)產(chǎn)品)上,然后采用 Trino 或者 Spark 去做分析。
如何加速查詢性能,使其盡可能接近專門的分布式數(shù)倉(如 ClickHouse 等),是需要思考和探究的問題。
索引是業(yè)界常用的提高查詢性能的手段之一,針對(duì) Iceberg 我們也采用了增加索引的方式。對(duì)常用的列字段構(gòu)建 Index,在進(jìn)行 table scan 時(shí)利用 Index 只返回匹配的數(shù)據(jù),降低匹配數(shù)據(jù)量,從而大大提高查詢性能。
Iceberg 介紹
介紹 Iceberg Index 功能之前,我們先簡單介紹下 Iceberg 的架構(gòu)。Iceberg 具有分層的元數(shù)據(jù)架構(gòu),如下如所示。
Spark、Presto、Flink 等多種引擎讀取 Iceberg 的數(shù)據(jù),就是利用分層的元數(shù)據(jù)找到 data file 列表。例如,Spark 引擎解析 SQL 語句,然后調(diào)用 Iceberg 的接口,獲取 data file 并進(jìn)行 task 切分。
在 Manifest file 中記錄了 data file 中字段的最大值和最小值。
"data_file": {
"content": 0,
"file_path": "hdfs://emr-cluster/warehouse/hive/db.db/sample/data/ts_day=2020-12-31/category=diamond/00000-0-220aa9a6-4530-499f-9450-da946d667624-00001.parquet",
"file_format": "PARQUET",
......
"lower_bounds": {
"array": [{
"key": 1,
"value": "\u0006\u0000\u0000\u0000"
}, {
"key": 2,
"value": "diamond"
}, {
"key": 3,
"value": "\u0000\u0004ü??·\u0005\u0000"
}]
},
"upper_bounds": {
"array": [{
"key": 1,
"value": "\u0007\u0000\u0000\u0000"
}, {
"key": 2,
"value": "diamond"
}, {
"key": 3,
"value": "\u0000¨od?·\u0005\u0000"
}]
},
......
}
利用這些信息,可以進(jìn)行 data file 級(jí)別的初步過濾,把不符合條件的 data file 過濾掉,進(jìn)而減少一部分?jǐn)?shù)據(jù)的讀取。
實(shí)現(xiàn)索引的必要性
既然 Iceberg 已經(jīng)提供 data file 級(jí)別的過濾。為什么我們還需要引入索引呢?以下面例子進(jìn)行介紹,左邊兩個(gè)表格分別是 data file 文件里面的內(nèi)容,右邊表格是 data file 對(duì)應(yīng)的 manifest file。
針對(duì)SELECT * FROM table WHERE age > 50
,利用 min-max 統(tǒng)計(jì)信息,很容易發(fā)現(xiàn) data file 1 中沒有滿足條件的數(shù)據(jù),因此 data file 1 就不會(huì)參與計(jì)算。
但是針對(duì)多維分析,如name = 'LiLy' AND age > 30
,利用name
和age
的min-max的統(tǒng)計(jì)信息分別對(duì)條件name = 'LiLy'
和age > 30
進(jìn)行判斷,得到
data file 1 和 data file 2 都滿足條件。然而,仔細(xì)分析 data file 1 和 data file 2
的數(shù)據(jù),并不存在符合條件的數(shù)據(jù),因此 min-max 過濾效果不太理想。所以通過引入合適的索引功能,可以提高 data skipping
的概率,提高查詢性能。
1. 首先探究索引類型
索引類型有多種,如 BloomFilter、Ribbon Filter、Dictionary Index、BitMap 等。為了滿足多維分析場(chǎng)景,我們選擇了[Range-Encoded BitMap]https://www.featurebase.com/blog/range-encoded-bitmaps ( Base-2, Bit-sliced Index),可適用于高基數(shù)場(chǎng)景,滿足=、<、>、IN、BETWEEN 等操作的多維分析。
例如,對(duì)上面的
name 和 age 兩列分別計(jì)算索引信息。由于 name 屬于字符串類型,需要先進(jìn)行字典編碼再進(jìn)行計(jì)算索引信息。采用
Range-Encoded 技術(shù),根據(jù)數(shù)據(jù)的二進(jìn)制相關(guān)信息以及對(duì)應(yīng)的 pos 信息生成索引數(shù)據(jù)。利用索引數(shù)據(jù)分析得到,同時(shí)滿足name = 'LiLy'
和age > 30
的數(shù)據(jù)不在同一行,恰好可利用 Range-Encoded 的交并運(yùn)算將數(shù)據(jù)進(jìn)行過濾掉,因此 data file 1 不用參與計(jì)算。
也就是說,BitMap 的交并運(yùn)算可以更好地在復(fù)雜過濾條件的情況下過濾掉更多的數(shù)據(jù)文件。
2. 接下來探究索引的粒度。
Iceberg 提供的 min-max,也是一種文件級(jí)別的索引。文件級(jí)別的索引就是根據(jù) filter 條件過濾掉不符合條件的 data file。文件級(jí)別的索引可適用于多種文件類型,但這種粒度比較粗,只要 data file 中有一條數(shù)據(jù)符合條件,該 data file 中的數(shù)據(jù)就會(huì)全部讀取出來參與計(jì)算,從而影響 SQL 的查詢性能。
對(duì)于 Parquet、ORC 的文件格式,提供有 file chunk 的概念(row group or stripe),我們完全可以按照 row group / stripe 粒度,對(duì)數(shù)據(jù)進(jìn)行過濾。(為了方便描述,我們將 row group 和 stripe 統(tǒng)稱 split。)
如:SQL語句:SELECT * FROM table WHERE col_1> v1 AND col_2 = v2
,其中對(duì) col_1 字段和 col_2 字段已構(gòu)建 Index 信息?,F(xiàn)在利用索引對(duì) SQL 語句作用。
SQL
語句解析后,將符合條件的 data file 列表進(jìn)行切分后,得到很多 split 的列表。利用索引,分析 split
中數(shù)據(jù)是否滿足條件,如果不滿足則跳過。如上圖 data file 列表切分后,得到數(shù)萬級(jí)別數(shù)量的 split 列表。將索引數(shù)據(jù)作用在
split1,發(fā)現(xiàn) split1 中沒有同時(shí)col_1> v1 AND col_2 = v2
滿足條件的數(shù)據(jù),該 split1 中的數(shù)據(jù)就不會(huì)參與計(jì)算。最后處理后,只得到了少量的 split 列表,數(shù)據(jù)過濾度達(dá)到 10% 以上,查詢性能有明顯提升。
因此,采用 row group / stripe 級(jí)別的細(xì)粒度索引,可以過濾大部分?jǐn)?shù)據(jù)。
細(xì)粒度索引實(shí)現(xiàn)邏輯
Iceberg 元數(shù)據(jù)中 manifest file 中除了提供 min-max 等統(tǒng)計(jì)信息,還提供有 split 相關(guān)信息:"split_offsets":{"array":[4,...]}
,極大方便我們實(shí)現(xiàn) row group / stripe 級(jí)別的細(xì)粒度索引。
- 提供索引的構(gòu)建 API
Iceberg 中提供構(gòu)建索引的 API,引擎端調(diào)用該 API 即可實(shí)現(xiàn)索引構(gòu)建功能。對(duì)于 Spark 3.3 及以上版本,已經(jīng)提供有索引的 SQL 語句,在 Iceberg 的 Spark 模塊實(shí)現(xiàn) Spark 提供的索引接口即可。
- 構(gòu)建索引
我們采用異步構(gòu)建索引,不影響主線任務(wù)。也提供了增量構(gòu)建索引功能,只對(duì) append 數(shù)據(jù)進(jìn)行構(gòu)建索引。調(diào)用 TableScan 讀取數(shù)據(jù),按照 data file 的 split offset 切分?jǐn)?shù)據(jù),進(jìn)行構(gòu)建索引,并保存索引數(shù)據(jù)和對(duì)應(yīng)的元數(shù)據(jù)信息。為了避免出現(xiàn)小文件存在,我們會(huì)進(jìn)行索引數(shù)據(jù)合并。
- 索引文件存儲(chǔ)
索引文件格式采用[puffin]https://iceberg.apache.org/puffin-spec/格式,這是一種二進(jìn)制格式。 Magic Blob? Blob? ... Blob? Footer
在 Footer 中保存每個(gè) blob 的元數(shù)據(jù)信息。索引構(gòu)建成功后,會(huì)生成類似于下面內(nèi)容的文件。
索引帶來的收益
Range-Encoded BitMap 適用于多維分析場(chǎng)景,且 Ranger 范圍較小時(shí),效果非常明顯。下面我們基于 Spark 引擎性能測(cè)試。
- 構(gòu)造 1TB 的 SSB 測(cè)試數(shù)據(jù),分別在構(gòu)建 Index 前后,對(duì)以下用例進(jìn)行測(cè)試。
Q1: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice = 19665277
Q2: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice = 19665277 AND lo_revenue = 2141624
Q3: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice = 19665277 AND lo_revenue >=10304000
Q4: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice = 21877827 AND lo_revenue >= 83800 AND lo_revenue <= 103800
Q5: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice > 21877827 AND lo_revenue >= 83800 AND lo_revenue <= 93800
Q6: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice >= 93565 AND lo_ordtotalprice < 93909
Q7: SELECT count(*) FROM lineorder WHERE lo_ordtotalprice >= 93565 AND lo_ordtotalprice < 91003562 AND lo_revenue >=904300 AND lo_revenue <= 9904300
左圖展示了 7 條 SQL 語句分別在沒有 Index 和采用 Index 情況下的執(zhí)行時(shí)間。右圖展示采用 Index 后,7 條 SQL 語句讀數(shù)據(jù)的 split 數(shù)量。很明顯讀數(shù)據(jù)的 split 數(shù)量越少,Index 效果越好。最糟糕的情況,所有的 split 都參數(shù)計(jì)算,這時(shí)和沒有構(gòu)建索引的效果類似。
- 采用 SSB 基準(zhǔn)測(cè)試
由于 SSB 提供的測(cè)試場(chǎng)景,和 Range-Encoded 有利的場(chǎng)景,不太匹配,所以 Index 的效果并沒有明顯的效果。但也不會(huì)比不采用 Index 的效果差。如下面左圖,分別是構(gòu)建索引前后,SQL 語句的執(zhí)行時(shí)間,構(gòu)建索引的優(yōu)勢(shì)并沒有體現(xiàn)出來。右圖中,可以看到所有的 split 都參與了計(jì)算。
總結(jié)
根據(jù)上面的介紹,這里總結(jié)下 Iceberg 中索引實(shí)現(xiàn)的一些特征:
- 細(xì)粒度索引級(jí)別:提供 RowGroup/Stripe 級(jí)別的索引,可以更加精確的定位數(shù)據(jù)的查詢范圍,減少不必要數(shù)據(jù)輸入,從而提高查詢性能;
- 索引作用于執(zhí)行端:查詢?nèi)蝿?wù)被分配多個(gè)執(zhí)行端,每個(gè)執(zhí)行端只判斷該節(jié)點(diǎn)上的 RowGroup/Stripe 數(shù)據(jù)是否符合即可;
- 適配多種引擎:索引構(gòu)建后,可用于多種引擎;
- 提供異步構(gòu)建 Index,從而不影響主業(yè)務(wù)的進(jìn)行;
- 適用于高基數(shù) & 低基數(shù)場(chǎng)景,且占有存儲(chǔ)空間小。滿足范圍查詢、等值查詢等場(chǎng)景。且范圍越小,收益效果越明顯。