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智庫]