B站基於Iceberg+Alluxio助力湖倉一體項目落地實踐

語言: CN / TW / HK

B站基於Iceberg+Alluxio助力湖倉一體項目落地實踐

本期分享的題目是B站基於Iceberg + Alluxio 助力湖倉一體項目落地實踐,內容包含諸多技術細節,主要從以下4個維度進行分享:

摘要

01. B站湖倉一體項目的背景介紹

當前B站每天會有pb級的數據進入Hadoop,從而衍生出大量的數據分析、BI報表、數據探索等需求。當前B站急需一套技術架構,在擁有數據湖靈活性的同時又具備數倉的高效性,在這樣的背景之下開始探索基於 Iceberg 推動從數據湖架構到湖倉一體架構的演進。

02.B站基於 Iceberg 湖倉一體——系列優化與落地實踐

- Z-Order對數據進行組織排序;

- Hilbert Curve Order對數據進行組織排序;

>>針對Z-order的缺陷,引入了BloomFilter索引;

>>針對bloomfilter的缺陷,引入了Bitmap索引;

>>針對RangeEncoded的缺陷,引入了BIT-SLICED 編碼。

03. Alluxio的實踐

1. 引入Alluxio的背景:隨着我們在Iceberg的一些改造,元數據文件也逐漸增大。同時為了降低新增索引文件的讀取開銷以及Hadoop集羣抖動等因素對文件讀取的影響,我們引入了Alluxio。
2. Alluxio上線後的收益:在引入Alluxio之後,訪問metastore抖動問題基本上沒有出現過了,這就保證了查詢的穩定。通過歷史查詢耗時的分析,發現Alluxio對元數據訪問整體有不錯的性能提升。

04. 未來規劃

第一點:在Iceberg上希望對預計算進行支持。
第二點:星型模型的優化。
第三點:用Alluxio進行熱點數據的緩存。
第四點:智能化的數據優化

以上僅為大咖演講概覽,完整內容可點擊觀看


附件:大咖分享文字版完整內容可見下文

01. B站湖倉一體項目的背景介紹

當前B站每天會有pb級的數據進入Hadoop,從而衍生出大量的數據分析、BI報表、數據探索等需求。傳統的SQL on Hadoop 不管是Hive、Spark還是 Presto 都很難滿足業務的性能需求。如果要出倉到像ES、Hbase、MySQL、Clickhouse等,不僅會增加額外的數據開發、數據宂餘、數據服務開發等成本,數據的穩定性和可靠性也會隨之降低。SQL on Hadoop 本質上是一套數據湖方案,它不僅支持海量數據存儲,擁有開放的存儲格式,支持開放的數據處理引擎和湖內的ETL流轉等。當前我們急需一套技術架構,在擁有數據湖靈活性的同時又具備數倉的高效性,所以我們開始探索基於 Iceberg 推動從數據湖架構到湖倉一體架構的演進。

02. 基於 Iceberg 湖倉一體的優化與落地實踐

下面是湖倉一體項目架構圖,大體分為三部分:

最左邊的部分是數據的攝入,主要分兩塊:一塊是實時數據的攝入,主要是通過Flink消費Kafka數據,然後落到Iceberg表裏面。第二塊是Spark通過ETL把數據落到Iceberg表裏。

第二部分是存儲的優化,我們有一個內部服務Magnus,會起一些相應的Spark job針對iceberg表進行一些存儲優化。

第三部分是交互式分析。目前我們交互式分析引擎主要是用的Trino,然後藉助Alluxio作為一個緩存加速。

 

首先跟大家簡單介紹一下Iceberg。

“Apache Iceberg is an open table format for hug analytic datasets.”這是在Iceberg官網上的一句話,Iceberg是針對海量數據的開放表格式。我理解本質上 Iceberg其實是在計算引擎與底層的存儲之間維護了針對表級的一套文件粒度的元數據管理API。

右圖是Iceberg的一個元數據架構圖,我們可以看到架構圖分為三層:

第一層 catalog,目前實現主要分為兩種是Iceberg——一種是Hadoop Catalog,一種是Hive Catalog,目前我們使用的是Hive Catalog。也就是説我們會把一些table的scheme信息保存在Hive Metastore裏面,Metadata location地址也會保存在這個Hive Metastore裏面。

第二層是元數據層。元數據層主要分為三類文件:第一類是metadata file(這是一個json文件),第二類manifest list文件,第三類是manifest file文件。後兩種是avro格式文件,metadata file文件會保存表的schema信息和分區信息,它會包含一個或多個的快照引用,快照引用會指向manifest list文件。manifest list文件會包含一個或多個的manifest file文件,也就是説每一個manifest file文件在manifest list裏面是一條一條的記錄,這條記錄會保存這個manifest file的文件路徑,也會保存這個manifest file對應的數據文件的分區範圍,也就是分區的一個Min/Max值。當然,它還會保存一些其他的元信息。manifest file文件會包含一個或多個的數據文件(data file),每個數據文件在manifest file裏面是一條或多條的記錄,每條記錄都會保存data file所在的文件地址和data file本身的大小和記錄數,最重要的是它會保存data file的每一個字段的最大、最小值。

Iceberg怎樣提升查詢性能?

也即如何通過在manifest file裏面引用的data file的每個字段的Min/Max值來加速查詢。

假設我們是通過計算引擎Trino,在SQL的plan階段就可以利用元信息的每個字段的Min/Max值進行高效過濾,把不在這個查詢範圍的數據文件直接過濾掉,不用等到實際執行階段再去做實際數據的過濾,這樣對查詢性能會有一個極大的提升。這裏有個前提條件,即通過字段的Min/Max值過濾需要過濾字段在文件間是有序的。

怎麼做到文件間有序?

下面我介紹一種最常見的做法——通過一種線性排序對數據進行組織優化:我們可以通過Spark的cluster by對錶的某一個或多個字段進行分區類的數據全排序。這種方式的優點是不需要額外的存儲宂餘,就可以利用排序字段在Iceberg元數據文件級別上的Min/Max值進行高效的過濾。不足是通常只有排序的一個字段會有比較好的過濾效果,通過其他字段過濾往往效果不佳。

下面介紹另外一種對數據組織排序的方式:Z-order,這是最近比較流行的一種排序方式。

Z-order是在圖像處理以及數倉中使用的一種排序方式。Z-order的曲線可以是一條無限長的一維曲線,穿過任意維度的所有空間。左邊有一個二維曲線圖,右邊是一個三維的,三維可能比較抽象,我們後面可能主要是針對二維進行展開,三維可以進行一些類比。對於每一條數據的多個排序字段可以看作是數據的多個維度,多維數據本身是沒有天然順序的。但是Z-order通過一定的規則將多維數據映射成一維數據,然後構建Z-value。

上圖是二維圖,包含兩個字段x跟y。x、y中間生成的這個值,我們就把它叫做Z-value,它的生成規則是根據這些排序字段按照交錯的比特位去生成的。紅色表示y,藍色表示x,也就是y取一位x取一位,這樣構成的Z-value值就變成一維的值了。

為什麼是叫Z-order排序?

因為它構成曲線是z字形的。z字形的曲線有什麼特點?在z字形的曲線上,根據Z-order生成的一維z-value是有序的。第一個點是六個零對應的二進制表示的是0。第二個點是5個零加個1,它對應的是1,後面依次對應的是234567…。也就是説沿着z字形的曲線是有序的。

Z-order的映射規則保證了按照一維數據排序後的數據可以同時根據多個字段進行聚集,也就是説這個曲線是有序的。假如把這份數據做一個切割,比如從中間把這個數據切成四份。其實它根據Z-value在每一個文件裏面的Z-order是有序的。同時對於我們x跟y來説它其實也是有聚集的。像我們第一個文件x它只包含0~3的範圍,y也只包含0~3的範圍。

下面將Z-order跟線性排序做一個比較。還是剛才那個例子,x的取值範圍是0~7,y的取值範圍也是0~7。假如我們用線性排序,根據x、y做一個range的分區。假如把它也是分到四個文件裏面。最後分到文件的效果,就應該是下面這個樣子。也就是我們看一下在文件一里面x的取值範圍是0~1。文件二里面x的取值範圍2~3,文件三是4~5,文件四是6~7,y的取值範圍的話都是0~8。因為這是一個線性的排序。假如我們現在通過x=5去過濾,我們可以看到這邊只需要讀取文件三就行了。因為文件三的x的取值範圍是4~5,也就是文件一、文件二、文件四都不需要讀取,可以直接skip掉。假如我們現在想通過y=2去過濾,因為y的取值不管在哪個文件裏面都是0~8,所以這四個文件都需要讀取。在這種情況下過濾是沒有任何過濾效果的。

我們也是根據x、y 進行Z-order的排序。剛才提到的切割,切割之後分在四個文件裏面,也在第一個文件裏的話,x取值就是0~3,y的取值也是0~3,這是我們第一個文件。第二個文件x取值4~7,y取值也是0~3,對應的第二個文件。第三個文件和第四個文件也是類似的。同樣的我們用x=5去過濾的話,可以看到在第二個文件跟第四個文件裏面,因為它的取值範圍是4~7,包含x=5這個範圍,所以兩個文件需要讀。在文件一和文件三中x取值範圍是0~3,所以我們可以直接skip掉,不用去讀這個文件。相應地,如果我們用y=2去過濾,可以只用讀文件一和文件二。因為y的取值範圍是0~3,所以這兩個文件是需要讀的。相應的文件三、文件四就可以直接跳過了。

總結

通過Z-order排序之後,不管是用x還是y過濾都能過濾50%的數據。實際應用場景中,數據分區裏面數據文件如果切分得更多,過濾效果也會更好,會超過50%。

Spark 本身支持 range partition,也就是支持範圍分區。範圍分區主要分為兩個階段:第一階段是對排序字段進行採樣,獲取指定分區(spark.sql.shuffle.partitions)個數的採樣點(默認 200),並將採樣值按大小依次排列,採樣會盡量使得數據均勻分佈在每個區間。第二階段是實際數據讀取、shuffle、寫入階段,shuffle 前,每條記錄的排序字段跟採樣點的值進行比較,把數據分別落入 0~199 個對應區間,從而實現了排序字段在文件間的有序。

Z-Order也是沿用了 range partition 的思想,在採樣階段,分別計算需要排序的多個字段的採樣點,並將採樣數據依次跟排序字段的採樣點進行比較,獲取對應字段分區下標值,多個字段下標值即可生成 z-value,然後通過所有 z-value 計算指定分區個數的採樣點。shuffle 前,對每條記錄的多個排序字段分別獲取採樣點的下標,生成 z-value,最後是將 z-value 跟分區的採樣點進行比較,落入對應分區,即可實現 Z-Order 排序。

右邊的這個曲線是HIlbert Curve Order,也就是希爾伯特曲線。

為什麼要介紹這種排序?是因為Z-order會有一個小小的缺陷,它的鄰近性比較差。

鄰近性是什麼意思?我們每一個數據點連線的跨度比較長,也就是鄰近性會比較差。

具體在案例中是怎麼體現呢?我們以剛才那個x取值是0~7的範圍為例。剛才把這邊分成四個文件,理想的情況下是所有的數據都是均勻地分佈在每個文件,但實際應用場景中可能數據切分得並不是那麼均勻。假如我們這邊實際是X取值可能是等於7的一個取值,它劃分到下面這個文件裏面來了。原本下面這個文件X取值的話可能是0~3。因為這個x=7的取值劃分到這個下面文件,導致我們下面這個文件的X取值範圍就變成0~7了。假如通過X在0~7的這個範圍裏面,不管用哪個值去過濾,這個文件都不能被過濾掉,也就是使得過濾效果會有一定的影響。

希爾伯特曲線的每一個點不會有很大的跨度,也就是避免了Z-order鄰近性比較差的問題。

總結

Z-order跟Hilbert曲線的優缺點

優點首先是無需額外的存儲宂餘。第二點是支持對多個字段進行組織排序,多個字段分別過濾時都會有比較好的文件級別的過濾效果。通常排序字段是2~4個時可能過濾效果會比較好。當排序字段超過4個的時候,過濾效果可能會變差,排序字段越多,效果可能會變得越差。另外,過濾的本質是利用字段Min/Max值去做過濾,它並不能做到比較準確的判斷。

針對Z-order的這個缺陷,我們引入了BloomFilter索引。

首先介紹一下BloomFilter是什麼?BloomFilter是一個很長的二進制向量,元素可以通過多個Hash函數計算後,將多個整形結果在對應的向量比特位上置為一,可以判斷某個元素是否在這個集合裏面。BloomFilter的優點是空間效率和查詢時間都非常高效,非常適用於檢索一個元素是否在一個集合裏面。它的缺點是首先會存在一定的誤判率,也就是説實際某個元素不在這個集合裏面,但可能會被誤判為存在這個集合。另外,BloomFilter只適合等值判斷,比如等於in或not null的判斷,對於 >、>=、<、<= 都是不支持的。另外還有一點不足是不能做準確的邏輯運算,比如過濾條件是a=1,並且b=2,這種情況下沒法過濾。為什麼呢?假如這個文件既存在a=1的記錄,又存在b=2的記錄。但是並不存在在同一條記錄上面a=1並且b=2。通過不同的BloomFilter沒法判斷。

由於bloomfilter的一些缺陷,我們引入了Bitmap索引。Bitmap是將一組正整型數據映射到對應的比特位,相對於BloomFilter,它是不存在哈希衝突的。

下面介紹Bitmap索引的幾個實現,首先介紹等值編碼的實現。我們假設有一個數據文件,有三個字段。我們現在想根據訂單的總價格這個字段,構建一個Bitmap。Bitmap構建的結果是下圖的右側所示。對應的橫軸0~8,也就是這個字段在文件裏面的一個下標。縱軸對應的就是字段的各個基數值按從小到大的排序(我們可以把這些基數值存在一個有序數組裏面)。對應比特為一的代表這個數據在這個文件裏面的位置,比如這個2對應這個文件裏面的第3行,所以它的下標也是在2的那個位置設置為1,其他的也是類似的。

假如我們現在想去獲得Bitmap等於18的值,我們怎麼去獲得?

剛才提到我們可以在有序數組裏面首先去判斷這個18存不存在,存在的話我們只要把這個18取出來就行了。假如我們想去做一個範圍過濾,比如算價格小於200這個結果,如果是根據等值編碼算的話,其實我們需要是把所有的bitmap都取出來,然後做一個bitmapOr的運算,也就是把所有結果做一個並集。這種返回的結果就是小於200的結果。如果基數很大,比如百萬級甚至更大,這個計算量就很大,成本也就很高了。

針對等值編碼缺陷,我們可以引入一個叫Range Encoded的編碼。左邊是剛等值編碼,右邊是RangeEncoded的編碼,它的編碼規則是什麼?它是所有大於該取值的值,在對應行號的比特位置為1,比如我們這個2的位置,這邊比2大,下面的這些18、20,我們把這個下面全部都置為1,最終生成的結果是如右圖所示。

如果根據RangeEncoded編碼再去計算,假如我們現在想去算一個取值等於20,只需要把20對應的bitmap以及比20小的bitmap取出來,然後做andNot操作。對於小於或小於等於運算,比如我們想算小於19的,只要把就是比19小最接近19的那個bitmap取出來,對應的結果就是bitmap的結果了。對於大於或者大於等於這些也是類似的(可以注意一下最後一行都是為1的,也就是notnull取值的bitmap,這裏對應的其實訂單價格全部非空,所以取值全為1)。這個優點是最多值取兩個bitmap就可以計算任意的等值,大於/大於等於、小於/小於等於的過濾條件,和字段基數大小沒有任何關係。它的不足是構建bitmap的時候仍然要生成對應基數個數的bitmap,存儲的索引文件會過大。

針對RangeEncoded的缺陷,我們又引入了BIT-SLICED 編碼。

假設 lo_ordtotalprice 取值區間為 0-255 的連續值,我們就需要256 個 Bitmap 來表示每個取值。如果用十進制 slice來表示的話,需要三個 Slice 位來表示 0~ 255,取值20的行號映射 Bitmap 為 100000000,我們把它對應的每個slice 位 (個位) - 0(20的個位), (十位) - 2(20的十位), (百位) – 0 ((20的十位))都置為100000000。如果我們希望獲取等於 20 的 bitmap,只需將三個位置的 bitmap 取出做and 運算,全為 0,表示不存在,否則結果即為 20 對應的Bitmap。從這裏可以看到,只需 3個(slices)* 10(0-9) = 30 個 bitmap 表示 0~255 的取值。

將BIT-SLICED ENCODED BITMAP與RANGEENCODED進行結合的話,BIT-SLICEDENCODE減少了BITMAP 存儲的個數,RANGEENCODE減少了計算需要讀取的BITMAP個數(也就是最多隻需要讀取2 個BITMAP就能完成計算)(存儲所需的BITMAP個數:如果是0~255的連續值,只需28 (10 * 3 -3(個重複)+ 1) 個BITMAP)。更進一步的,如果我們用二進制切片的話,按照基數取值範圍(0-255),總共需要9 (比特位需要用8個slice位,每個slice都是0/1,最後一個是全1,只需要1個表示就可以:8*2 –8 + 1)個bitmap,如果基數取值是在int表示的範圍內,最多也不超過32個bitmap就可以存儲整個的int的取值範圍了。

下面介紹一下bitmap最終的一個實現,總體是通過BIT-SLICED ENCODED 與RANGEENCODED結合並使用二進制切片。

實現的過程是:對字段A構建bitmap索引時,先讀取對應數據文件,依次讀取每一行,將A的值保存在以A字段作為key,A的行號構成的List作為值的有序MAP中。將MAP的key作為字典(字典是有序的,就可以進行二分查找)以及每個List經過bit-sliced編碼之後的bitmap都保存在索引文件中。Bitmap對應的取值是字典的下標,即從0開始到最大基數的連續取值,所需bitmap個數log2(基數),由於保存了字典信息,bitmap索引可以支持各種非嵌套數據類型。比如正常bitmap可能只支持int類型。這樣我們就可以支持像String或者 Double等類型。

總結:Bitmap的優缺點

優點:不僅支持等值過濾,>、>=、<、<= 等運算都支持,也不存在誤判;另外能準確作邏輯運算,比如 A = 1 and B = 2, 我們只需要把A = 1對應的bitmap和B = 2對應的bitmap取出來做一個and操作,然後再去計算這個bitmap的基數。如果這個bitmap的基數大於0的話,就説明這樣一條記錄是在這個文件裏面的。

不足:Bitmap 索引通常會比 BloomFilter 大不少,會有一定的存儲和讀取的開銷。

下面看一下我們在Iceberg上經過一系列的優化之後做的一個SSB的測試。SSB有13個query,然後我們是加了TPC-H的q5.1、q5.2,總共五個測試結果。主要分為四類:

第一類是basic,basic是指我們把數據導入之後沒經過任何優化;

第二類是z-order+min/max;

第三類是z-order+bloomfilter;

第四類是z-order+bitmap索引。

從這個圖裏面可以看出,就在z-order+min/max不能達到比較好過濾效果的時候,用z-order +bitmap可以做到一個比較好的補充。然後從整個查詢時間跟讀取文件的數量的測試結果來看,它總體的查詢時間有1~10倍的性能提升,掃描文件數有1~400倍的減少。這對應的是我們線上的一個實際的例子,也是一個百億數據的查詢。最開始查詢耗時是13s多,讀取了一百億的數據。經過我們的優化,結合z-order與bitmap索引,最終整個查詢的耗時只花了一點幾秒,實際數據讀取量只讀取了三十幾萬條數據。最後看一下CPU耗時,之前CPU耗時花費了17 min+,後面CPU耗時只花費了一點幾秒,大大節省了計算資源。

03. Alluxio的實踐

1. Alluxioa引入的背景

隨着我們在Iceberg的一些改造,元數據文件也逐漸增大。同時為了降低新增索引文件的一個讀取開銷以及Hadoop集羣抖動等因素對文件讀取的一個影響,我們引入了Alluxio。

目前,Alluxio 主要用來存儲 Iceberg的元數據,即其自身的 metadata,以及我們新增的索引文件數據和目前我們正在做的 cube 功能,cube 文件也會保存在 Alluxio 中。我們只存儲 Iceberg 元數據的考量:

第一個原因是我們早期的業務量不大,用了獨立的Hadoop 集羣用來保存Iceberg表的數據。在大多數情況下我們能夠保證集羣的穩定,然後數據讀取基本也能滿足業務的性能需求。

第二點是因為Iceberg元數據有版本的概念,可以直接通過元數據保存的文件路徑去讀取元數據,不需要擔心元數據過期導致元數據讀取不一致的問題。在我們業務開展前期的使用Alluxio成本就大大降低了,我們不用擔心讀取這個文件它可能會失效。

這邊是我們在引入Alluxio之前做了一個基準測試,主要是分四種情況:

第一種情況是在引入之前;

第二種是引入Alluxio之後,引入之後第一次讀取相當於是沒有命中緩存;

第三種是讀取遠程worker節點的性能;

第四個就是讀取本地worker的一個性能。

我們得到的以下結果:在引入Alluxio之後,我們第一次讀取開銷可能會有10%~25%的性能損失,這個隨着文件增大會減小。緩存生效之後,去讀取遠程worker文件,有1.5倍到2倍的這樣一個性能提升。讀取緩存在本地worker文件,有5~10倍的性能提升。文件越大,性能提升也會越大。

2. Alluxio上線後的收益

在我們上線之前,遇到過一個問題:我們通過Trino去訪問Iceberg元數據,它一個偶發的抖動會導致性能急劇下降。在我們引入Alluxio之後,訪問metastore抖動問題基本上沒有出現過了,這就保證了查詢的穩定。目前我們單個元數據文件也並不是很大,都是kb級到幾十M不等。我們通過歷史查詢耗時的分析,發現Alluxio對元數據訪問整體有不錯的性能提升。

04. 未來規劃

首先我們在Iceberg上希望對預計算進行支持。預計算最主要的場景是針對一些聚合查詢或者説是多表關聯的一些查詢。我們希望對一些高頻的聚合查詢進行預先構建cube,然後通過計算引擎直接把查詢下推至cube裏面,通過cube去響應。

第二點是星型模型的優化。星型模型主要是事實表跟一個或多個維表join,會通過維表的多個關聯字段進行過濾。這種情況我們考慮把維表的過濾字段作為虛擬列保存在事實表裏面,然後直接下推到事實表。這樣就可以起到一個比較好的過濾效果。

第三點就是用Alluxio進行熱點數據的緩存,最主要兩點考量。第一點是為了保證線上的SLA,第二點是想加速熱點數據訪問的一個性能。

最後是智能化的數據優化。主要是通過分析歷史的查詢,自動去優化數據的一些排序、索引等信息。

分享嘉賓

向阿鯤—嗶哩嗶哩OLAP平台資深開發工程師

負責B站OLAP平台,湖倉一體方向:Iceberg/Trino 內核研發、優化探索實踐,智能化管理平台設計、開發,以及業務接入支持、優化等相關工作。

想要獲取更多有趣有料的【活動信息】【技術文章】【大咖觀點】,請關注[Alluxio智庫]