您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關(guān)怎么實現(xiàn)分布式圖數(shù)據(jù)庫Nebula Graph 的Index實踐,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
索引是數(shù)據(jù)庫系統(tǒng)中不可或缺的一個功能,數(shù)據(jù)庫索引好比是書的目錄,能加快數(shù)據(jù)庫的查詢速度,其實質(zhì)是數(shù)據(jù)庫管理系統(tǒng)中一個排序的數(shù)據(jù)結(jié)構(gòu)。不同的數(shù)據(jù)庫系統(tǒng)有不同的排序結(jié)構(gòu),目前常見的索引實現(xiàn)類型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各種索引類型都有各自的排序算法。
雖然索引可以帶來更高的查詢性能,但是也存在一些缺點,例如:
創(chuàng)建索引和維護索引要耗費額外的時間,往往是隨著數(shù)據(jù)量的增加而維護成本增大
索引需要占用物理空間
在對數(shù)據(jù)進行增刪改的操作時需要耗費更多的時間,因為索引也要進行同步的維護
Nebula Graph 作為一個高性能的分布式圖數(shù)據(jù)庫,對于屬性值的高性能查詢,同樣也實現(xiàn)了索引功能。本文將對 Nebula Graph的索引功能做一個詳細介紹。
開始之前,這里羅列一些可能會使用到的圖數(shù)據(jù)庫和 Nebula Graph 專有術(shù)語:
Tag:點的屬性結(jié)構(gòu),一個 Vertex 可以附加多種 tag,以 TagID 標識。(如果類比 SQL,可以理解為一張點表)
Edge:類似于 Tag,EdgeType 是邊上的屬性結(jié)構(gòu),以 EdgeType 標識。(如果類比 SQL,可以理解為一張邊表)
Property:tag / edge 上的屬性值,其數(shù)據(jù)類型由 tag / edge 的結(jié)構(gòu)確定。
Partition:Nebula Graph 的最小邏輯存儲單元,一個 StorageEngine 可包含多個 Partition。Partition 分為 leader 和 follower 的角色,Raftex 保證了 leader 和 follower 之間的數(shù)據(jù)一致性。
Graph space:每個 Graph Space 是一個獨立的業(yè)務(wù) Graph 單元,每個 Graph Space 有其獨立的 tag 和 edge 集合。一個 Nebula Graph 集群中可包含多個 Graph Space。
Index:本文中出現(xiàn)的 Index 指 nebula graph 中點和邊上的屬性索引。其數(shù)據(jù)類型依賴于 tag / edge。
TagIndex:基于 tag 創(chuàng)建的索引,一個 tag 可以創(chuàng)建多個索引。目前(2020.3)暫不支持跨 tag 的復(fù)合索引,因此一個索引只可以基于一個 tag。
EdgeIndex:基于 Edge 創(chuàng)建的索引。同樣,一個 Edge 可以創(chuàng)建多個索引,但一個索引只可以基于一個 edge。
Scan Policy:Index 的掃描策略,往往一條查詢語句可以有多種索引的掃描方式,但具體使用哪種掃描方式需要 Scan Policy 來決定。
Optimizer:對查詢條件進行優(yōu)化,例如對 where 子句的表達式樹進行子表達式節(jié)點的排序、分裂、合并等。其目的是獲取更高的查詢效率。
Nebula Graph 是一個圖數(shù)據(jù)庫系統(tǒng),查詢場景一般是由一個點出發(fā),找出指定邊類型的相關(guān)點的集合,以此類推進行(廣度優(yōu)先遍歷)N 度查詢。另一種查詢場景是給定一個屬性值,找出符合這個屬性值的所有的點或邊。在后面這種場景中,需要對屬性值進行高性能的掃描,查出與此屬性值對應(yīng)的邊或點,以及邊或點上的其它屬性。為了提高屬性值的查詢效率,在這里引入了索引的功能。對邊或點的屬性值進行排序,以便快速的定位到某個屬性上。以此避免了全表掃描。
可以看到對圖數(shù)據(jù)庫 Nebula Graph 的索引要求:
支持 tag 和 edge 的屬性索引
支持索引的掃描策略的分析和生成
支持索引的管理,如:新建索引、重建索引、刪除索引、list | show 索引等。
從架構(gòu)圖可以看到,每個Storage Server 中可以包含多個 Storage Engine, 每個 Storage Engine中可以包含多個Partition, 不同的Partition之間通過 Raft 協(xié)議進行一致性同步。每個 Partition 中既包含了 data,也包含了 index,同一個點或邊的 data 和 index 將被存儲到同一個 Partition 中。
為了更好的描述索引的存儲結(jié)構(gòu),這里將圖數(shù)據(jù)庫 Nebula Graph 原始數(shù)據(jù)的存儲結(jié)構(gòu)一起拿出來分析下。
Vertex 的索引結(jié)構(gòu)如上表所示,下面來詳細地講述下字段:
PartitionId:一個點的數(shù)據(jù)和索引在邏輯上是存放到同一個分區(qū)中的。之所以這么做的原因主要有兩點:
當掃描索引時,根據(jù)索引的 key 能快速地獲取到同一個分區(qū)中的點 data,這樣就可以方便地獲取這個點的任何一種屬性值,即使這個屬性列不屬于本索引。
目前 edge 的存儲是由起點的 ID Hash 分布,換句話說,一個點的出邊存儲在哪是由該點的 VertexId 決定的,這個點和它的出邊如果被存儲到同一個 partition 中,點的索引掃描能快速地定位該點的出邊。
IndexId:index 的識別碼,通過 indexId 可獲取指定 index 的元數(shù)據(jù)信息,例如:index 所關(guān)聯(lián)的 TagId,index 所在列的信息。
Index binary:index 的核心存儲結(jié)構(gòu),是所有 index 相關(guān)列屬性值的字節(jié)編碼,詳細結(jié)構(gòu)將在本文的 #Index binary# 章節(jié)中講解。
VertexId:點的識別碼,在實際的 data 中,一個點可能會有不同 version 的多行數(shù)據(jù)。但是在 index 中,index 沒有 Version 的概念,index 始終與最新 Version 的 Tag 所對應(yīng)。
上面講完字段,我們來簡單地實踐分析一波:
假設(shè) PartitionId 為 100,TagId 有 tag_1 和 tag_2,其中 tag_1 包含三列 :colt1_1、col_t1_2、col_t1_3,_tag_2 包含兩列:col_t2_1、col_t2_2。
現(xiàn)在我們來創(chuàng)建索引:
i1 = tag_1 (col_t1_1, col_t1_2) ,假設(shè) i1 的 ID 為 1;
i2 = tag_2(col_t2_1, col_t2_2), 假設(shè) i2 的 ID 為 2;
可以看到雖然 tag_1 中有 col_t1_3 這列,但是建立索引的時候并沒有使用到 col_t1_3,因為在圖數(shù)據(jù)庫 Nebula Graph 中索引可以基于 Tag 的一列或多列進行創(chuàng)建。
// 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 標識對應(yīng)的數(shù)值經(jīng)過 Hash 得到,如果標識對應(yīng)的數(shù)值本身已經(jīng)為 int64,則無需進行 Hash 或者其他轉(zhuǎn)化數(shù)值為 int64 的運算。而此時數(shù)據(jù)存儲如下:
此時點的 Data 結(jié)構(gòu)
此時點的 Index 結(jié)構(gòu)
說明:index 中 row 和 key 是一個概念,為索引的唯一標識;
邊的索引結(jié)構(gòu)和點索引結(jié)構(gòu)原理類似,這里不再贅述。但有一點需要說明,為了使索引 key 的唯一性成立,索引的 key 的生成借助了不少 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,這也是為什么點索引中并沒有 TagId 字段(邊索引中也沒有 EdgeType 字段),這是因為 IndexId 本身帶有 VertexId 等信息可直接區(qū)分具體的 tagId 或 EdgeType。
Index binary 是 index 的核心字段,在 index binary 中區(qū)分定長字段和不定長字段,int、double、bool 為定長字段,string 則為不定長字段。由于 index binary 是將所有 index column 的屬性值編碼連接存儲,為了精確地定位不定長字段,Nebula Graph 在 index binary 末尾用 int32 記錄了不定長字段的長度。
舉個例子:
我們現(xiàn)在有一個 index binary 為 index1,是由 int 類型的索引列1 c1、string 類型的索引列 c2,string 類型的索引列 c3 組成:
index1 (c1:int, c2:string, c3:string)
假如索引列 c1、c2、c3 某一行對應(yīng)的 property 值分別為:23、”abc”、”here”,則在 index1 中這些索引列將被存儲為如下(在示例中為了便于理解,我們直接用原值,實際存儲中是原值會經(jīng)過編碼再存儲):
length = sizeof(“abc”) = 3
length = sizeof(“here”) = 4
所以 index1 該 row 對應(yīng)的 key 則為 23abchere34;
回到我們 Index binary 章節(jié)開篇說的 index binary 格式中存在
Variable-length field lenght
字段,那么這個字段的的具體作用是什么呢?我們來簡單地舉個例:
現(xiàn)在我們又有了一個 index binary,我們給它取名為 index2,它由 string 類型的索引列1 c1、string 類型的索引列 c2,string 類型的索引列 c3 組成:
index2 (c1:string, c2:string, c3:string)
假設(shè)我們現(xiàn)在 c1、c2、c3 分別有兩組如下的數(shù)值:
row1 : (“ab”, “ab”, “ab”)
row2: (“aba”, “ba”, “b”)
可以看到這兩行的 prefix(上圖紅色部分)是相同,都是 “ababab”,這時候怎么區(qū)分這兩個 row 的 index binary 的 key 呢?別擔心,我們有
Variable-length field lenght
。
若遇到 where c1 == “ab” 這樣的條件查詢語句,在 Variable-length field length 中可直接根據(jù)順序讀取出 c1 的長度,再根據(jù)這個長度取出 row1 和 row2 中 c1 的值,分別是 “ab” 和 “aba” ,這樣我們就精準地判斷出只有 row1 中的 “ab” 是符合查詢條件的。
當 Tag / Edge中的一列或多列創(chuàng)建了索引后,一旦涉及到 Tag / Edge 相關(guān)的寫操作時,對應(yīng)的索引必須連同數(shù)據(jù)一起被修改。下面將對索引的write操作在storage層的處理邏輯進行簡單介紹:
當用戶產(chǎn)生插入點/邊操作時,insertProcessor 首先會判斷所插入的數(shù)據(jù)是否有存在索引的 Tag 屬性 / Edge 屬性。如果沒有關(guān)聯(lián)的屬性列索引,則按常規(guī)方式生成新 Version,并將數(shù)據(jù) put 到 Storage Engine;如果有關(guān)聯(lián)的屬性列索引,則通過原子操作寫入 Data 和 Index,并判斷當前的 Vertex / Edge 是否有舊的屬性值,如果有,則一并在原子操作中刪除舊屬性值。
當用戶發(fā)生 Drop Vertex / Edge 操作時,deleteProcessor 會將 Data 和 Index(如果存在)一并刪除,在刪除的過程中同樣需要使用原子操作。
Vertex / Edge 的更新操作對于 Index 來說,則是 drop 和 insert 的操作:刪除舊的索引,插入新的索引,為了保證數(shù)據(jù)的一致性,同樣需要在原子操作中進行。但是對應(yīng)普通的 Data 來說,僅僅是 insert 操作,使用最新 Version 的 Data 覆蓋舊 Version 的 data 即可。
在圖數(shù)據(jù)庫 Nebula Graph 中是用
LOOKUP
語句來處理 index scan 操作的,LOOKUP
語句可通過屬性值作為判斷條件,查出所有符合條件的點/邊,同樣
LOOKUP
語句支持
WHERE
和
YIELD
子句。
正如根據(jù)本文#數(shù)據(jù)存儲結(jié)構(gòu)#章節(jié)所描述那樣,index 中的索引列是按照創(chuàng)建 index 時的列順序決定。
舉個例子,我們現(xiàn)在有 tag (col1, col2),根據(jù)這個 tag 我們可以創(chuàng)建不同的索引,例如:
index1 on tag(col1)
index2 on tag(col2)
index3 on tag(col1, col2)
index4 on tag(col2, col1)
我們可以對 clo1、col2 建立多個索引,但在 scan index 時,上述四個 index 返回結(jié)果存在差異,甚至是完全不同,在實際業(yè)務(wù)中具體使用哪個 index,及 index 的最優(yōu)執(zhí)行策略,則是通過索引優(yōu)化器決定。
下面我們再來根據(jù)剛才 4 個 index 的例子深入分析一波:
lookup on tag where tag.col1 ==1 # 最優(yōu)的 index 是 index1 lookup on tag where tag.col2 == 2 # 最優(yōu)的 index 是index2 lookup on tag where tag.col1 > 1 and tag.col2 == 1 # index3 和 index4 都是有效的 index,而 index1 和 index2 則無效
在上述第三個例子中,index3 和 index4 都是有效 index,但最終必須要從兩者中選出來一個作為 index,根據(jù)優(yōu)化規(guī)則,因為 tag.col2 == 1 是一個等價查詢,因此優(yōu)先使用 tag.col2 會更高效,所以優(yōu)化器應(yīng)該選出 index4 為最優(yōu) index。
在這部分我們就不具體講解某個語句的用途是什么了,如果你對語句不清楚的話可以去圖數(shù)據(jù)庫 Nebula Graph 的官方論壇進行提問: https://discuss.nebula-graph.io/
(user@127.0.0.1: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 (user@127.0.0.1:6999) [(none)]> USE my_space; Execution succeeded (Time spent: 7.681/8.303 ms) Thu Feb 20 12:46:51 2020 (user@127.0.0.1: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 (user@127.0.0.1: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
(user@127.0.0.1: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
如果你是從較老版本的 Nebula Graph 升級上來,或者用 Spark Writer 批量寫入過程中(為了性能)沒有打開索引,那么這些數(shù)據(jù)還沒有建立過索引,這時可以使用 REBUILD INDEX 命令來重新全量建立一次索引。這個過程可能會耗時比較久,在 rebuild index 完成前,客戶端的讀寫速度都會變慢。
REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE]
需要說明一下,使用 LOOKUP 語句前,請確保已經(jīng)建立過索引(CREATE INDEX 或 REBUILD INDEX)。
(user@127.0.0.1:6999) [my_space]> INSERT VERTEX lookup_tag_1(col1, col2, col3) VALUES 200:("col1_200", "col2_200", "col3_200"), 201:("col1_201", "col2_201", "col3_201"), 202:("col1_202", "col2_202", "col3_202"); Execution succeeded (Time spent: 18.185/19.267 ms) Thu Feb 20 12:49:44 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200"; ============ | VertexID | ============ | 200 | ------------ Got 1 rows (Time spent: 12.001/12.64 ms) Thu Feb 20 12:49:54 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200" YIELD lookup_tag_1.col1, lookup_tag_1.col2, lookup_tag_1.col3; ======================================================================== | VertexID | lookup_tag_1.col1 | lookup_tag_1.col2 | lookup_tag_1.col3 | ======================================================================== | 200 | col1_200 | col2_200 | col3_200 | ------------------------------------------------------------------------ Got 1 rows (Time spent: 3.679/4.657 ms) Thu Feb 20 12:50:36 2020
關(guān)于怎么實現(xiàn)分布式圖數(shù)據(jù)庫Nebula Graph 的Index實踐就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。