1. 程式人生 > >分散式圖資料庫 Nebula Graph 的 Index 實踐

分散式圖資料庫 Nebula Graph 的 Index 實踐

![](https://oscimg.oschina.net/oscnet/up-58d405e2714878019a2538ef619d42e5b3e.png) ## 導讀 索引是資料庫系統中不可或缺的一個功能,資料庫索引好比是書的目錄,能加快資料庫的查詢速度,其實質是資料庫管理系統中一個排序的資料結構。不同的資料庫系統有不同的排序結構,目前常見的索引實現型別如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各種索引型別都有各自的排序演算法。 雖然索引可以帶來更高的查詢效能,但是也存在一些缺點,例如: - 建立索引和維護索引要耗費額外的時間,往往是隨著資料量的增加而維護成本增大 - 索引需要佔用物理空間 - 在對資料進行增刪改的操作時需要耗費更多的時間,因為索引也要進行同步的維護 Nebula Graph 作為一個高效能的分散式圖資料庫,對於屬性值的高效能查詢,同樣也實現了索引功能。本文將對 Nebula Graph的索引功能做一個詳細介紹。 ## 圖資料庫 Nebula Graph 術語 開始之前,這裡羅列一些可能會使用到的圖資料庫和 Nebula Graph 專有術語: - Tag:點的屬性結構,一個 Vertex 可以附加多種 tag,以 TagID 標識。(如果類比 SQL,可以理解為一張點表) - Edge:類似於 Tag,EdgeType 是邊上的屬性結構,以 EdgeType 標識。(如果類比 SQL,可以理解為一張邊表) - Property:tag / edge 上的屬性值,其資料型別由 tag / edge 的結構確定。 - Partition:Nebula Graph 的最小邏輯儲存單元,一個 StorageEngine 可包含多個 Partition。Partition 分為 leader 和 follower 的角色,Raftex 保證了 leader 和 follower 之間的資料一致性。 - Graph space:每個 Graph Space 是一個獨立的業務 Graph 單元,每個 Graph Space 有其獨立的 tag 和 edge 集合。一個 Nebula Graph 叢集中可包含多個 Graph Space。 - Index:本文中出現的 Index 指 nebula graph 中點和邊上的屬性索引。其資料型別依賴於 tag / edge。 - TagIndex:基於 tag 建立的索引,一個 tag 可以建立多個索引。目前(2020.3)暫不支援跨 tag 的複合索引,因此一個索引只可以基於一個 tag。 - EdgeIndex:基於 Edge 建立的索引。同樣,一個 Edge 可以建立多個索引,但一個索引只可以基於一個 edge。 - Scan Policy:Index 的掃描策略,往往一條查詢語句可以有多種索引的掃描方式,但具體使用哪種掃描方式需要 Scan Policy 來決定。 - Optimizer:對查詢條件進行優化,例如對 where 子句的表示式樹進行子表示式節點的排序、分裂、合併等。其目的是獲取更高的查詢效率。 ## 索引需求分析 Nebula Graph 是一個圖資料庫系統,查詢場景一般是由一個點出發,找出指定邊型別的相關點的集合,以此類推進行(廣度優先遍歷)N 度查詢。另一種查詢場景是給定一個屬性值,找出符合這個屬性值的所有的點或邊。在後面這種場景中,需要對屬性值進行高效能的掃描,查出與此屬性值對應的邊或點,以及邊或點上的其它屬性。為了提高屬性值的查詢效率,在這裡引入了索引的功能。對邊或點的屬性值進行排序,以便快速的定位到某個屬性上。以此避免了全表掃描。 可以看到對圖資料庫 Nebula Graph 的索引要求: - 支援 tag 和 edge 的屬性索引 - 支援索引的掃描策略的分析和生成 - 支援索引的管理,如:新建索引、重建索引、刪除索引、list | show 索引等。 ## 系統架構概覽 ### 圖資料庫 Nebula Graph 儲存架構 ![](https://oscimg.oschina.net/oscnet/up-c85a468a9c7aad264c38686f254380fe7e1.png) 從架構圖可以看到,每個Storage Server 中可以包含多個 Storage Engine, 每個 Storage Engine中可以包含多個Partition, 不同的Partition之間通過 Raft 協議進行一致性同步。每個 Partition 中既包含了 data,也包含了 index,同一個點或邊的 data 和 index 將被儲存到同一個 Partition 中。 ## 業務具體分析 ### 資料儲存結構 為了更好的描述索引的儲存結構,這裡將圖資料庫 Nebula Graph 原始資料的儲存結構一起拿出來分析下。 #### 點的儲存結構 ##### 點的 Data 結構 ![](https://oscimg.oschina.net/oscnet/up-9cf8025c50fce1a348e4c93e990a80b3455.png) ##### 點的 Index 結構 ![](https://oscimg.oschina.net/oscnet/up-36d4069ce9791b79e4a8518a3d6f010eb4f.png) Vertex 的索引結構如上表所示,下面來詳細地講述下欄位: **PartitionId**:一個點的資料和索引在邏輯上是存放到同一個分割槽中的。之所以這麼做的原因主要有兩點: 1. 當掃描索引時,根據索引的 key 能快速地獲取到同一個分割槽中的點 data,這樣就可以方便地獲取這個點的任何一種屬性值,即使這個屬性列不屬於本索引。 1. 目前 edge 的儲存是由起點的 ID Hash 分佈,換句話說,一個點的出邊儲存在哪是由該點的 VertexId 決定的,這個點和它的出邊如果被儲存到同一個 partition 中,點的索引掃描能快速地定位該點的出邊。 **IndexId**:index 的識別碼,通過 indexId 可獲取指定 index 的元資料資訊,例如:index 所關聯的 TagId,index 所在列的資訊。 **Index binary**:index 的核心儲存結構,是所有 index 相關列屬性值的位元組編碼,詳細結構將在本文的 #Index binary# 章節中講解。 **VertexId**:點的識別碼,在實際的 data 中,一個點可能會有不同 version 的多行資料。但是在 index 中,**index 沒有 Version 的概念,index 始終與最新 Version 的 Tag 所對應**。 上面講完欄位,我們來簡單地實踐分析一波: 假設 _PartitionId_ 為 _100,TagId 有 tag_1 _和_ tag_2,_其中 _tag_1_ 包含三列 :col_t1_1、col_t1_2、col_t1_3,_tag_2_ 包含兩列:col_t2_1、col_t2_2。 現在我們來建立索引: - i1 = tag_1 (col_t1_1, col_t1_2) ,假設 i1 的 ID 為 1; - i2 = tag_2(col_t2_1, col_t2_2),  假設 i2 的 ID 為 2; 可以看到雖然 tag_1 中有 col_t1_3 這列,但是建立索引的時候並沒有使用到 col_t1_3,**因為在圖資料庫 Nebula Graph 中索引可以基於 Tag 的一列或多列進行建立**。 ##### 插入點 ```cpp // VertexId = hash("v_t1_1"),假如為 50 INSERT VERTEX tag_1(col_t1_1, col_t1_2, col_t1_3), tag_2(col_t2_1, col_t2_2) \ VALUES hash("v_t1_1"):("v_t1_1", "v_t1_2", "v_t1_3", "v_t2_1", "v_t2_2"); ``` 從上可以看到 VertexId 可由 ID 標識對應的數值經過 Hash 得到,如果標識對應的數值本身已經為 int64,則無需進行 Hash 或者其他轉化數值為 int64 的運算。而此時資料儲存如下: **此時點的 Data 結構** ![](https://oscimg.oschina.net/oscnet/up-51192c519b07a2ff4ba00b39ba9b90bf120.png) **此時點的 Index 結構** ![](https://oscimg.oschina.net/oscnet/up-a4fd2ed463d0303e8ff4df4c3187b62eeb2.png) 說明:index 中 row 和 key 是一個概念,為索引的唯一標識; #### 邊的儲存結構 邊的索引結構和點索引結構原理類似,這裡不再贅述。但有一點需要說明,為了使索引 key 的唯一性成立,索引的 key 的生成藉助了不少 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,這也是為什麼點索引中並沒有 TagId 欄位(邊索引中也沒有 EdgeType 欄位),這是因為** IndexId 本身帶有 VertexId 等資訊可直接區分具體的 tagId 或 EdgeType**。 ##### 邊的 Data 結構 ![](https://oscimg.oschina.net/oscnet/up-25135a07713cc6b652b73ecd4731d8a93ef.png) ##### 邊的 Index 結構 ![](https://oscimg.oschina.net/oscnet/up-aa992a8280c8a7bef85ea4bae2286d0fbcb.png) ### Index binary 介紹 ![](https://oscimg.oschina.net/oscnet/up-e6e83f6439a88652e6fdb0a3fba73b4cb75.png) Index binary 是 index 的核心欄位,在 index binary 中區分定長欄位和不定長欄位,int、double、bool 為定長欄位,string 則為不定長欄位。由於** index binary 是將所有 index column 的屬性值編碼連線儲存**,為了精確地定位不定長欄位,Nebula Graph 在 index binary 末尾用 int32 記錄了不定長欄位的長度。 舉個例子: 我們現在有一個 index binary 為 index1,是由 int 型別的索引列1 c1、string 型別的索引列 c2,string 型別的索引列 c3 組成: ```bash index1 (c1:int, c2:string, c3:string) ``` 假如索引列 c1、c2、c3 某一行對應的 property 值分別為:23、"abc"、"here",則在 index1 中這些索引列將被儲存為如下(在示例中為了便於理解,我們直接用原值,實際儲存中是原值會經過編碼再儲存): - length = sizeof("abc") = 3 - length = sizeof("here") = 4 ![](https://oscimg.oschina.net/oscnet/up-e572425d738f599277c69d4617460d38273.png) 所以 index1 該 row 對應的 key 則為 23abchere34; 回到我們 Index binary 章節開篇說的 index binary 格式中存在 `Variable-length field lenght` 欄位,那麼這個欄位的的具體作用是什麼呢?我們來簡單地舉個例: 現在我們又有了一個 index binary,我們給它取名為 index2,它由 string 型別的索引列1 c1、string 型別的索引列 c2,string 型別的索引列 c3 組成: ```bash index2 (c1:string, c2:string, c3:string) ``` 假設我們現在 c1、c2、c3 分別有兩組如下的數值: - row1 : ("ab", "ab", "ab") - row2: ("aba", "ba", "b") ![](https://oscimg.oschina.net/oscnet/up-06fc405dc2cfdd18fdf9834350bdb8a76e6.png) 可以看到這兩行的 prefix(上圖紅色部分)是相同,都是 "ababab",這時候怎麼區分這兩個 row 的 index binary 的 key 呢?別擔心,我們有 `Variable-length field lenght` 。 ![](https://oscimg.oschina.net/oscnet/up-a66e23a0f4522bb04da06a40d7df45f47d6.png) 若遇到 where c1 == "ab" 這樣的條件查詢語句,在 Variable-length field length 中可直接根據順序讀取出 c1 的長度,再根據這個長度取出 row1 和 row2 中 c1 的值,分別是 "ab" 和 "aba" ,這樣我們就精準地判斷出只有 row1 中的 "ab" 是符合查詢條件的。 ### 索引的處理邏輯 #### Index write 當 Tag / Edge中的一列或多列建立了索引後,一旦涉及到 Tag / Edge 相關的寫操作時,對應的索引必須連同資料一起被修改。下面將對索引的write操作在storage層的處理邏輯進行簡單介紹: ##### INSERT——插入資料 當用戶產生插入點/邊操作時,insertProcessor 首先會判斷所插入的資料是否有存在索引的 Tag 屬性 / Edge 屬性。如果沒有關聯的屬性列索引,則按常規方式生成新 Version,並將資料 put 到 Storage Engine;如果有關聯的屬性列索引,則通過原子操作寫入 Data 和 Index,並判斷當前的 Vertex / Edge 是否有舊的屬性值,如果有,則一併在原子操作中刪除舊屬性值。 ##### DELETE——刪除資料 當用戶發生 Drop Vertex / Edge 操作時,deleteProcessor 會將 Data 和 Index(如果存在)一併刪除,在刪除的過程中同樣需要使用原子操作。 ##### UPDATE——更新資料 Vertex / Edge 的更新操作對於 Index 來說,則是 drop 和 insert 的操作:刪除舊的索引,插入新的索引,為了保證資料的一致性,同樣需要在原子操作中進行。但是對應普通的 Data 來說,僅僅是 insert 操作,使用最新 Version 的 Data 覆蓋舊 Version 的 data 即可。 #### Index scan 在圖資料庫 Nebula Graph 中是用 `LOOKUP` 語句來處理 index scan 操作的,`LOOKUP` 語句可通過屬性值作為判斷條件,查出所有符合條件的點/邊,同樣 `LOOKUP` 語句支援 `WHERE` 和 `YIELD` 子句。  ###### LOOKUP 使用技巧 正如根據本文#資料儲存結構#章節所描述那樣,index 中的索引列是按照建立 index 時的列順序決定。 舉個例子,我們現在有 tag (col1, col2),根據這個 tag 我們可以建立不同的索引,例如: - index1 on tag(col1) - index2 on tag(col2) - index3 on tag(col1, col2) - index4 on tag(col2, col1) 我們可以對 clo1、col2 建立多個索引,但在 scan index 時,上述四個 index 返回結果存在差異,甚至是完全不同,在實際業務中具體使用哪個 index,及 index 的最優執行策略,則是通過索引優化器決定。 下面我們再來根據剛才 4 個 index 的例子深入分析一波: ```bash lookup on tag where tag.col1 ==1 # 最優的 index 是 index1 lookup on tag where tag.col2 == 2 # 最優的 index 是index2 lookup on tag where tag.col1 > 1 and tag.col2 == 1 # index3 和 index4 都是有效的 index,而 index1 和 index2 則無效 ``` 在上述第三個例子中,index3 和 index4 都是有效 index,但最終必須要從兩者中選出來一個作為 index,根據優化規則,因為 tag.col2 == 1 是一個**等價查詢**,因此**優先使用** tag.col2 會更高效,所以優化器應該選出 index4 為最優 index。 ## 實操一下圖資料庫 Nebula Graph 索引 在這部分我們就不具體講解某個語句的用途是什麼了,如果你對語句不清楚的話可以去圖資料庫 Nebula Graph 的官方論壇進行提問:[https://discuss.nebula-graph.io/](https://discuss.nebula-graph.io/) ### CREATE——索引的建立 ```cpp ([email protected]:6999) [(none)]> CREATE SPACE my_space(partition_num=3, replica_factor=1); Execution succeeded (Time spent: 15.566/16.602 ms) Thu Feb 20 12:46:38 2020 ([email protected]:6999) [(none)]> USE my_space; Execution succeeded (Time spent: 7.681/8.303 ms) Thu Feb 20 12:46:51 2020 ([email protected]:6999) [my_space]> CREATE TAG lookup_tag_1(col1 string, col2 string, col3 string); Execution succeeded (Time spent: 12.228/12.931 ms) Thu Feb 20 12:47:05 2020 ([email protected]:6999) [my_space]> CREATE TAG INDEX t_index_1 ON lookup_tag_1(col1, col2, col3); Execution succeeded (Time spent: 1.639/2.271 ms) Thu Feb 20 12:47:22 2020 ``` ### DROP——刪除索引 ```cpp ([email protected]:6999) [my_space]> DROP TAG INDEX t_index_1; Execution succeeded (Time spent: 4.147/5.192 ms) Sat Feb 22 11:30:35 2020 ``` ### REBUILD——重建索引 如果你是從較老版本的 Nebula Graph 升級上來,或者用 Spark Writer 批量寫入過程中(為了效能)沒有開啟索引,那麼這些資料還沒有建立過索引,這時可以使用 REBUILD INDEX 命令來重新全量建立一次索引。這個過程可能會耗時比較久,在 rebuild index 完成前,客戶端的讀寫速度都會變慢。 ```cpp REBUILD {TAG | EDGE