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

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

![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/Query-Engine.png) 摘要:本文主要介紹 Query 層的整體結構,並通過一條 nGQL 語句來介紹其通過 Query 層的四個主要模組的流程。 ## 一、概述 分散式圖資料庫 Nebula Graph 2.0 版本相比 1.0 有較大改動,最明顯的變化便是,在 1.0 版本中 Query、Storage 和 Meta 模組程式碼不作區分放在同一個程式碼倉中,而 Nebula Graph 2.0 開始在架構上先解耦成三個程式碼倉:[nebula-graph](https://github.com/vesoft-inc/nebula-graph)、[nebula-common](https://github.com/vesoft-inc/nebula-common) 和 [nebula-storage](https://github.com/vesoft-inc/nebula-storage),其中 nebula-common 中主要是表示式的定義、函式定義和一些公共介面、nebula-graph 主要負責 Query 模組、nebula-storage 主要負責 Storage 和 Meta 模組。 本文主要介紹 Query 層的整體結構,並通過一條 nGQL 語句來介紹其通過 Query 層的四個主要模組的流程,由於 Nebula Graph 2.0 仍處於開發中,版本變化比較頻繁,本文主要針對 2.0 的 nebula-graph 倉中 master 分支的 aea5befd179585c510fb83452cb82276a7756529 版本。 ## 二、框架 Query 層主要框架如下所示: ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/query-engine-architecture.png) 主要分為 4 個子模組 - Parser:詞法語法解析模組 - Validator:語句校驗模組 - Planner:執行計劃和優化器模組 - Executor:執行運算元模組 ## 三、程式碼結構 下面講下 nebula-graph 的程式碼層次結構,如下所示 ```markdown |--src |--context // 校驗期和執行期上下文 |--daemons |--executor // 執行運算元 |--mock |--optimizer // 優化規則 |--parser // 詞法語法分析 |--planner // 執行計劃結構 |--scheduler // 排程器 |--service |--util // 基礎元件 |--validator // 語句校驗 |--vistor ``` ## 四、一個案例聊 Query 自 Nebula Graph v2.0 起,nGQL 的語法規則已經支援起始點的型別為 `string` ,正在相容 1.0 的 `int` 型別。舉個例子: ```cpp GO FROM "Tim" OVER like WHERE like.likeness > 8.0 YIELD like._dst ``` 上面的一條 nGQL 語句在 Nebula Graph 的 Query 層的資料流如下所示: ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/query-engine-data-flow.png) 主要流程如下: ### 第一階段:生成 AST 第一階段:首先經過 Flex 和 Bison 組成的詞法語法解析器模組 Parser 生成對應的 AST, 結構如下:  ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/parser-ast-tree.png) 在此階段 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` 子句要求結果是 `bool`,`null` 或者 `empty` 。 - **'*' 展開** 例如,若輸入語句為 `GO FROM "Tim" OVER * YIELD like._dst, like.likeness, serve._dst`,則在校驗 `OVER` 子句時需要查詢 Schema 將 `*` 展開為所有的邊,假如 Schema 中只有 _like_ 和 _serve_ 兩條邊時,該語句會展開為:`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](https://www-cdn.nebula-graph.com.cn/nebula-blog/exe-planner.png) #### Query 執行流 執行流:該執行計劃是一個有向無環圖,其中節點間的依賴關係在 Validator 中每個模組的 [toPlan()](https://github.com/vesoft-inc/nebula-graph/blob/master/src/validator/PipeValidator.cpp) 函式中確定,在這個例子中 Project 依賴 Filter, Filter 依賴 GetNeighbor,依次類推直到 Start 節點為止。 在執行階段執行器會對每個節點生成一個對應的運算元,並且從根節點(這個例子中是 Project 節點)開始排程,此時發現此節點依賴其他節點,就先**遞迴呼叫依賴的節點**,一直找到沒有任何依賴的節點(此時為 Start 節點),然後開始執行,執行此節點後,繼續執行此節點被依賴的其他節點(此時為 GetNeighbor 節點),一直到根節點為止。 #### Query 資料流 資料流:每個節點的輸入輸出也是在 [toPlan()](https://github.com/vesoft-inc/nebula-graph/blob/master/src/validator/PipeValidator.cpp) 中確定的, 雖然**執行的時候會按照執行計劃的先後關係執行**,但是每個節點的輸入並不完全依賴上個節點,可以自行定義,因為所有節點的輸入、輸出其實是儲存在一個雜湊表中的,其中 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](https://www-cdn.nebula-graph.com.cn/nebula-blog/query-engine-optimizer.png) 此時會將 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](https://www-cdn.nebula-graph.com.cn/nebula-blog/exe-planner-optimizer.png) 例如,當搜尋到 Filter 節點時,發現 Filter 節點的子節點是 GetNeighbors,和規則中事先定義的模式匹配成功,啟動轉換,將 Filter 節點融入到 GetNeighbors 節點中,然後移除掉 Filter 節點,繼續匹配下一個規則。 優化的程式碼在 nebula-graph 倉下 `src/optimizer/` 目錄下。 ### 第五階段:執行 第五階段:最後 Scheduler 會根據執行計劃生成對應的執行運算元,從葉子節點開始執行,一直到根節點結束。其結構如下: ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/exe-flow.png) 其中每一個執行計劃節點都一一對應一個執行運算元節點,其輸入輸出在執行計劃期間已經確定,每個運算元只需要拿到輸入變數中的值然後進行計算,最後將計算結果放入對應的輸出變數中即可,所以只需要從開始節點一步步執行,最後一個運算元的結果會作為最終結果返回給使用者。 ## 五、例項 下面執行一個最短路徑的例項看看執行計劃的具體結構,開啟 [nebula-console](https://github.com/vesoft-inc/nebula-console), 輸入下面語句 `FIND SHORTEST PATH FROM "YAO MING" TO "Tim Duncan" OVER like, serve UPTO 5 STEPS` ,在這條語句前加 `EXPLAIN` 關鍵字就可以得到該語句生成的執行計劃詳細資訊: ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/explain-detail.jpg) 上圖從左到右依次顯示執行計劃中每個節點的唯一 ID、節點的名稱、該節點所依賴的節點 ID、profiling data(執行 profile 命令時的資訊)、該節點的詳細資訊(包括輸入輸出變數名稱,輸出結果的列名,節點的引數資訊)。 如果想要視覺化一點可以在這條語句前加 ` EXPLAIN format="dot" `,這時候 nebula-console 會生成 dot 格式的資料,然後開啟 [Graphviz Online](https://dreampuf.github.io/GraphvizOnline/) 這個網站將生成的 dot 資料貼上上去,就可以看到如下結構,該結構對應著執行階段各個運算元的執行流程。 ![初識 Nebula Graph 2.0 Query Engine](https://www-cdn.nebula-graph.com.cn/nebula-blog/work-flow.png) 因為最短路徑使用了雙向廣度搜索演算法分別從`"YAO MING"` 和 `"Tim Duncan"` 兩邊同時擴充套件,所以中間的 `GetNeighbors`、`BFSShortest`、 `Project`、 `Dedup` 分別有兩個運算元,通過 `PassThrough` 運算元連線輸入,由 `ConjunctPath` 運算元拼接路徑。然後由 `LOOP` 運算元控制向外擴充套件的步數,可以看到 `DataCollect` 運算元的輸入其實是從 `ConjuctPath` 運算元的輸出變數中取值的。 各個運算元的資訊在 nebula-graph 倉下的 `src/executor` 目錄下。 > 作者有話說:Hi,我是明泉,是圖資料 Nebula Graph 研發工程師,主要工作和資料庫查詢引擎相關,希望本次的經驗分享能給大家帶來幫助,如有不當之處也希望能幫忙糾正,謝謝~ 喜歡這篇文章?來來來,給我們的 [GitHub](https://github.com/vesoft-inc/nebula) 點個 star 表鼓勵啦~~