1. 程式人生 > 實用技巧 >初識分散式圖資料庫 Nebula Graph 2.0 Query Engine

初識分散式圖資料庫 Nebula Graph 2.0 Query Engine

摘要:本文主要介紹 Query 層的整體結構,並通過一條 nGQL 語句來介紹其通過 Query 層的四個主要模組的流程。

一、概述

分散式圖資料庫 Nebula Graph 2.0 版本相比 1.0 有較大改動,最明顯的變化便是,在 1.0 版本中 Query、Storage 和 Meta 模組程式碼不作區分放在同一個程式碼倉中,而 Nebula Graph 2.0 開始在架構上先解耦成三個程式碼倉:nebula-graphnebula-commonnebula-storage,其中 nebula-common 中主要是表示式的定義、函式定義和一些公共介面、nebula-graph 主要負責 Query 模組、nebula-storage 主要負責 Storage 和 Meta 模組。

本文主要介紹 Query 層的整體結構,並通過一條 nGQL 語句來介紹其通過 Query 層的四個主要模組的流程,由於 Nebula Graph 2.0 仍處於開發中,版本變化比較頻繁,本文主要針對 2.0 的 nebula-graph 倉中 master 分支的 aea5befd179585c510fb83452cb82276a7756529 版本。

二、框架

Query 層主要框架如下所示:

主要分為 4 個子模組

  • Parser:詞法語法解析模組
  • Validator:語句校驗模組
  • Planner:執行計劃和優化器模組
  • Executor:執行運算元模組

三、程式碼結構

下面講下 nebula-graph 的程式碼層次結構,如下所示

|--src
    |--context     // 校驗期和執行期上下文
    |--daemons 
    |--executor    // 執行運算元
    |--mock
    |--optimizer   // 優化規則
    |--parser      // 詞法語法分析                         
    |--planner     // 執行計劃結構    
    |--scheduler   // 排程器
    |--service
    |--util        // 基礎元件
    |--validator   // 語句校驗    
    |--vistor

四、一個案例聊 Query

自 Nebula Graph v2.0 起,nGQL 的語法規則已經支援起始點的型別為 string,正在相容 1.0 的 int 型別。舉個例子:

GO FROM "Tim" OVER like WHERE like.likeness > 8.0 YIELD like._dst

上面的一條 nGQL 語句在 Nebula Graph 的 Query 層的資料流如下所示:

主要流程如下:

第一階段:生成 AST

第一階段:首先經過 Flex 和 Bison 組成的詞法語法解析器模組 Parser 生成對應的 AST, 結構如下:

在此階段 Parser 會攔截掉不符合語法規則的語句。舉個例子,GO "Tim" FROM OVER like YIELD like._dst 這種語法使用錯誤的語句會在語法解析階段直接被攔截。

第二階段:校驗

第二階段:Validator 在 AST 上進行一系列的校驗工作,主要工作如下:

  • 元資料資訊的校驗

在解析 OVERWHEREYIELD語句時,會查詢 Schema,校驗 edge、tag 的資訊是否存在。或者在 INSERT 資料時校驗插入資料型別和 Schema 中的是否一致

  • 上下文引用校驗

遇到多語句時,例如:$var = GO FROM "Tim" OVER like YIELD like._dst AS ID; GO FROM $var.ID OVER serve YIELD serve._dst ,Validator 會校驗 $var.ID 首先檢查變數 var 是否定義,其次再檢查屬性 ID 是否屬於變數 var, 如果是將 $var.ID 替換為 $var1.ID 或者 $var.IID, 則會校驗失敗。

  • 型別推斷校驗

推斷表示式的結果屬於什麼型別,並根據具體的子句,校驗型別是否正確。比如 WHERE 子句要求結果是 boolnull 或者 empty

  • '*' 展開

例如,若輸入語句為 GO FROM "Tim" OVER * YIELD like._dst, like.likeness, serve._dst,則在校驗 OVER子句時需要查詢 Schema 將 *展開為所有的邊,假如 Schema 中只有 likeserve 兩條邊時,該語句會展開為:GO FROM "Tim" OVER serve, like YIELD like._dst, like.likeness, serve._dst

  • 輸入輸出校驗

遇到 PIPE 語句時,例如:GO FROM "Tim" OVER like YIELD like._dst AS ID | GO FROM $-.ID OVER serve YIELD serve._dst,Validator 會校驗 $-.ID 由於 ID 在上一條語句中已經定義,則該子句合法,如果是將$-.ID 換為 $-.a 而此時 a 未定義,因此該子句非法。

第三階段:生成可執行計劃

第三階段:經過 Validator 之後會生成一個可執行計劃,其中執行計劃的資料結構在 src/planner目錄下,其邏輯結構如下:

Query 執行流

執行流:該執行計劃是一個有向無環圖,其中節點間的依賴關係在 Validator 中每個模組的 toPlan() 函式中確定,在這個例子中 Project 依賴 Filter, Filter 依賴 GetNeighbor,依次類推直到 Start 節點為止。

在執行階段執行器會對每個節點生成一個對應的運算元,並且從根節點(這個例子中是 Project 節點)開始排程,此時發現此節點依賴其他節點,就先遞迴呼叫依賴的節點,一直找到沒有任何依賴的節點(此時為 Start 節點),然後開始執行,執行此節點後,繼續執行此節點被依賴的其他節點(此時為 GetNeighbor 節點),一直到根節點為止。

Query 資料流

資料流:每個節點的輸入輸出也是在 toPlan() 中確定的, 雖然執行的時候會按照執行計劃的先後關係執行,但是每個節點的輸入並不完全依賴上個節點,可以自行定義,因為所有節點的輸入、輸出其實是儲存在一個雜湊表中的,其中 key 是在建立每個節點的時候自己定義的名稱,假如雜湊表的名字為 ResultMap,在建立 Filter 這個節點時,定義該節點從 ResultMap["GN1"]中取資料,然後將結果放入 ResultMap["Filter2"]中,依次類推,將每個節點的輸入輸出都確定好,該雜湊表定義在 nebula-graph 倉下 src/context/ExecutionContext.cpp中,因為執行計劃並不是真正地執行,所以對應雜湊表中每個 key 的 value 值都為空(除了開始節點,此時會將起始資料放入該節點的輸入變數中),其值會在 Excutor 階段被計算並填充。

這個例子比較簡單,最後會放一個複雜點的例子以便更好地理解執行計劃。

第四階段:執行計劃優化

第四階段:執行計劃優化。如果 etc/nebula-graphd.conf 配置檔案中 enable_optimizer 設定為 true,則會對執行計劃的優化,例如上邊的例子,當開啟優化時:

此時會將 Filter 節點融入到 GetNeighbor 節點中,在執行階段當 GetNeighbor 運算元呼叫 Storage 層的介面獲取一個點的鄰邊的時候,Storage 層內部會直接將不符合條件的邊過濾掉,這樣就可以極大的減少資料量的傳輸,俗稱過濾下推。

在執行計劃中,每個節點直接依賴另外一個節點。為了探索等價的變換和重用計劃中相同的部分,會將節點的這種直接依賴關係轉換為 OptGroupNode 與 OptGroup 的依賴。每個 OptGroup 中可以包含等價的 OptGroupNode 的集合,每個 OptGroupNode 都包含執行計劃中的一個節點,同時 OptGroupNode 依賴的不再是 OptGroupNode 而是 OptGroup,這樣從該 OptGroupNode 出發可以根據其依賴 OptGroup 中的不同的 OptGroupNode 拓展出很多等價的執行計劃。同時 OptGroup 還可以被不同的 OptGroupNode 共用,節省儲存的空間。

目前我們實現的所有優化規則認為是 RBO(rule-based optimization),即認為應用規則後的計劃一定比應用前的計劃要優。CBO(cost-based optimization) 目前正在同步開發。整個優化的過程是一個"自底向上"的探索過程,即對於每個規則而言,都會由執行計劃的根節點(此例中是 Project 節點)開始,一步步向下找到最底層的節點,然後由該節點開始一步步向上探索每個 OptGroup 中的 OptGroupNode 是否匹配該規則,直到整個 Plan 都不能再應用該規則為止,再執行下一個規則的探索。

本例中的優化如下圖所示:

例如,當搜尋到 Filter 節點時,發現 Filter 節點的子節點是 GetNeighbors,和規則中事先定義的模式匹配成功,啟動轉換,將 Filter 節點融入到 GetNeighbors 節點中,然後移除掉 Filter 節點,繼續匹配下一個規則。

優化的程式碼在 nebula-graph 倉下 src/optimizer/ 目錄下。

第五階段:執行

第五階段:最後 Scheduler 會根據執行計劃生成對應的執行運算元,從葉子節點開始執行,一直到根節點結束。其結構如下:

其中每一個執行計劃節點都一一對應一個執行運算元節點,其輸入輸出在執行計劃期間已經確定,每個運算元只需要拿到輸入變數中的值然後進行計算,最後將計算結果放入對應的輸出變數中即可,所以只需要從開始節點一步步執行,最後一個運算元的結果會作為最終結果返回給使用者。

五、例項

下面執行一個最短路徑的例項看看執行計劃的具體結構,開啟 nebula-console, 輸入下面語句
FIND SHORTEST PATH FROM "YAO MING" TO "Tim Duncan" OVER like, serve UPTO 5 STEPS
,在這條語句前加 EXPLAIN 關鍵字就可以得到該語句生成的執行計劃詳細資訊:

上圖從左到右依次顯示執行計劃中每個節點的唯一 ID、節點的名稱、該節點所依賴的節點 ID、profiling data(執行 profile 命令時的資訊)、該節點的詳細資訊(包括輸入輸出變數名稱,輸出結果的列名,節點的引數資訊)。

如果想要視覺化一點可以在這條語句前加 EXPLAIN format="dot",這時候 nebula-console 會生成 dot 格式的資料,然後開啟 Graphviz Online 這個網站將生成的 dot 資料貼上上去,就可以看到如下結構,該結構對應著執行階段各個運算元的執行流程。

因為最短路徑使用了雙向廣度搜索演算法分別從"YAO MING""Tim Duncan" 兩邊同時擴充套件,所以中間的 GetNeighborsBFSShortestProjectDedup分別有兩個運算元,通過 PassThrough 運算元連線輸入,由 ConjunctPath 運算元拼接路徑。然後由 LOOP 運算元控制向外擴充套件的步數,可以看到 DataCollect 運算元的輸入其實是從 ConjuctPath 運算元的輸出變數中取值的。

各個運算元的資訊在 nebula-graph 倉下的 src/executor 目錄下。

作者有話說:Hi,我是明泉,是圖資料 Nebula Graph 研發工程師,主要工作和資料庫查詢引擎相關,希望本次的經驗分享能給大家帶來幫助,如有不當之處也希望能幫忙糾正,謝謝~

喜歡這篇文章?來來來,給我們的 GitHub 點個 star 表鼓勵啦~~