AnalyticDB實現和特點淺析
阿新 • • 發佈:2020-07-01
[toc]
本篇主要是根據AnalyticDB的論文,來討論AnalyticDB出現的背景,各個模組的設計,一些特性的解析。可能還會在一些點上還會穿插一些與當前業界開源實現的比對,希望能夠有一個更加深入的探討。OK,那我們開始吧。
# AnalyticDB介紹與背景
要說AnalyticDB,那起碼得知道它是幹什麼的。這裡直接貼下百度百科的介紹:
> AnalyticDB是阿里雲自主研發的一款實時分析資料庫,可以毫秒級針對千億級資料進行即時的多維分析透視。
簡單地說,就是實時OLAP型資料庫,它的對標產品是Apache Kylin,Apache Druid,Clickhouse這些。然後AnalyticDB的特點,**包括高併發實時攝入資料,相容Mysql協議,無需預計算即可有的極快響應時間,多種資料來源接入,大規模叢集管理等**。好吧,這幾個特點都很官方,不急,接下來會逐漸討論各個點。
然後介紹下AnalyticDB的背景。
首先先說說傳統的OLAP型資料倉庫,以往構建OLAP型資料倉庫通常都是採用離線模式,**即在晚上設定定時任務將前一天的資料同步到資料倉庫中,第二天資料分析師或報表工具就可以根據資料產出分析結果**。但這樣的問題是資料延遲太高了,商業瞬息萬變,可能今天線上出現了什麼訂單激增的情況,資料分析師卻要等明天才能進行分析,這誰受得了呀。所以近幾年的趨勢就是實時數倉,簡單說就是增加一個實時接收資料以供查詢的模組,這也叫做lambda架構。如圖,就是用一個Batch層和一個Real-time層共同提供查詢結果。[^1]
![lambda](https://tva1.sinaimg.cn/large/007S8ZIlgy1gf09l829e2j30nc09rgm4.jpg)
好像有點扯遠了,說回AnalyticDB,它就是在大背景下提出的,所以它的一個主要特性就是實時。然後由於它本身是雲原生的結構,也就是本身就是根植於阿里雲上面的,面向的客戶更加廣泛,所以是有通用性的要求的。比如傳統企業都是使用Mysql,Postgresql等關係型資料庫,這些企業也沒有人力去搭建和維護Hadoop和Kylin,Druid這些叢集。而Postgresql這類關係型資料庫可能會有對複雜結構對支援,比如json,vector等,所以AnalyticDB也提供了對這種複雜型別的支援。
**在效能方面,AnalyticDB維持所有列的索引,用以快速檢索資料。在儲存方面,使用行-列混合儲存,使得AnalyticDB可以同時對OLAP分析和行級查詢快速響應。然後為了高併發的查詢和高吞吐的寫入,又提出了讀,寫分離**。這幾個效能方面的特性,以及這些優化如何與實時查詢結合起來,在後面會詳細介紹。
總而言之,目前業界對海量資料的OLAP分析查詢方案無非兩種,通過預計算構建多維立方體,在查詢的時候直接讀取預計算好的資料做一些簡單的合併(因為分割槽儲存)然後返回給使用者。這種型別的代表是Kylin和Druid,它們的好處是比較簡單,OLAP分析查詢速度很快,缺點是不夠靈活,比如Kylin一點改動可能就要全部資料rebuild。
另一種是非預計算,充分利用各種資源(CPU,記憶體,列儲存,向量化執行),或是架構儘量優化(如AnalyticDB),來讓海量資料快速查詢得到結果。比較典型的代表是Clickhouse,查詢效能不賴,也相對靈活,但缺點是叢集資料量沒法拓展到很大。
這兩種方案都有辦法這實時這個點上進行拓展,只是實現思路也不大一樣。第一種是新增一個流式層,OLAP查詢的時候分別查詢歷史資料和流式層資料然後合併返回。第二種則是用微批方式倒入資料倉庫中實現流式查詢。
整體上,AnalyticDB更加偏向於第二種非預計算的方式實現,不過在很多設計上還考慮了行級查詢的實現和效能,所以要比Clickhouse這種要複雜一些。下面我們從幾個方面來討論它的實現。
# AnalyticDB詳細解析
AnalyticDB是一個能夠在PB資料集上高併發,低延遲,實時分析查詢,並且能夠在2000+雲伺服器上執行的OLAP資料庫。**在設計上有多個挑戰,需要兼顧多種查詢型別的效能要求。這裡的多種情境包括全表掃描分析,多表join的點查詢操作,多個列的多個篩選條件等等**,而這些操作又難以優化。
**第二個挑戰是要設計一個底層儲存,應對不同型別的查詢所需要的不同儲存結構。比如OLAP查詢需要列式儲存,而點查詢(行級查詢)需要行式儲存**。如何將這兩種儲存結構(列式,行式)結合起來以供不同查詢型別使用,同時還需要考慮到複雜型別,json,vector,text等,這也是一個難點。
**第三個是實時方面的,要如何做到每秒數百萬資料寫入吞吐的同時,呈現給使用者低延遲的查詢響應和資料延遲**。以前的做法將讀寫操作交由同一程序處理,但這樣一來讀寫操作的效能是互斥的,即高吞吐的寫入會影響到查詢效能和資料延遲[^3]。
為了解決上述挑戰,AnalyticDB引入以下特性:
- 高效的索引引擎
- 混合(列-行)儲存引擎
- 讀寫分離
- 高效檢索引擎
這些特性暫時就有個映像就好,後面會詳細對這部分闡述。
## 架構設計
要說這塊,我們先來看看整體的架構設計圖。
![AnalyticDB架構](https://img2020.cnblogs.com/blog/1011838/202006/1011838-20200616093334995-191715919.png)
前面與說到,AnalyticDB是雲原生的,AnalyticDB主要依賴於兩個外部結構,任務管理與排程元件Fuxi,和分散式儲存系統Pangu,而這幾個元件又都是基於阿里雲的Apsara(負責管理底層的物理主機並向上層提供服務)。
整體架構上看還是比較簡單的,主要就是對外提供JDBC/ODBC介面,內部由多個協調器(Coordinator)負責統一管理寫節點和讀節點(讀寫分離)。
- 協調器(Conrdinator):協調器負責接收JDBC/ODBC的讀寫請求,分發到不同的讀或寫節點。
- 寫節點(Write Node):負責處理寫請求並將資料寫入到Pangu中持久化。
- 寫節點(Read Node):負責處理查詢請求並返回。
在具體的處理流程中,Fuxi資源分配和非同步排程(類似yarn)。而資料計算則是使用管道的方式進行計算,如下圖:
![dataflow](https://img2020.cnblogs.com/blog/1011838/202006/1011838-20200616093406709-5562289.png)
圖中,資料按頁(Page)進行切分(Pangu的儲存特性),資料處理以管道的方式進行處理,且資料流轉的不同階段均在記憶體中執行。看這張圖其實有點像Spark的資料處理流程,當然AnalyticDB本身也是使用DAG模型進行資料處理。
不過按照論文中說的資料完全在記憶體中處理還是有點不現實,雖然這樣能極大提高處理的效率,但遇到資料量太大導致記憶體裝不下的情況,還是需要暫時落到磁碟上,就類似Spark有提供多種persist方案一樣。否則查詢的併發量勢必會受到一些影響,但這樣一來可能查詢響應又降低了,魚與熊掌不可兼得啊。
### 資料分割槽
在一開始建立表的時候,可以分配資料按照兩級分割槽進行儲存,這裡通過論文中的小例子闡述兩級分割槽的實現,如以下建表語句:
```
CREATE TABLE db_name.table_name (
id int,
city varchar,
dob date,
primary key (id)
)
PARTITION BY HASH KEY(id)
PARTITION NUM 50
SUBPARTITION BY LIST (dob)
SUBPARTITION OPTIONS (available_partition_num = 12);
```
**第一級索引,可以讓資料按照指定列進行hash分割槽以及指定分割槽數**,比如上述建表語句指定一級索引為id,分割槽數是50個。這樣可以讓資料根據id的hash值分佈到不同的50個分割槽中,這一列通常是使用高基數的列,諸如使用者Id等。
**第二級索引(SUBPARTITION,可選)可以針對某個列指定最大分割槽數,用來對資料保留和回收**,通常使用日期型別資料。比如如果指定按天進行分割槽,最大分割槽為12,那麼資料僅會保留12天內的資料。
### 讀寫分離和讀寫流程
大多數傳統的OLAP資料庫,都是使用一個執行緒負責處理使用者SQL的操作,不管是寫請求(Insert)還是讀請求(Select)。這在查詢和寫入的併發量都很高的情況下會出現資源爭用的情況,**針對這種情況AnalyticDB提出讀寫分離的解決方案。寫節點負責寫,讀節點負責讀資料**,兩種節點彼此分離,這樣就避免了高併發場景下讀寫資源互斥的情況。
寫節點主要是master和worker架構,由zookeeper進行協調管理。寫master節點負責分配一張表的分割槽給不同的寫worker節點。在一個SQL到達的時候,Coordinators會首先識別是讀還是寫SQL語句,若是寫,那麼會先發送到對應的寫worker節點,寫worker節點先將資料存到記憶體,定期以日誌的形式持久化到Pangu中形成Pangu日誌。當日志一定規模的時候,才會構建真正的資料和全量索引。
而對於讀節點,同樣每個節點會被實現分配不同的分割槽。功能上,**AnalyticDB有兩種讀模式,實時讀取(real-time read),寫入資料立即可讀,和延遲讀(boundedstaleness)**,即容忍一定時間的寫入資料延遲。延遲讀是預設採用的方式,雖然與一定資料延遲,但查詢響應更快,通常而言也足夠了。
而實時讀,那麼可以立即查詢到剛剛寫入的資料。之所以能這麼快,**其中一個原因是讀節點會直接從寫節點中獲取更新資料,也就是說寫節點在某種程度上說充當了快取**。其他的OLAP資料庫的做法通常有兩種,一種是用一個segment專門儲存實時資料,OLAP查詢的時候,會掃描實時segment和離線資料,合併後返回使用者,比如最新的kylin streaming就是這樣實現。一種是微批匯入資料到儲存引擎中,然後用以檢索,這樣的話寫入頻率(微批的間隔)會大大影響檢索效能。AnalyticDB的方式可以說是一種比較新穎的方式,藉助讀寫分離的架構和強大的索引能力(下面介紹),可以實現實時寫入且低延遲檢索。
不過其實這會面臨一個問題,資料同步的一致性問題(讀寫節點資料不一致),AnalyticDB是怎麼做的呢?這裡也不賣關子,主要是使用一個版本號來處理。Coordinators在分發寫請求給寫節點,寫節點更新後會返回更新後的分割槽版本號給Coordinators。Coordinators分發讀請求給讀節點時,也會帶上這一個分割槽版本號,讀節點就會與自己快取的版本號對比,發現自己小的話,就會去拉取寫節點的最新資料(寫節點有一定的快取功能)。
可以發現,通過讀寫分離的機制,以及預先分配好讀/寫節點的資料分割槽(hash),能提高資料處理的並行度,並且減少資料計算產生的資料傳輸網路開銷,比如join的shuffle操作就不需要進行大規模的資料再分割槽。而後有能夠將兩種請求相互解耦,每種操作關心自身就可以,方便以後的拓展。
OK,到這裡系統的架構,資料分割槽,讀寫流程就差不多說完了,接下來再討論下它的其他特性。
## 其他特性介紹
### 混合(列-行)儲存引擎
先說下背景,**OLAP查詢一般會有全表掃描操作,所以主流做法是使用列式儲存,因為列式儲存可以極大減少磁碟IO操作,提高提高全表掃描效能,但這種對點查詢(即行級別)查詢和更新等不甚友好**。而如Mysql這種行級儲存,點查詢方便,但OLAP操作又會又額外更多開銷(資料壓縮比低)。許多主流系統的做法是,基本摒棄另一種功能,比如Mysql不適合做大規模OLAP查詢,Kylin,或者說hive這種不支援行級別更新(特殊情況下可以,但支援不好),Druid則更加極致,直接就不存明細了。
而AnalyticDB卻通過行-列混合儲存結構,不僅兼顧OLAP分析和點查詢,還實現了複雜型別的儲存(json,vector)。不過在介紹它的行-列混合儲存結前,先來看看流行的列式儲存結構,然後再引出AnalyticDB的行列混合。
我們以開源的列式儲存結構Parquet為例來看列式儲存是怎麼儲存資料的。
![parquet](https://img2020.cnblogs.com/blog/1011838/202006/1011838-20200608221928473-1958508451.png)
Parquet本身是hadoop底層使用的儲存引擎,其強大毋庸置疑。所謂列式儲存,可以簡單理解成就是將一整列資料壓縮打包,然後按順序儲存。
儲存中有三級結構:
- 行組(Row Group):按照行將資料物理上劃分為多個單元,每一個行組包含一定的行數。一個行組包含這個行組對應的區間內的所有列的列塊。
- 列塊(Column Chunk):在一個行組中每一列儲存在一個列塊中,行組中的所有列連續的儲存在這個行組檔案中。不同的列塊可能使用不同的演算法進行壓縮。一個列塊由多個頁組成。
- 頁(Page):每一個列塊劃分為多個頁,頁是壓縮和編碼的單元,對資料模型來說頁是透明的。在同一個列塊的不同頁可能使用不同的編碼方式[^2]。
在最後是Footer模組,這裡儲存的是資料的元資料資訊,比如列名,列的型別。還有一些統計資訊,min,max,用以提升部分檢索的效率。同時Parquet也支援複雜型別的儲存,說簡單點就是將複雜型別Map,List等轉換成schema樹,把樹的葉子節點當做列資料儲存。
簡單瞭解了列式儲存,我們再來看AnalyticDB的行-列混合儲存。
![storage](https://img2020.cnblogs.com/blog/1011838/202006/1011838-20200616093435334-1365174636.png)
注意圖中左右兩部分分別是兩個檔案,左邊的是元資料檔案,儲存諸如欄位名,一些簡單的統計資訊幫助過濾,這個檔案比較小通常駐存在記憶體中。這部分內容和前面的Parquet的Footer儲存內容類似,這裡就不多介紹了。主要還是介紹下右邊部分,即資料儲存方式。
圖片右邊,資料以row group的形式儲存,**每個row group中儲存固定數量的行**。但是在row group中依舊採用列式儲存,即同一列的被儲存到一起,稱為Data bolck,所有的Data block按順序儲存(這點和列式儲存一樣)。而Data block是最小的操作單元(快取,讀取等)。注意這裡不像Parquet那樣,每一個Data block再分多個Page。
看上去,它的儲存結構和Parquet是類似的,只是沒有再將Data block劃分成多個Page,這裡論文沒和Parquet對比,也沒論述很清楚。不過最主要的區別應該就是這裡了。**為什麼Data block不需要再劃分?因為它沒那麼多資料呀,在Parquet裡,一個row group的資料量是GB級別的,所以一個row group中的列需要再劃分**。而AnalyticDB中,它的row group明顯是小數量級的,可能一個row group僅僅是MB級別的資料量。**這一點細微的差別,使AnalyticDB在點查詢的時候就可以直接幾MB內獲取一行全部資料,而Parquet可能需要在1G內才能獲取一行資料**。這也是為什麼AnalyticDB的叫做行-列混合儲存結構。
對比Parquet和AnalyticDB,它們的設計分歧可能是天生的,Hadoop適合儲存追加的資料,以及非結構化資料,它的場景更多是在大資料儲存和載入,所以不會考慮單行查詢的場景。而AnalyticDB要考慮各種檢索,所以設計上就會要差異。當然AnalyticDB這樣也不是沒有缺點,比如它在全表掃描效能會有所下降。
**說完AnalyticDB的儲存結構,再來說說AnalyticDB如何儲存複雜型別資料**。
複雜型別資料(json,vector)儲存有個難點,這種複雜型別的資料通常大小是不定的,而且往往會出乎意料的大。如果按照上面提到row group的方式,可能一個block entry會非常大,所以需要一種其他型別的儲存結構來儲存複雜型別資料。
具體的做法可以說借鑑了hadoop的儲存思路。既然複雜型別資料大小不一樣,可能大可能小,那就將資料統一用32KB大小的塊組織起來,稱為FBlock。一個複雜型別資料可能分散在多個FBlock中(超過32KB),多個FBlock按順序儲存。然後使用稀疏索引,方便快速查詢。這樣的設計無疑可以方便得將複雜資料進行儲存,同時通過稀疏索引又能在一定程度上保證檢索的速度。
**最後再說說如何支援update和delete操作**。
一般的列式儲存是不怎麼支援行級更新和刪除操作的,因為資料都是壓縮成二進位制進行儲存,如果支援行級更新,那並你需要先解壓縮,整塊資料,然後刪除資料,再壓縮儲存。要是併發量一上來那簡直是災難。
那hbase的底層是hadoop,它是怎樣實現更新和刪除的呢?這是因為hbase使用LSM-tree,我之前也過一篇介紹這個東西,也興趣可以看看[資料的儲存結構淺析LSM-Tree和B-tree](https://www.cnblogs.com/listenfwind/p/13046863.html)。粗略說就是將更新和刪除操作都按key-value的形式追加到檔案末尾,然後整個檔案定期去重,只保留最新的key的資料,舊的key資料就被刪除了。檢索的時候如果也多個key,只會認最新的那個key的資料。當然具體細節要複雜得多。
AnalyticDB也是類似的思想,不過做了一些改變。它使用一個儲存在記憶體中的bit-set結構,記錄更新和刪除的資料id以及對應版本號。同時使用copy-on-write(寫時複製)技術提供多版本支援。更新和刪除操作都會改變版本號,然後查詢的時候會提供一個版本號去查詢對應的更新和刪除資訊,然後在查詢結果中和結果進行合併。這樣就實現了更新和刪除操作。
稍稍總結下AnalyticDB的儲存結構,行-列混合儲存的優勢確實是也的,它算是犧牲一部分OLAP查詢的效能,換取一些靈活性。而這樣的換取,使得它擁有快速行級檢索,更新刪除的能力,對AnalyticDB而言是值得的。
### 索引
索引可以說是一個數據庫系統中極為重要的優化設計。目前主流的索引,包括B+tree,倒排索引,稀疏索引等等,但它們都有各種侷限,比如B+tree插入分裂代價太大,倒排索引只支援特定型別,有些索引雖然能提供快速檢索的能力但對寫入效能有負擔。那麼AnalyticDB的索引是怎樣實現的呢?
**AnalyticDB重度使用倒排索引加速檢索效率**,首先,AnalyticDB對每一列都建立一個倒排索引,索引的key是列的值,索引的值的列的行號。前面的儲存結構中可以看到,每個row group儲存的是固定的行數,所以可以快速檢索到對應的行。而針對不同的資料量特點,提供了bitmap和int array兩種結構儲存倒排索引,達到一定閾值的時候會做相應轉化。
**而針對複雜型別資料(三種,json,full text,vector),還是通過倒排提供支援,只是針對不同型別做了不同的優化改動**。
先說json,以往查詢json資料的做法,是要先讀取json然後解析再然後查詢,這樣效率很低。AnalyticDB採用空間換時間的思路,將json資料先解析,然後對每個列構建倒排索引(和單列倒排索引類似)。在查詢的時候就可以直接根據索引快速定位到對應的json。
full-text的索引方式應該是和ElasticSearch類似的,即詞到整個文件的倒排索引,查詢時還會按TF/IDF評分將結果返回給使用者(ES也是這樣)。
第三種類型是vector型別資料,主要採用NNS(nearest neighbour search)方法來加速查詢(看名字和KNN演算法有點像),大意也是用類似計算臨近資料的方式加速檢索。
對於增量資料,由於資料是落盤到磁碟上才構建全量索引,索引增量資料和已經落盤的資料有檢索效能的區別,所以需要對增量資料額外構建索引來彌補這種差距。**而AnalyticDB對增量資料構建的是排序索引**。所謂排序索引,本質上是一個數組,儲存的是資料的id。具體原理比較難解釋清楚,可以理解為就是儲存排序後的資料的id。通過排序索引可以將全表檢索的複雜度從O(n)降低到O(log n),也是一種空間換時間的思路了。
說完AnalyticDB,我們來對比下其他索引結構。Apache Kylin就不必多說了,基本就是依託於Hbase的row-key索引機制,算是比較弱的索引機制。
和AnalyticDB比較像是應該算是Druid,也是倒排索引,不過是bitmap結構儲存的倒排索引,它的倒排索引是經過優化的,叫Roaring bitmap,可以規避儲存小資料時候的儲存空間問題。相比於AnalyticDB的大而全的索引,Druid可以說是小而美。只對維度資料儲存bitmap索引,並且是和資料一起儲存在檔案中,而非AnalyticDB那樣資料和索引分開存。出現這樣的原因一個是場景上,Druid畢竟是面向OLAP查詢的,索引它只需要對維度索引構建就行。這樣的好處在於實現簡單,儲存也不會佔用太多空間。而針對單一OLAP場景,其實這樣也已經足夠了。
## 小結
總而言之言而總之,AnalyticDB因為其是雲原生,底層儲存,資源排程等都是依託於阿里雲的其他服務,所以它開源出來是不現實的(畢竟人家還靠這個賺錢),哪怕真的開源,使用者使用開源儲存和資源排程方案估計也難以做到它在阿里雲生態上那麼好。
不過它的架構和一些特性還是很有借鑑意義的,比如讀寫分離,預先分割槽,還有行-列混合儲存,強大的索引機制,索引機制如何跟底層的儲存相互配合等,這些東西目前開源的一些系統可能還沒有或沒AnalyticDB那麼完善。一方面可能是因為這些東西實現起來後,配置上會比較複雜,阿里雲的東西不怕複雜,因為後端對使用者是不可見的。要是開源系統搞得特別複雜,工程師們就不大會想用這些東西。畢竟為了一些可能用不著的效能提升,引入一個後續可能維護複雜的系統,是否值得也是需要權衡的。
總體上看,AnalyticDB還是走在了業界的前頭的,好像也通過了TPC-DS的全流程測試,算是未來可期。未來開源資料庫的方向會不會從分化走上AnalyticDB這種全面的道路呢?
以上~
[^1]:[Applying the Kappa architecture in the telco industry](https://www.cnblogs.com/gym333/p/10001203.html)
[^2]:[深入分析 Parquet 列式儲存格式
](https://www.infoq.cn/article/in-depth-analysis-of-parquet-column-storage-format)
[^3]:[AnalyticDB paper](http://www.vldb.org/pvldb/vol12/p2059-z