1. 程式人生 > >關於資料儲存引擎結構,沒有比這篇更詳細的

關於資料儲存引擎結構,沒有比這篇更詳細的

摘要:常見儲存演算法結構涵蓋:雜湊儲存,B 、B+、B*樹儲存,LSM樹儲存引擎,R樹,倒排索引,矩陣儲存,物件與塊,圖結構儲存等等。

介紹

在儲存系統的設計中,儲存引擎屬於底層資料結構,直接決定了儲存系統所能夠提供的效能和功能。常見儲存演算法結構涵蓋:雜湊儲存,B 、B+、B*樹儲存,LSM樹儲存引擎,R樹,倒排索引,矩陣儲存,物件與塊,圖結構儲存等等。

雜湊儲存引擎是雜湊表的持久化實現,一般用於鍵值型別的儲存系統。而大多傳統關係型資料庫使用索引來輔助查詢資料,用以加速對資料庫資料的訪問。考慮到經常需要範圍查詢,因此其索引一般使用樹型結構。譬如MySQL、SQL Server、Oracle中,資料儲存與索引的基本結構是B-樹和B+樹。

主流的NoSQL資料庫則使用日誌結構合併樹(Log-structured Merge Tree)來組織資料。LSM 樹儲存引擎和B樹一樣,支援增、刪、改、隨機讀取以及順序掃描。通過批量轉儲技術規避磁碟隨機寫入問題,極大地改善了磁碟的IO效能,被廣泛應用於後臺儲存系統,如Google Big table、Level DB,Facebook Cassandra系統,開源的HBase,Rocks dB等等。

……

一.雜湊儲存

雜湊儲存的基本思想是以關鍵字Key為自變數,通過一定的函式關係(雜湊函式或雜湊函式),計算出對應函式值(雜湊地址),以這個值作為資料元素的地址,並將資料元素存入到相應地址的儲存單元中。查詢時再根據要查詢的關鍵字採用同樣的函式計算出雜湊地址,然後直接到相應的儲存單元中去取要找的資料元素。代表性的使用方包括Redis,Memcache,以及儲存系統Bitcask等。

基於記憶體中的Hash,支援隨機的增刪改查,讀寫的時間複雜度O(1)。但無法支援順序讀寫(指典型Hash,不包括如Redis的基於跳錶的ZSet的其它功能),在不需要有序遍歷時,效能最優。

1. 常用雜湊函式

構造雜湊函式的總的原則是儘可能將關鍵字集合空間均勻的對映到地址集合空間中,同時儘可能降低衝突發生的概率。

  • 除留餘數法:H(Key)=key % p (p ≤ m)p最好選擇一個小於或等於m(雜湊地址集合的個數)的某個最大素數。
  • 直接地址法: H(Key) =a * Key + b;“a,b”是常量。
  • 數字分析法

比如有一組key1=112233,key2=112633,key3=119033,分析數中間兩個數比較波動,其他數不變。那麼取key的值就可以是 key1=22,key2=26,key3=90。

  • 平方取中法
  • 摺疊法

比如key=135790,要求key是2位數的雜湊值。那麼將key變為13+57+90=160,然後去掉高位“1”,此時key=60。

2. 衝突處理方法

1) 開放地址法

如果兩個資料元素的雜湊值相同,則在雜湊表中為後插入的資料元素另外選擇一個表項。當程式查詢雜湊表時,如果沒有在第一個對應的雜湊表項中找到符合查詢要求的資料元素,程式就會繼續往後查詢,直到找到一個符合查詢要求的資料元素,或者遇到一個空的表項。

①.線性探測法

這種方法在解決衝突時,依次探測下一個地址,直到有空的地址後插入,若整個空間都找遍仍然找不到空餘的地址,產生溢位。Hi =( H(Key) + di ) % m ( i = 1,2,3,...,k , k ≤ m-1 )

地址增量 di = 1,2,..., m-1, 其中 i 為探測次數

②.二次探測法

地址增量序列為: di= 1^2,-1^2,2^2,-2^2 ,...,q^2,-q^2 (q≤ m/2)

Python字典dict的實現是使用二次探查來解決衝突的。

③.雙雜湊函式探測法

Hi =( H(Key) + i * RH(Key) ) % m ( i=1,2,3,..., m-1)

H(Key) , RH(Key) 是兩個雜湊函式,m為雜湊表長度。先用第一個雜湊函式對關鍵字計算雜湊地址,一旦產生地址衝突,再用第二個函式確定移動的步長因子,最後通過步長因子序列由探測函式尋找空餘的雜湊地址。H1= (a +b) % m, H2 = (a + 2b) % m, ..., Hm-1= (a+ (m-1)*b) %m

2) 鏈地址法

將雜湊值相同的資料元素存放在一個連結串列中,在查詢雜湊表的過程中,當查詢到這個連結串列時,必須採用線性查詢方法。

Hash儲存示例

假定一個待雜湊儲存的線性表為(32,75,29,63,48,94,25,46,18,70),雜湊地址空間為HT[13],採用除留餘數法構造雜湊函式和線性探測法處理衝突。

二.B 樹儲存引擎

B樹儲存引擎是B樹的持久化實現,不僅支援單條記錄的增、刪、讀、改操作,還支援順序掃描(B+樹的葉子節點間的指標)。相比雜湊儲存引擎,B樹儲存引擎不僅支援隨機讀取,還支援範圍掃描。Mysql的MyISAM和InnoDB支援B-樹索引,InnoDB還支援B+樹索引,Memory支援Hash。

1. B-樹儲存

B-樹為一種多路平衡搜尋樹,與紅黑樹最大的不同在於,B樹的結點可以有多個子節點,從幾個到幾千個。B樹與紅黑樹相似,一棵含n個結點的B樹的高度也為O(lgn),但可能比一棵紅黑樹的高度小許多,因為它的分支因子比較大。所以B樹可在O(logn)時間內,實現各種如插入,刪除等動態集合操作。

B-樹的規則定義:

1) 定義任意非葉子節點最多可以有M個兒子節點;且M>2;

2) 則根節點的兒子數為:[2,M];

3) 除根節點為的非葉子節點的兒子書為[M/2,M];

4) 每個結點存放至少M/2-1(取上整)且至多M -1 個關鍵字;(至少為2)

5) 非葉子結點的關鍵字個數 = 指向子節點的指標書 -1;

6) 非葉子節點的關鍵字:K[1],K[2],K[3],…,K[M-1;且K[i] < K[i +1];

7) 非葉子結點的指標:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;

8) 所有葉子結點位於同一層;

下圖是一個M為3的B-樹:

B-樹的搜尋

從根結點開始,對結點內的關鍵字(有序)序列進行二分查詢,如果命中則結束,否則進入查詢關鍵字所屬範圍的兒子結點;重複,直到所對應的兒子指標為空,或已經是葉子結點;

B-樹的特性

關鍵字集合分佈在整顆樹中,任何一個關鍵字出現且只出現在一個結點中;

所有結點都儲存資料,搜尋有可能在非葉子結點結束;

搜尋效能等價於在關鍵字全集內做一次二分查詢,查詢時間複雜度不固定,與Key在樹中的位置有關,最好為O(1);

非葉子節點儲存了data資料,導致資料量很大的時候,樹的層數可能會比較高,隨著資料量增加,IO次數的控制不如B+樹優秀。

MongoDB 儲存結構

MongoDB是聚合型資料庫,而B-樹恰好Key和data域聚合在一起,所有節點都有Data域,只要找到指定索引就可以進行訪問,無疑單次查詢平均快於MySql。

MongoDB並不是傳統的關係型資料庫,而是以Json格式作為儲存的NoSQL,目的就是高效能、高可用、易擴充套件。

2. B+樹儲存

B樹在提高磁碟IO效能的同時並沒有解決元素遍歷的效率低下的問題。正是為了解決這個問題,B+樹應運而生。B+樹是對B樹的一種變形,本質還是多路平衡查詢樹。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在資料庫中基於範圍的查詢是非常頻繁的,而B樹不支援這樣的操作(或者說效率太低)。RDBMS需要B+樹用以減少尋道時間,順序訪問較快。

B+樹通常被用於資料庫和作業系統的檔案系統中。像NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等檔案系統都在使用B+樹作為元資料索引。B+樹的特點是能夠保持資料穩定有序,其插入與修改擁有較穩定的對數時間複雜度。B+樹元素為自底向上插入。

下圖是一棵高度為M=3的B+樹

B+樹上有兩個頭指標,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即資料節點)之間是一種鏈式環結構。因此可以對B+樹進行兩種查詢運算:一種是對於主鍵的範圍查詢和分頁查詢,另一種是從根節點開始,進行隨機查詢。

與普通B-樹相比,B+樹的非葉子節點只有索引,所有關鍵字都位於葉子節點,這樣會使樹節點的度比較大,而樹的高度就比較低,從而有利於提高查詢效率。並且葉子節點上的資料會形成有序連結串列。

主要優點如下:

  • 結構比較扁平,高度低(一般不超過4層),隨機尋道次數少;
  • 有n棵子樹的結點中含有n個關鍵字,不用來儲存資料,只用來索引。結點中僅含其子樹(根結點)中的最大(或最小)關鍵字。
  • 資料儲存密度大,且都位於葉子節點,查詢穩定,遍歷方便;
  • 葉子節點存放數值,按照值大小順序排序,形成有序連結串列,區間查詢轉化為順序讀,效率高。且所有葉子節點與根節點的距離相同,因此任何查詢效率都很相似。而B樹則必須通過中序遍歷才支援範圍查詢。
  • 與二叉樹不同,B+樹的資料更新操作不從根節點開始,而從葉子節點開始,並且在更新過程中樹能以比較小的代價實現自平衡。

B+樹的缺點:

  • 如果寫入的資料比較離散,那麼尋找寫入位置時,子節點有很大可能性不會在記憶體中,最終產生大量隨機寫,效能下降。下圖說明了這一點。

  • B+樹在查詢過程中應該不會慢,但如B+樹已執行很長時間,寫入了很多資料,隨著葉子節點分裂,其對應的塊會不再順序儲存而變得分散,可能會導致邏輯上原本連續的資料實際上存放在不同的物理磁碟塊位置上,這時執行範圍查詢也會變成隨機讀,會導致較高的磁碟IO,效率降低。

譬如:資料更新或者插入完全無序時,如先插入0,後80000,然後200,然後666666,這樣跨度很大的資料時,由於不在一個磁碟塊中,就需先去查詢到這個資料。資料非常離散,就意味著每次查詢時,它的葉子節點很可能都不在記憶體中,此時就會出現效能的瓶頸。並且隨機寫產生的子樹的分裂等,產生很多的磁碟碎片,也是不太友好的一面。

可見B+樹在多讀少寫(相對而言)的情境下比較有優勢。當然,可用SSD來獲得成倍提升的讀寫速率,但成本相對較高。

B+樹的搜尋

與B-樹基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可在非葉子結點命中),其效能也等價於在關鍵字全集做一次二分查詢。

B+樹的特性

非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是儲存(關鍵字)資料的資料層;B+樹的葉子結點都是相鏈的,因此對整棵樹的遍歷只需要一次線性遍歷葉子結點即可。而且由於資料順序排列並且相連,所以便於區間查詢和搜尋。而B-樹則需要進行每一層的遞迴遍歷。相鄰的元素可能在記憶體中不相鄰,所以快取命中性沒有B+樹好。

比B-樹更適合實際應用中作業系統的檔案索引和資料庫索引

1) 磁碟讀寫代價更低

B+樹的內部結點並沒有指向關鍵字具體資訊的指標。因此其內部結點相對B樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入記憶體中的需要查詢的關鍵字也就越多。相對來說IO讀寫次數也就降低了。

舉個例子,假設磁碟中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體資訊指標2bytes。一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+樹內部結點只需要1個盤快。當需要把內部結點讀入記憶體中的時候,B樹就比B+樹多一次盤塊查詢時間。

2)查詢效率更加穩定

由於非葉子結點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

MySQL InnoDB

InnoDB儲存引擎中頁大小為16KB,一般表的主鍵型別為INT(佔用4位元組)或long(8位元組),指標型別也一般為4或8個位元組,也就是說一個頁(B+樹中的一個節點)中大概儲存16KB/(8B+8B)=1K個鍵值(K取估值為10^3)。即一個深度為3的B+樹索引可維護10^3 * 10^3 * 10^3=10億條記錄。

在資料庫中,B+樹的高度一般都在2~4層。MySql的InnoDB儲存引擎在設計時是將根節點常駐記憶體的,也就是說查詢某一鍵值的行記錄時最多隻需要1~3次磁碟I/O操作。

資料庫中的B+樹索引可以分為聚集索引(clustered index)和輔助索引(secondary index)。聚集索引的B+樹中的葉子節點存放的是整張表的行記錄資料。輔助索引與聚集索引的區別在於輔助索引的葉子節點並不包含行記錄的全部資料,而是儲存相應行資料的聚集索引鍵,即主鍵。當通過輔助索引來查詢資料時,InnoDB儲存引擎會遍歷輔助索引找到主鍵,然後再通過主鍵在聚集索引中找到完整的行記錄資料。

3. B*樹儲存

B+樹的一種變形,在B+樹的基礎上將索引層以指標連線起來,使搜尋取值更加快捷。如下圖(M=3)

相對B+樹的變化,如下:

  • B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3代替B+樹的1/2,將結點的最低利用率從1/2提高到2/3;
  • B+樹的分裂:當一個結點滿時分配一個新的結點,並將原結點中1/2的資料複製到新結點,最後在父結點中增加新結點的指標;B+樹的分裂隻影響原結點和父結點,而不影響兄弟結點,所以它不需指向兄弟的指標;
  • B*樹的分裂:當一個結點滿時,如它的下一個兄弟結點未滿,那麼將一部分資料移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因兄弟結點的關鍵字範圍改變),如兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3資料到新結點,最後在父結點增加新結點的指標.

相對於B+樹,B*樹分配新結點的概率比B+樹要低,空間利用率、查詢速率也有所提高。

三.LSM樹儲存引擎

資料庫的資料大多儲存在磁碟上,無論是機械硬碟還是固態硬碟(SSD),對磁碟資料的順序讀寫速度都遠高於隨機讀寫。大量的隨機寫,導致B樹在資料很大時,出現大量磁碟IO,速度越來越慢,基於B樹的索引結構是違背上述磁碟基本特點的—需較多的磁碟隨機讀寫。於是,基於日誌結構的新型索引結構方法應運而生,主要思想是將磁碟看作一個大的日誌,每次都將新的資料及其索引結構新增到日誌的最末端,以實現對磁碟的順序操作,從而提高索引效能。

對海量儲存叢集而言,LSM樹也是作為B+樹的替代方案而產生。當今很多主流DB都使用了LSM樹的儲存模型,包括LevelDB,HBase,Google BigTable,Cassandra,InfluxDB, RocksDB等。LSM樹通過儘可能減少寫磁碟次數,實際落地儲存的資料按key劃分,形成有序的不同檔案;結合其“先記憶體更新後合併落盤”的機制,儘量達到順序寫磁碟,儘可能減少隨機寫;對於讀則需合併磁碟已有歷史資料和當前未落盤的駐於記憶體的更新。LSM樹儲存支援有序增刪改查,寫速度大幅提高,但隨機讀取資料時效率低。

LSM樹實際不是一棵樹,而是2個或者多個樹或類似樹的結構的集合。

下圖為包含2個結構的LSM樹

在LSM樹中,最低一級即最小的C0樹位於記憶體,而更高階的C1、C2...樹都位於磁盤裡。資料會先寫入記憶體中的C0樹,當它的大小達到一定閾值之後,C0樹中的全部或部分資料就會刷入磁碟中的C1樹,如下圖所示。

由於記憶體讀寫速率比外存要快非常多,因此資料寫入C0樹的效率很高。且資料從記憶體刷入磁碟時是預排序的,也就是說,LSM樹將原本隨機寫操作轉化成了順序寫操作,寫效能大幅提升。不過,它的tradeoff就是犧牲了一部分讀效能,因為讀取時需將記憶體中資料和磁碟中的資料合併。總體上講這種權衡還是值得的,因為:

  • 可以先讀取記憶體中C0樹的快取資料。記憶體效率高且根據區域性性原理,最近寫入的資料命中率也高。
  • 寫入資料未刷到磁碟時不會佔用磁碟的I/O,不會與讀取競爭。讀取操作就能取得更長的磁碟時間,變相地彌補了讀效能差距。

在實際應用中,為防止記憶體因斷電等原因丟失資料,寫入記憶體的資料同時會順序在磁碟上寫日誌,類似於預寫日誌(WAL),這就是LSM這個詞中Log一詞的來歷。另外,如有多級樹,低階的樹在達到大小閾值後也會在磁碟中進行合併,如下圖所示。

1. Leveldb/Rocksdb

基本描述

1) 對資料,按key劃分為若干level,每個level對應若干檔案,包括存在於記憶體中和落盤的。檔案內key都有序,同級的各個檔案之間一般也有序,level0對應於記憶體中的資料(0.sst),後面依次是1、2、3、...的各級檔案(預設到level6)。

2) 寫時,先寫對應記憶體的最低level的檔案,這也是快的一個原因。記憶體的資料也是被持久化的,達到一定大小後被合併到下一級檔案落盤。

3) 落盤後的各級檔案也會定期進行排序加合併,合併後資料進入下一層level;這樣的寫入操作,大多數都是對一個頁順序的寫,或者申請新頁寫,而不再是隨機寫。

Rocksdb Compact

Compact是個很重要的步驟,下面是rocksdb的compact過程:

Rocksdb的各級檔案組織形式:

各級的每個檔案,內部都按key有序,除了記憶體對應的level0檔案,內部檔案之間也是按key有序;這樣查詢一個key,很方便二分查詢。

然後,當每一級的資料到達一定閾值時,會觸發排序歸併,簡單說,就是在兩個level的檔案中,把key有重疊的部分,合併到高層level的檔案裡

這個在LSM樹裡叫資料壓縮(compact);

對於Rocksdb,除了記憶體level0到level1的compact,其他各級之間的compact可以並行;通常設定較小的level0到level1的compact閾值,加快這一層的compact。良好的歸併策略的配置,使資料儘可能集中在最高層(90%以上),而不是中間層,這樣有利於compact的速度;另外,對於基於LSM樹的讀,需要在各級檔案中二分查詢,磁碟IO也不少,此外還需要關注level0裡的對於這個key的操作,比較明顯的優化是,通過Bloomfilter略掉肯定不存在該key的檔案,減少無謂查詢;

2. HBase LSM

說明:本小節需事先了解HBase的讀寫流程及MemStore。

MemStore作為列族級別的寫入和讀取快取,它就是HBase中LSM樹的C0層。它未採用樹形結構來儲存,而是採用了跳錶(一種替代自平衡BST二叉排序樹的結構)。MemStore Flush的過程,也就是LSM樹中C0層刷寫到C1層的過程,而LSM中的日誌對應到HBase自然就是HLog了。

HBase讀寫流程簡圖

HFile就是LSM樹中的高層實現。從邏輯上來講,它是一棵滿的3層B+樹,從上到下的3層索引分別是Root index block、Intermediate index block和Leaf index block,對應到下面的Data block就是HFile的KeyValue結構。HFile V2索引結構的圖示如下:

HFile過多會影響讀寫效能,因此高層LSM樹的合併即對應HFile的合併(Compaction)操作。合併操作又分Minor和Major Compaction,由於Major Compaction涉及整個Region,對磁碟壓力很大,因此要儘量避免。

布隆過濾器(Bloom Filter)

是儲存在記憶體中的一種資料結構,可用來驗證“某樣東西一定不存在或者可能存在”。由一個超長的二進位制位陣列和一系列的Hash函式組成,二進位制位陣列初始全部為0,當有元素加入集合時,這個元素會被一系列Hash函式計算映射出一系列的值,所有的值在位陣列的偏移量處置為1。如需判斷某個元素是否存在於集合中,只需判斷該元素被Hash後的值在陣列中的值,如果存在為0的則該元素一定不存在;如果全為1則可能存在,這種情況可能有誤判。

HBase為了提升LSM結構下的隨機讀效能而引入布隆過濾器(建表語句中可指定),對應HFile中的Bloom index block,其結構圖如下。

通過布隆過濾器,HBase能以少量的空間代價,換來在讀取資料時非常快速地確定是否存在某條資料,效率進一步提升。

LSM-樹的這種結構非常有利於資料的快速寫入(理論上可接近磁碟順序寫速度),但不利於讀,因為理論上讀的時候可能需要同時從memtable和所有硬碟上的sstable中查詢資料,這樣顯然會對效能造成較大影響。為解決這個問題,LSM-樹採取以下主要相關措施。

  • 定期將硬碟上小的sstable合併(Merge或Compaction)成大的sstable,以減少sstable的數量。且平時的資料更新刪除操作並不會更新原有的資料檔案,只會將更新刪除操作加到當前的資料檔案末端,只有在sstable合併的時候才會真正將重複的操作或更新去重、合併。
  • 對每個sstable使用布隆過濾器,以加速對資料在該sstable的存在性進行判定,從而減少資料的總查詢時間。

SSTable(Sorted String Table)

當記憶體中的MemTable達到一定大小,需將MemTable轉儲(Dump)到磁碟中生成SSTable檔案。由於資料同時存在MemTable和可能多個SSTable中,讀取操作需按從舊到新的時間順序合併SSTable和記憶體中的MemTable資料。

SSTable 中的資料按主鍵排序後存放在連續的資料塊(Block)中,塊之間也有序。接著,存放資料塊索引,由每個Block最後一行的主鍵組成,用於資料查詢中的Block定位。接著存放布隆過濾器和表格的Schema資訊。最後存放固定大小的Trailer以及Trailer的偏移位置。

本質上看,SSTable是一個兩級索引結構:塊索引以及行索引。分為稀疏格式和稠密格式。對於稀疏格式,某些列可能存在也可能不存在,因此每一行只儲存包含實際值的列,每一列儲存的內容為:<列ID,列值>; 而稠密格式中每一行都需儲存所有列,每一列只需儲存列值,不需儲存列ID,列ID可從表格Schema中獲取。

… …

3. 相簿ArangoDB LSM

ArangoDB採用RocksDB做底層儲存引擎,RocksDB使用LSM-Tree資料結構。

儲存的格式非常類似JSON,叫做VelocyPack格式的二進位制格式儲存。

(1)文件被組織在集合中。

(2)有兩種集合:文件(V),邊集合(E)

(3)邊集合也是以文件形式儲存,但包含兩個特殊的屬性_from和_to,被用來建立在文件和文件之間建立關係

索引型別

· Primary Index,預設索引,建立欄位是_key或_id上,一個雜湊索引

· Edge Index,預設索引,建立在_from、_to上,雜湊索引;不能用於範圍查詢、排序,弱於OrientDB

· Hash Index,自建

· Skiplist Index,有序索引,

o 用於快速查詢具有特定屬性值的文件,範圍查詢以及按索引排序,順序返回文件。

o 用於查詢,範圍查詢和排序。補全範圍查詢的缺點。

· Persistent Index,RocksDB的索引。

o 永續性索引是具有永續性的排序索引。當儲存或更新文件時,索引條目將寫入磁碟。

o 使用永續性索引可能會減少集合載入時間。

· Geo Index,可以在集合中的一個或多個屬性上建立其他地理索引。

· Fulltext Index,全文索引

四.R樹的儲存結構

R樹是B樹在高維空間的擴充套件,是一棵平衡樹。每個R樹的葉子結點包含了多個指向不同資料的指標,這些資料可以是存放在硬碟中,也可存在記憶體中。根據R樹的這種資料結構,當需進行一個高維空間查詢時,只需要遍歷少數幾個葉子結點所包含的指標,檢視這些指標指向的資料是否滿足即可。這種方式使得不必遍歷所有資料即可獲得答案,效率顯著提高。

下圖是R樹的一個簡單例項:

R樹運用空間分割理念,採用一種稱為MBR(Minimal Bounding Rectangle)的方法,在此譯作“最小邊界矩形”。從葉子結點開始用矩形將空間框起來,結點越往上,框住的空間就越大,以此對空間進行分割。

以二維空間舉例,下圖是Guttman論文中的一幅圖:

1) 首先假設所有資料都是二維空間下的點,圖中僅僅標誌了R8區域中的資料,也就是那個shape of data object。別把那一塊不規則圖形看成一個數據,把它看作是多個數據圍成的一個區域。為了實現R樹結構,用一個最小邊界矩形恰好框住這個不規則區域,這樣就構造出了一個區域:R8。R8的特點很明顯,就是正好框住所有在此區域中的資料。其他實線包圍住的區域,如R9,R10,R12等都是同樣道理。這樣一共得到了12個最最基本的最小矩形。這些矩形都將被儲存在子結點中。

2) 下一步操作就是進行高一層次的處理,發現R8,R9,R10三個矩形距離最為靠近,因此就可以用一個更大的矩形R3恰好框住這3個矩形。

3) 同樣,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之後,再次迭代,用更大的框去框住這些矩形。

用地圖和餐廳的例子來解釋,就是所有的資料都是餐廳所對應的地點,先把相鄰的餐廳劃分到同一塊區域,劃分好所有餐廳之後,再把鄰近的區域劃分到更大的區域,劃分完畢後再次進行更高層次的劃分,直到劃分到只剩下兩個最大的區域為止。要查詢的時候就方便了。

然後就可以把這些大大小小的矩形存入R樹中。根結點存放的是兩個最大的矩形,這兩個最大的矩形框住了所有的剩餘的矩形,當然也就框住了所有的資料。下一層的結點存放了次大的矩形,這些矩形縮小了範圍。每個葉子結點都是存放的最小的矩形,這些矩形中可能包含有n個數據。

查詢特定的資料

以餐廳為例,假設查詢廣州市天河區天河城附近一公里的所有餐廳地址

1) 開啟地圖(即整個R樹),先選擇國內還是國外(根結點);

2) 然後選擇華南地區(對應第一層結點),選擇廣州市(對應第二層結點);

3) 再選擇天河區(對應第三層結點);

4) 最後選擇天河城所在的那個區域(對應葉子結點,存放有最小矩形),遍歷所有在此區域內的結點,看是否滿足要求即可。

其實R樹的查詢規則跟查地圖很像,對應下圖:

一棵R樹滿足如下性質:

1) 除非它是根結點之外,所有葉子結點包含有m至M個記錄索引(條目)。作為根結點的葉子結點所具有的記錄個數可以少於m。通常,m=M/2。

2) 對於所有在葉子中儲存的記錄(條目),I是最小的可以在空間中完全覆蓋這些記錄所代表的點的矩形(此處所說“矩形”是可擴充套件到高維空間)。

3) 每一個非葉子結點擁有m至M個孩子結點,除非它是根結點。

4) 對於在非葉子結點上的每一個條目,i是最小的可在空間上完全覆蓋這些條目所代表的店的矩形(同性質2)

5) 所有葉子結點都位於同一層,因此R樹為平衡樹。

說明:

葉子結點的結構,資料形式為: (I, tuple-identifier),tuple-identifier表示的是一個存放於資料庫中的tuple,也就是一條記錄,是n維的。I是一個n維空間的矩形,並可恰好框住這個葉子結點中所有記錄代表的n維空間中的點。I=(I0,I1,…,In-1)。

R樹是一種能夠有效進行高維空間搜尋的資料結構,已被廣泛應用在各種資料庫及其相關的應用中。但R樹的處理也具侷限性,它的最佳應用範圍是處理2至6維的資料,更高維的儲存會變得非常複雜,這樣就不適用。近年來,R樹也出現了很多變體,R*樹就是其中的一種。這些變體提升了R樹的效能,如需更多瞭解,可以參考相關文獻。

應用示例

地理圍欄(Geo-fencing)是LBS(Location Based Services)的一種應用,就是用一個虛擬的柵欄圍出一個虛擬地理邊界,當手機進入、離開某個特定地理區域,或在該區域內活動時,手機可以接收自動通知和警告。譬如,假設地圖上有三個商場,當用戶進入某個商場的時候,手機自動收到相應商場傳送的優惠券push訊息。地理圍欄應用非常廣泛,當今移動網際網路主要app如美團、大眾點評、手淘等都可看到其應用身影。

五.倒排索引儲存

對Mysql來說,採用B+樹索引,對Elasticsearch/Lucene來說,是倒排索引。倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地講,正向索引是通過key找value,反向索引則是通過value找key。

Elasticsearch是建立在全文搜尋引擎庫Lucene基礎上的搜尋引擎,它隱藏了Lucene的複雜性,提供一套簡單一致的RESTful API。Elasticsearch的倒排索引其實就是Lucene的倒排索引。

1. Lucene 的倒排索引

倒排索引,通過Term搜尋到文件ID。首先想想看,世界上那麼多單詞,中文、英文、日文、韓文 … 如每次搜尋一個單詞都要全域性遍歷一遍,明顯不行。於是有了對單詞進行排序,像B+樹一樣可在頁裡實現二分查詢。光排序還不行,單詞都放在磁碟,磁碟IO慢的不得了,大量存放記憶體會導致記憶體爆了。

下圖:通過字典把所有單詞都貼在目錄裡。

Lucene的倒排索引,增加了最左邊的一層「字典樹」term index,它不儲存所有的單詞,只儲存單詞字首,通過字典樹找到單詞所在的塊,也就是單詞的大概位置,再在塊裡二分查詢,找到對應的單詞,再找到單詞對應的文件列表。

假設有多個term,如:Carla,Sara,Elin,Ada,Patty,Kate,Selena。找出某個特定term,可通過排序後以二分查詢方式,logN次磁碟查詢得到目標,但磁碟隨機讀操作較昂貴(單次Random access約10ms)。所以儘量少的讀磁碟,可把一些資料快取到記憶體。但term dictionary太大,於是就有了term index.通過trie樹來構建index;

通過term index可快速定位term dictionary的某個offset,從這個位置再往後順序查詢。再加上一些壓縮技術(FST),term index 的尺寸可以只有所有term尺寸的幾十分之一,使得用記憶體快取整個term index變成可能。

FST Tree

一種有限狀態轉移機,優點:1)空間佔用小。通過對詞典中單詞字首和字尾的重複利用,壓縮了儲存空間;2)查詢速度快。O(len(str))的查詢時間複雜度。

示例:對“cat”、 “deep”、 “do”、 “dog” 、“dogs”這5個單詞進行插入構建FST(必須已排序),得到如下一個有向無環圖。

FST壓縮率一般在3倍~20倍之間,相對於TreeMap/HashMap的膨脹3倍,記憶體節省就有9倍到60倍!

2. 與MySQL檢索對比

MySQL只有term dictionary,以B-樹排序的方式儲存在磁碟上。檢索一個term需若干次random access磁碟操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,以樹的形式快取在記憶體中。從term index查到對應term dictionary的block位置後,再去磁碟上找term,大大減少磁碟的random access次數。

Term index在記憶體中是以FST(finite state transducers)的壓縮形式儲存,其特點是非常節省記憶體。Term dictionary在磁碟上是以分block的方式儲存的,一個block內部利用公共字首壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可比B-樹更節約空間。

3. 聯合索引結構及檢索

給定多個查詢過濾條件,如”age=18 AND gender=女”就是把兩個posting list做一個“與”的合併。對於MySql來說,如果給age和gender兩個欄位都建立了索引,查詢時只會選擇其中最selective的來用,然後另一個條件是在遍歷行的過程中在記憶體中計算之後過濾掉。那如何聯合使用兩個索引呢?兩種辦法:

  • 使用skip list資料結構,同時遍歷gender和age的posting list,互相skip;
  • 使用bitset資料結構,對gender和age兩個filter分別求出 bitset,對兩個bitset做AND操作。

PostgreSQL從8.4版本開始支援通過bitmap聯合使用兩個索引,就是利用bitset資料結構來做的。一些商業的關係型資料庫也支援類似聯合索引的功能。

ES支援以上兩種的聯合索引方式,如果查詢的filter快取到了記憶體中(以bitset形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有快取,那麼就用skip list的方式去遍歷兩個on disk的posting list。

1) 利用Skip List合併

即使對於排過序的連結串列,查詢還是需要進行通過連結串列的指標進行遍歷,時間複雜度很高依然是O(n),這個顯然不能接受。是否可以像陣列那樣,通過二分法查詢,但由於在記憶體中的儲存的不確定性,不能這麼做。但可結合二分法思想,跳錶就是連結串列與二分法的結合。跳錶是有序連結串列。

  • 連結串列從頭節點到尾節點都是有序的
  • 可以進行跳躍查詢(形如二分法),降低時間複雜度

說明:在上圖中如要找node6節點

1) 第一次比較headerNode->next[2]的值,即node5的值。顯然node5小於node6,所以,下一次應從第2級的node5開始查詢,令targetNode=targetNode->next[2];

2) 第二次應該比較node5->next[2]的值,即tailNode的值。tailNode的值是最大的,所以結果是大於,下一次應從第1級的node5開始查詢。這裡從第2級跳到第1級。但沒有改變targetNode。

3) 第三次應該比較node5->next[1]的值,即node7的值。因node7大於node6,所以,下一次應該從第0級的node5開始查詢。這裡從第1級跳到第0級。也沒有改變targetNode。

4) 第四次應該比較node5->next[0]的值,即node6的值。這時相等,結束。如果小於,targetNode往後移,改變targetNode=targetNode->next[0],如果大於,則沒找到,結束。因為這已經是第0級,沒法再降了。

考慮到頻繁出現的term,如gender裡的男或女。如有1百萬個文件,那性別為男的posting list裡就會有50萬個int值。用FOR編碼進行壓縮可極大減少磁碟佔用,對於減少索引尺寸有非常重要的意義。當然MySql B-樹裡也有類似的posting list,是未經這樣壓縮的。因為FOR的編碼有解壓縮成本,利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了CPU。

Frame Of Reference

以下三個步驟組成了Frame Of Reference(FOR)壓縮編碼技術

Step 1:增量編碼

Step 2:分割成塊

Lucene每個塊是256個文件ID,每個塊增量編碼後每個元素都不會超過256(1 byte).為方便演示,圖中假設每個塊是3個文件ID。

Step 3:按需分配空間

對於第一個塊 [73, 227, 2],最大元素是227,需要 8 bits,那給這個塊的每個元素,都分配8 bits的空間。但對於第二個塊[30,11,29],最大才30,只需5bits,那給每個元素只分配5bits空間。這一步可說是精打細算,按需分配。

2) 利用bitset合併

Bitset是一種很直觀的資料結構,posting list如:[1,3,4,7,10]對應的bitset就是:[1,0,1,1,0,0,1,0,0,1], 每個文件按照文件id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文件。所以100萬個文件只需要12.5萬個byte。但考慮到文件可能有數十億之多,在記憶體裡儲存bitset仍然是很奢侈的事情。且對於每一個filter都要消耗一個bitset,比如age=18快取起來的話是一個bitset,18<=age<25是另外一個filter,快取起來也要一個bitset。所以需要一個數據結構:

  • 可以壓縮地儲存上億個bit代表對應的文件是否匹配filter;
  • 壓縮的bitset仍然可以很快地進行AND和OR的邏輯操作。

Roaring Bitmap

Lucene使用這個資料結構,其壓縮思路其實很簡單,與其儲存100個0,佔用100個bit,還不如儲存0一次,然後宣告這個0重複了100遍。

bitmap不管有多少文件,佔用的空間都一樣。譬如一個數組裡面只有兩個文件ID:[0, 65535],表示[1,0,0,0,….(很多個0),…,0,0,1],需要65536個bit,也就是65536/8=8192 bytes。而用Integer陣列只需2*2bytes=4 bytes。可見在文件數量不多時使用Integer陣列更節省記憶體。算一下臨界值,無論文件數量多少,bitmap都需要8192bytes,而Integer陣列則和文件數量成線性相關,每個文件ID佔2bytes,所以8192/2=4096,當文件數量少於4096時用Integer陣列,否則用bitmap.

4. 儲存文件的減少方式

一種常見的壓縮儲存時間序列的方式是把多個數據點合併成一行。Opentsdb支援海量資料的一個絕招就是定期把很多行資料合併成一行,這個過程叫compaction。類似的vivdcortext使用MySql儲存的時候,也把一分鐘的很多資料點合併儲存到MySql的一行以減少行數。

ES可實現類似優化,那就是Nested Document。可把一段時間的很多個數據點打包儲存到一個父文件裡,變成其巢狀的子文件。這樣可把資料點公共的維度欄位上移到父文件裡,而不用在每個子文件裡重複儲存,從而減少索引的尺寸。

儲存時,無論父文件還是子文件,對於Lucene來說都是文件,都會有文件Id。但對於巢狀文件來說,可以儲存起子文件和父文件的文件id是連續的,且父文件總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文件的posting list就可跟蹤所有的父子關係。也可以很容易地在父子文件id之間做轉換。把父子關係也理解為一個filter,那麼查詢檢索的時候不過是又AND了另外一個filter而已。

使用了巢狀文件之後,對於term的posting list只需要儲存父文件的docid就可,可以比儲存所有的資料點的doc id要少很多。如可在一個父文件裡塞入50個巢狀文件,那posting list可變成之前的1/50。

… … …

六.物件與塊儲存

本章節描述,以Ceph 分散式儲存系統為參考。

1. 物件儲存結構

在檔案系統一級提供服務,只是優化了目前的檔案系統,採用扁平化方式,棄用了目錄樹結構,便於共享,高速訪問。

物件儲存體系結構定義了一個新的、更加智慧化的磁碟介面OSD。OSD是與網路連線的裝置,包含儲存介質,如磁碟或磁帶,並具有足夠智慧可管理本地儲存的資料。計算結點直接與OSD通訊,訪問它儲存的資料,不需要檔案伺服器的介入。物件儲存結構提供的效能是目前其它儲存結構難以達到的,如ActiveScale物件儲存檔案系統的頻寬可以達到10GB/s。

物件儲存的結構包括元資料伺服器(控制節點MDS)和資料儲存伺服器(OSD),兩者進行資料的儲存,還需客服端進行儲存的服務訪問和使用。

2. 塊裝置儲存

塊儲存技術的構成基礎是最下層的硬體儲存裝置,可能是機械硬碟也可能是固態硬碟。一個作業系統下可以獨立控制多個硬體儲存裝置,但這些硬體儲存裝置的工作相對獨立,通過作業系統命令看到的也是幾個獨立的裝置檔案。通過陣列控制層的裝置可以在同一個作業系統下協同控制多個儲存裝置,讓後者在作業系統層被視為同一個儲存裝置。

典型裝置:磁碟陣列,硬碟,虛擬硬碟,這種介面通常以QEMU Driver或者Kernel Module的方式存在,介面需要實現Linux的Block Device的介面或者QEMU提供的Block Driver介面,如Sheepdog,AWS的EBS,阿里雲的盤古系統,還有Ceph的RBD(RBD是Ceph面向塊儲存的介面)。

  • 可通過Raid與LVM等手段對資料提供了保護;
  • 多塊廉價的硬碟組合起來,提高容量;
  • 多塊磁碟組合出來的邏輯盤,提升讀寫效率。

Ceph的RBD(RADOS Block Device)使用方式:先建立一個塊裝置,然後對映塊裝置到伺服器,掛載後即可使用。

七.矩陣的儲存結構

說明:本章節以矩陣儲存為重心,而非矩陣的運算。

矩陣具有元素數目固定以及元素按下標關係有序排列等特點,所以在使用高階語言程式設計時,一般是用二維陣列來儲存矩陣。資料庫表資料是行列儲存,可視為矩陣的應用形式。矩陣的儲存包括完全儲存和稀疏儲存方式。

1. 常規&特殊矩陣形態

常規矩陣形態

採用二維陣列來儲存,可按行優先或列優先的形式記錄。

特殊矩陣形態

特殊矩陣指的是具有許多相同元素或零元素,並且這些元素的分佈有一定規律性的矩陣。這種矩陣如果還使用常規方式來儲存,就會產生大量的空間浪費,為了節省儲存空間,可以對這類矩陣採用壓縮儲存,壓縮儲存的方式是把那些呈現規律性分佈的相同元素只分配一個儲存空間,對零元素不分配儲存空間。

1) 對稱矩陣

對於矩陣中的任何一個元素都有aij=aji 1≤i,j≤n這樣的矩陣就叫對稱矩陣。也就是上三角等於下三角。對於對稱矩陣的儲存方式是儲存上三角或者下三角加對角線,假設一個n階對稱方陣,如果全部儲存使用的儲存空間是n*n,壓縮儲存則是n(n+1)/2,對角線元素為n個,除了對角線之外為n*n-n,需要儲存的元素(n*n-n)/2,加上對角線上的元素後結果就是n(n+1)/2。假設儲存一個n階對稱矩陣,使用sa[n(n+1)/2]來儲存,那麼sa[k]與ai,j的關係是:

當i>=j時,k= i(i-1)/2+j-1

當i<j 時,k =j(j-1)/2+i-1

2) 三角矩陣

上三角或下三角全為相同元素的矩陣。可用類似於對稱矩陣的方式儲存,再加上一個位置用來儲存上三角或者下三角元素就好。

a[i][j]=a[0][0] + (i* (i+1) /2+j) *size;

size為每個資料所佔用的儲存單元大小。

3) 帶狀對角矩陣

矩陣中所有非0元素集中在主對角線為中心的區域中。下圖為3條對角線區域,其他區域的元素都為0。除了第一行和最後一行僅2個非零元素,其餘行都是3個非零元素,所以所需的一維空間大小為:3n - 2;

a[i][j]的記憶體地址=a00首地址+ ((3 * i -1) + (j-i+1)) * size;(size為每個資料所佔用的儲存單元大小)。比如首地址為1000,每個資料佔用2個儲存單元,那麼a45在記憶體中的地址=1000+13 * 2=1026;

2. 稀疏矩陣及壓縮

由於特殊矩陣中非零元素的分佈是有規律的,所以總是可以找到矩陣元素與一維陣列下標的對應關係,但還有一種矩陣,矩陣中大多數元素都為0,一般情況下非零元素個數只佔矩陣元素總數的5%以下,且元素的分佈無規律,這樣的矩陣稱為稀疏矩陣。

三元組順序法

如果採用常規方法儲存稀疏矩陣,會相當浪費儲存空間,因此只儲存非零元素。除了儲存非零元素的值之外,還需要同時儲存非零元素的行、列位置,也就是三元組(i,j,aij)。矩陣元素的儲存順序並沒有改變,也是按列的順序進行儲存。

三元組也就是一個矩陣,一個二維陣列,每一行三個列,分別為行號、列號、元素值。由於三元組在稀疏矩陣與記憶體地址間扮演了一箇中間人的角色,所以稀疏矩陣進行壓縮儲存後,便失去了隨機存取的特性。

行邏輯連結的順序表

為了隨機訪問任意一行的非零元,這種方式需要一個數組指向每一行開始元素(非零元素)的位置。這種方式適合矩陣相乘。

十字連結串列法

當矩陣中元素非零元個數和位置在操作過程中變化較大時,就不宜採用順序儲存結構來表示三元組的線性表。如在進行加減操作時,會出現非零元變成零元的情況,因此,就適合用十字連結串列來儲存。

十字連結串列的結構有五個域,一個數據域存放矩陣元,i、j 域分別存放該非零元所在的行、列。還有right、down域,right指向右邊第一個矩陣元的位置,down用來指向下面第一個矩陣元的位置。然後建立兩個陣列,分別指向每行/列的第一個元素。十字連結串列在圖中也有應用,用來儲存圖。

八.圖結構儲存

圖通常用來表示和儲存具有“多對多”關係的資料,是資料結構中非常重要的一種結構。

1. 鄰接矩陣結構

圖的鄰接矩陣儲存方式是用兩個陣列來表示圖。一個一維陣列儲存圖中頂點資訊,一個二維陣列(鄰接矩陣)儲存圖中的邊或弧的資訊。

設圖G有n個頂點,則鄰接矩陣是一個n*n的方陣,定義為:

無向圖

1)基於鄰接矩陣容易判斷任意兩頂點是否有邊無邊;

2)某個頂點的度就是這個頂點vi在鄰接矩陣中第i行或(第i列)的元素和;

3)vi的所有鄰接點就是矩陣中第i行元素,如arc[i][j]為1就是鄰接點;

n個頂點和e條邊的無向網圖的建立,時間複雜度為O(n + n2 + e),其中對鄰接矩陣的初始化耗費了O(n2)的時間。

有向圖

有向圖講究入度和出度,頂點vi的入度為1,正好是第i列各數之和。頂點vi的出度為2,即第i行的各數之和。

若圖G是網圖,有n個頂點,則鄰接矩陣是一個n*n的方陣,定義為:

wij表示(vi,vj)上的權值。無窮大表示一個計算機允許的、大於所有邊上權值的值,也就是一個不可能的極限值。

下面左圖是一個有向網圖,右圖是其鄰接矩陣。

2. 鄰接表結構

鄰接矩陣是不錯的一種圖儲存結構,但對邊數相對頂點較少的圖存在對儲存空間的極大浪費。因此,找到一種陣列與連結串列相結合的儲存方法稱為鄰接表。鄰接表的處理方法:

1)圖中頂點用一個一維陣列儲存,當然,頂點也可用單鏈表來儲存,不過陣列較容易的讀取頂點的資訊。

2)圖中每個頂點vi的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表儲存,無向圖稱為頂點vi的邊表,有向圖則稱為頂點vi作為弧尾的出邊表。

例如,下圖就是一個無向圖的鄰接表的結構。

從圖中可以看出,頂點表的各個結點由data和firstedge兩個域表示,data是資料域,儲存頂點的資訊,firstedge是指標域,指向邊表的第一個結點,即此頂點的第一個鄰接點。邊表結點由adjvex和next兩個域組成。adjvex是鄰接點域,儲存某頂點的鄰接點在頂點表中的下標,next則儲存指向邊表中下一個結點的指標。

對於帶權值的網圖,可以在邊表結點定義中再增加一個weight的資料域,儲存權值資訊即可,如下圖所示。

對於無向圖,一條邊對應都是兩個頂點,所以在迴圈中,一次就針對i和j分佈進行插入。本演算法的時間複雜度,對於n個頂點e條邊來說,容易得出是O(n+e)。

3. 十字連結串列儲存

對於有向圖來說,鄰接表是有缺陷的。關心了出度問題,想了解入度就必須要遍歷整個圖才知道,反之,逆鄰接表解決了入度卻不瞭解出度情況。而十字連結串列儲存方法可把鄰接表和逆鄰接表結合。

重新定義頂點表結點結構,如下所示

其中firstin表示入邊表頭指標,指向該頂點的入邊表中第一個結點,firstout表示出邊表頭指標,指向該頂點的出邊表中的第一個結點。

重新定義邊表結構,如下所示

其中tailvex是指弧起點在頂點表的下表,headvex是指弧終點在頂點表的下標,headlink是指入邊表指標域,指向終點相同的下一條邊,taillink是指邊表指標域,指向起點相同的下一條邊。還可增加一個weight域來儲存權值。

比如下圖,頂點依然是存入一個一維陣列,實線箭頭指標的圖示完全與鄰接表相同。就以頂點v0來說,firstout指向的是出邊表中的第一個結點v3。所以v0邊表結點hearvex=3,而tailvex其實就是當前頂點v0的下標0,由於v0只有一個出邊頂點,所有headlink和taillink都是空的。

虛線箭頭的含義。它其實就是此圖的逆鄰接表的表示。對於v0來說,它有兩個頂點v1和v2的入邊。因此它的firstin指向頂點v1的邊表結點中headvex為0的結點,如上圖圓圈1。接著由入邊結點的headlink指向下一個入邊頂點v2,如上圖圓圈2。對於頂點v1,它有一個入邊頂點v2,所以它的firstin指向頂點v2的邊表結點中headvex為1的結點,如上圖圓圈3。

十字連結串列的好處就是因為把鄰接表和逆鄰接表整合在一起,這樣既容易找到以v為尾的弧,也容易找到以v為頭的弧,因而較易求得頂點的出度和入度。除了結構複雜一點外,其實建立圖演算法的時間複雜度和鄰接表相同,因此在有向圖應用中,十字連結串列是非常好的資料結構模型。

九.分散式圖儲存

分散式圖(巨型圖)的儲存總體上有邊分割和點分割兩種方式,目前業界廣泛接受並使用的儲存方式為點分割,點分割在處理效能上要高於邊分割。

  • 邊分割(Edge-Cut):每個頂點都儲存一次,但有的邊會被打斷分到兩臺機器上。這樣做的好處是節省儲存空間;壞處是對圖進行基於邊的計算時,對於一條兩個頂點被分到不同機器上的邊來說,要跨機器通訊傳輸資料,內網通訊流量大。
  • 點分割(Vertex-Cut):每條邊只儲存一次,都只會出現在一臺機器上。鄰居多的點會被複制到多臺機器上,增加了儲存開銷,同時會引發資料同步問題。好處是可以大幅減少內網通訊量。

雖然兩種方法互有利弊,但現在是點分割佔上風,各種分散式圖計算框架都將自己底層的儲存形式變成了點分割。主要原因有以下兩個。

1) 磁碟價格下降,儲存空間不再是問題,而內網通訊資源無突破性進展,叢集計算時內網頻寬是寶貴的,時間比磁碟更珍貴。這點類似於常見的空間換時間的策略。

2) 在當前應用場景中,絕大多數網路都是“無尺度網路”,遵循冪律分佈,不同點的鄰居數量相差非常懸殊。而邊分割會使那些多鄰居的點所相連的邊大多數被分到不同的機器上,這樣的資料分佈會使得內網頻寬更加捉襟見肘,於是邊分割儲存方式被漸漸拋棄。

1. GraphX儲存模式

借鑑PowerGraph,使用點分割方式儲存圖,用三個RDD(Resilient Distributed Dataset,彈性分散式資料集)儲存圖資料資訊:

VertexTable(id, data): id為Vertex id,data為Edge data

EdgeTable(pid, src, dst, data):pid為PartionId,src為原頂點id,dst為目的頂點id

RoutingTable(id, pid):id為Vertex id,pid為Partion id

點分割儲存實現如下圖所示:

2. 超大規模圖儲存

超大規模圖(數十億點,數百億邊)儲存,需要分散式的儲存架構。在圖載入時,整張圖在圖處理引擎內部被切分為多個子圖,每個計算節點被分配1個或幾個子圖進行載入。

一張有向圖的基本元素是頂點和邊,一般都具有型別和權重,邊是有向的,一條邊由:起點、終點、型別三者標識,即相同的兩點之間可以同時具有多條不同型別的邊。下面是一個簡單的帶權異構屬性圖示例:

頂點以uint64標識,頂點型別、邊型別,以及點邊上的三種屬性均採用字串描述。對於例子中的圖,需要將點邊歸類編號,得到一張可以識別的圖。

圖資料JSON格式

JSON檔案由兩大部分組成,點和邊,分別儲存在JSON物件的“nodes”、“edges”中。每個節點物件包含了節點的id,type,weight以及name,type和value屬性資訊欄位。每個邊物件則包含這個邊相關聯的起點和終點id:src和dst,和邊相關的屬性資訊。

點和邊屬性索引

  • 全域性hash索引:全域性取樣過濾精確匹配某種屬性的點和邊。適用於全域性取樣負例的時候,加上過濾條件,只採樣滿足條件的負例。支援的查詢有:eq,not_eq,in,not_in。
  • 全域性range索引:全域性取樣過濾某種屬性在某個範圍內的點和邊。適用於全域性取樣負例時,加上過濾條件取樣滿足條件的負例。支援:eq,not_eq,ge,le,gt,lt,in,not_in
  • 鄰居索引: 取樣某個點的滿足某種屬性的鄰居節點。適用於鄰居取樣的時候,加上過濾條件,只採樣滿足條件的鄰居節點。支援的查詢與全域性range索引相同。

圖資料二進位制生成

將JSON檔案轉化為分散式圖引擎載入所需要的二進位制格式,包括分片個數等。

… 廣告搜尋場景:圖嵌入,向量化最近鄰檢索網路結構

… …

最 後

當今的計算機以運算器為中心,I/O裝置與儲存器間的資料傳送都要經過運算器。相對處理器的速度來說,IO裝置就慢多了。就SSD來說,IO也是遠低於RAM的讀寫速率。IO讀寫的耗時常常成為效能的瓶頸點,所以要減少IO次數,且隨著資料量增加,IO次數穩定是資料儲存引擎的核心要務。當然了,CPU等指標也是很重要的。

文中倒排索引儲存章節介紹了Lucene如何實現倒排索引的關鍵技術,如何精打細算每一塊記憶體、磁碟空間、如何用詭譎的位運算加快處理速度,但往高處思考,再類比一下MySql就會發現,雖然都是索引,但實現機制卻截然不同。

很多業務、技術上要解決的問題,大都可以抽象為一道演算法題,複雜問題簡單化。每種資料儲存引擎都有自己要解決的問題(或者說擅長的領域),對應的就有自己的資料結構,而不同的使用場景和資料結構,需要引入不同的索引,才能起到最大化加快查詢、檢索的目的。

… ……

 

點選關注,第一時間瞭解華為雲新鮮技