Apache Iceberg 中引入索引提升查詢(xún)性能
Apache Iceberg 是一種開(kāi)源數(shù)據(jù) Lakehouse 表格式,提供強(qiáng)大的功能和開(kāi)放的生態(tài)系統(tǒng),如:Time travel,ACID 事務(wù),partition evolution,schema evolution 等功能。
本文將討論火山引擎EMR團(tuán)隊(duì)針對(duì) Iceberg 組件的優(yōu)化思路,通過(guò)引入索引來(lái)提高查詢(xún)性能。
采用 Iceberg 構(gòu)建數(shù)據(jù)湖倉(cāng)
火山引擎 E-MapReduce(簡(jiǎn)稱(chēng) EMR)是火山引擎數(shù)智平臺(tái)(VeDI)旗下的云原生開(kāi)源大數(shù)據(jù)平臺(tái)產(chǎn)品, 提供了企業(yè)級(jí)的 Hadoop、Spark、Flink、Hive、Presto、Kafka、StarRocks、Doris、Hudi、Iceberg 等大數(shù)據(jù)生態(tài)組件,100% 開(kāi)源兼容,可以幫助企業(yè)快速構(gòu)建企業(yè)級(jí)大數(shù)據(jù)平臺(tái),降低運(yùn)維門(mén)檻。秉承業(yè)界領(lǐng)先的 EMR Stateless 理念,火山引擎 EMR 可以實(shí)現(xiàn)集群級(jí)別的彈性伸縮,即無(wú)業(yè)務(wù)需求時(shí)釋放集群,有業(yè)務(wù)需求時(shí)再拉起集群,配合智能化的冷熱數(shù)據(jù)分層存儲(chǔ)能力,助力企業(yè)在大數(shù)據(jù)基建領(lǐng)域進(jìn)一步降本提效。
基于火山引擎 EMR 產(chǎn)品,可以構(gòu)建數(shù)據(jù)湖倉(cāng)、近實(shí)時(shí)數(shù)倉(cāng)、實(shí)時(shí)數(shù)倉(cāng)等場(chǎng)景。例如,使用 Iceberg 構(gòu)建數(shù)據(jù)湖倉(cāng),從 ODS 到 DWD 等不同的分層進(jìn)行建模,將數(shù)據(jù) HFDS 或 TOS(火山引擎對(duì)象存儲(chǔ)產(chǎn)品)上,然后采用 Trino 或者 Spark 去做分析。
如何加速查詢(xún)性能,使其盡可能接近專(zhuān)門(mén)的分布式數(shù)倉(cāng)(如 ClickHouse 等),是需要思考和探究的問(wèn)題。
索引是業(yè)界常用的提高查詢(xún)性能的手段之一,針對(duì) Iceberg 我們也采用了增加索引的方式。對(duì)常用的列字段構(gòu)建 Index,在進(jìn)行 table scan 時(shí)利用 Index 只返回匹配的數(shù)據(jù),降低匹配數(shù)據(jù)量,從而大大提高查詢(xún)性能。
Iceberg 介紹
介紹 Iceberg Index 功能之前,我們先簡(jiǎn)單介紹下 Iceberg 的架構(gòu)。Iceberg 具有分層的元數(shù)據(jù)架構(gòu),如下如所示。
Spark、Presto、Flink 等多種引擎讀取 Iceberg 的數(shù)據(jù),就是利用分層的元數(shù)據(jù)找到 data file 列表。例如,Spark 引擎解析 SQL 語(yǔ)句,然后調(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í)別的初步過(guò)濾,把不符合條件的 data file 過(guò)濾掉,進(jìn)而減少一部分?jǐn)?shù)據(jù)的讀取。
實(shí)現(xiàn)索引的必要性
既然 Iceberg 已經(jīng)提供 data file 級(jí)別的過(guò)濾。為什么我們還需要引入索引呢?以下面例子進(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 中沒(méi)有滿(mǎn)足條件的數(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 都滿(mǎn)足條件。然而,仔細(xì)分析 data file 1 和 data file 2
的數(shù)據(jù),并不存在符合條件的數(shù)據(jù),因此 min-max 過(guò)濾效果不太理想。所以通過(guò)引入合適的索引功能,可以提高 data skipping
的概率,提高查詢(xún)性能。
1. 首先探究索引類(lèi)型
索引類(lèi)型有多種,如 BloomFilter、Ribbon Filter、Dictionary Index、BitMap 等。為了滿(mǎn)足多維分析場(chǎng)景,我們選擇了[Range-Encoded BitMap]https://www.featurebase.com/blog/range-encoded-bitmaps ( Base-2, Bit-sliced Index),可適用于高基數(shù)場(chǎng)景,滿(mǎn)足=、<、>、IN、BETWEEN 等操作的多維分析。
例如,對(duì)上面的
name 和 age 兩列分別計(jì)算索引信息。由于 name 屬于字符串類(lèi)型,需要先進(jìn)行字典編碼再進(jìn)行計(jì)算索引信息。采用
Range-Encoded 技術(shù),根據(jù)數(shù)據(jù)的二進(jìn)制相關(guān)信息以及對(duì)應(yīng)的 pos 信息生成索引數(shù)據(jù)。利用索引數(shù)據(jù)分析得到,同時(shí)滿(mǎn)足name = 'LiLy'
和age > 30
的數(shù)據(jù)不在同一行,恰好可利用 Range-Encoded 的交并運(yùn)算將數(shù)據(jù)進(jìn)行過(guò)濾掉,因此 data file 1 不用參與計(jì)算。
也就是說(shuō),BitMap 的交并運(yùn)算可以更好地在復(fù)雜過(guò)濾條件的情況下過(guò)濾掉更多的數(shù)據(jù)文件。
2. 接下來(lái)探究索引的粒度。
Iceberg 提供的 min-max,也是一種文件級(jí)別的索引。文件級(jí)別的索引就是根據(jù) filter 條件過(guò)濾掉不符合條件的 data file。文件級(jí)別的索引可適用于多種文件類(lèi)型,但這種粒度比較粗,只要 data file 中有一條數(shù)據(jù)符合條件,該 data file 中的數(shù)據(jù)就會(huì)全部讀取出來(lái)參與計(jì)算,從而影響 SQL 的查詢(xún)性能。
對(duì)于 Parquet、ORC 的文件格式,提供有 file chunk 的概念(row group or stripe),我們完全可以按照 row group / stripe 粒度,對(duì)數(shù)據(jù)進(jìn)行過(guò)濾。(為了方便描述,我們將 row group 和 stripe 統(tǒng)稱(chēng) split。)
如:SQL語(yǔ)句:SELECT * FROM table WHERE col_1> v1 AND col_2 = v2
,其中對(duì) col_1 字段和 col_2 字段已構(gòu)建 Index 信息?,F(xiàn)在利用索引對(duì) SQL 語(yǔ)句作用。
SQL
語(yǔ)句解析后,將符合條件的 data file 列表進(jìn)行切分后,得到很多 split 的列表。利用索引,分析 split
中數(shù)據(jù)是否滿(mǎn)足條件,如果不滿(mǎn)足則跳過(guò)。如上圖 data file 列表切分后,得到數(shù)萬(wàn)級(jí)別數(shù)量的 split 列表。將索引數(shù)據(jù)作用在
split1,發(fā)現(xiàn) split1 中沒(méi)有同時(shí)col_1> v1 AND col_2 = v2
滿(mǎn)足條件的數(shù)據(jù),該 split1 中的數(shù)據(jù)就不會(huì)參與計(jì)算。最后處理后,只得到了少量的 split 列表,數(shù)據(jù)過(guò)濾度達(dá)到 10% 以上,查詢(xún)性能有明顯提升。
因此,采用 row group / stripe 級(jí)別的細(xì)粒度索引,可以過(guò)濾大部分?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 語(yǔ)句,在 Iceberg 的 Spark 模塊實(shí)現(xiàn) Spark 提供的索引接口即可。
- 構(gòu)建索引
我們采用異步構(gòu)建索引,不影響主線(xiàn)任務(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ì)生成類(lèi)似于下面內(nèi)容的文件。
索引帶來(lá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 語(yǔ)句分別在沒(méi)有 Index 和采用 Index 情況下的執(zhí)行時(shí)間。右圖展示采用 Index 后,7 條 SQL 語(yǔ)句讀數(shù)據(jù)的 split 數(shù)量。很明顯讀數(shù)據(jù)的 split 數(shù)量越少,Index 效果越好。最糟糕的情況,所有的 split 都參數(shù)計(jì)算,這時(shí)和沒(méi)有構(gòu)建索引的效果類(lèi)似。
- 采用 SSB 基準(zhǔn)測(cè)試
由于 SSB 提供的測(cè)試場(chǎng)景,和 Range-Encoded 有利的場(chǎng)景,不太匹配,所以 Index 的效果并沒(méi)有明顯的效果。但也不會(huì)比不采用 Index 的效果差。如下面左圖,分別是構(gòu)建索引前后,SQL 語(yǔ)句的執(zhí)行時(shí)間,構(gòu)建索引的優(yōu)勢(shì)并沒(méi)有體現(xiàn)出來(lái)。右圖中,可以看到所有的 split 都參與了計(jì)算。
總結(jié)
根據(jù)上面的介紹,這里總結(jié)下 Iceberg 中索引實(shí)現(xiàn)的一些特征:
- 細(xì)粒度索引級(jí)別:提供 RowGroup/Stripe 級(jí)別的索引,可以更加精確的定位數(shù)據(jù)的查詢(xún)范圍,減少不必要數(shù)據(jù)輸入,從而提高查詢(xún)性能;
- 索引作用于執(zhí)行端:查詢(xún)?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ǔ)空間小。滿(mǎn)足范圍查詢(xún)、等值查詢(xún)等場(chǎng)景。且范圍越小,收益效果越明顯。