初識分佈式圖數據庫 Nebula Graph 2.0 Query Engine

初識 Nebula Graph 2.0 Query Engine

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

1、概述

分佈式圖數據庫 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 模塊。github

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

2、框架

Query 層主要框架以下所示:數據庫

初識 Nebula Graph 2.0 Query Engine

主要分爲 4 個子模塊微信

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

3、代碼結構

下面講下 nebula-graph 的代碼層次結構,以下所示數據結構

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

4、一個案例聊 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 層的數據流以下所示:框架

初識 Nebula Graph 2.0 Query Engine

主要流程以下:分佈式

第一階段:生成 AST

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

初識 Nebula Graph 2.0 Query Engine

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

第二階段:校驗

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

  • 元數據信息的校驗

在解析 OVER 、 WHERE 和 YIELD 語句時,會查找 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 目錄下,其邏輯結構以下:

初識 Nebula Graph 2.0 Query Engine

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 ,則會對執行計劃的優化,例如上邊的例子,當開啓優化時:

初識 Nebula Graph 2.0 Query Engine

此時會將 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 都不能再應用該規則爲止,再執行下一個規則的探索。

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

初識 Nebula Graph 2.0 Query Engine

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

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

第五階段:執行

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

初識 Nebula Graph 2.0 Query Engine

其中每個執行計劃節點都一一對應一個執行算子節點,其輸入輸出在執行計劃期間已經肯定,每一個算子只須要拿到輸入變量中的值而後進行計算,最後將計算結果放入對應的輸出變量中便可,因此只須要從開始節點一步步執行,最後一個算子的結果會做爲最終結果返回給用戶。

5、實例

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

初識 Nebula Graph 2.0 Query Engine

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

若是想要可視化一點能夠在這條語句前加 EXPLAIN format="dot",這時候 nebula-console 會生成 dot 格式的數據,而後打開 Graphviz Online 這個網站將生成的 dot 數據粘貼上去,就能夠看到以下結構,該結構對應着執行階段各個算子的執行流程。

初識 Nebula Graph 2.0 Query Engine

由於最短路徑使用了雙向廣度搜索算法分別從"YAO MING""Tim Duncan" 兩邊同時擴展,因此中間的 GetNeighborsBFSShortestProjectDedup 分別有兩個算子,經過 PassThrough 算子鏈接輸入,由 ConjunctPath 算子拼接路徑。而後由 LOOP 算子控制向外擴展的步數,能夠看到 DataCollect 算子的輸入實際上是從 ConjuctPath 算子的輸出變量中取值的。

各個算子的信息在 nebula-graph 倉下的 src/executor 目錄下。

做者有話說:Hi,我是明泉,是圖數據 Nebula Graph 研發工程師,主要工做和數據庫查詢引擎相關,但願本次的經驗分享能給你們帶來幫助,若有不當之處也但願能幫忙糾正,謝謝~

喜歡這篇文章?來來來,給咱們的 GitHub 點個 star 表鼓勵啦~~ ????‍♂️????‍♀️ [手動跪謝]

交流圖數據庫技術?交個朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你進交流羣~~

推薦閱讀

相關文章
相關標籤/搜索