摘要:本文主要介紹 Query 層的總體結構,並經過一條 nGQL 語句來介紹其經過 Query 層的四個主要模塊的流程。git
分佈式圖數據庫 Nebula Graph 2.0 版本相比 1.0 有較大改動,最明顯的變化即是,在 1.0 版本中 Query、Storage 和 Meta 模塊代碼不做區分放在同一個代碼倉中,而 Nebula Graph 2.0 開始在架構上先解耦成三個代碼倉:nebula-graph、nebula-common 和 nebula-storage,其中 nebula-common 中主要是表達式的定義、函數定義和一些公共接口、nebula-graph 主要負責 Query 模塊、nebula-storage 主要負責 Storage 和 Meta 模塊。github
本文主要介紹 Query 層的總體結構,並經過一條 nGQL 語句來介紹其經過 Query 層的四個主要模塊的流程,因爲 Nebula Graph 2.0 仍處於開發中,版本變化比較頻繁,本文主要針對 2.0 的 nebula-graph 倉中 master 分支的 aea5befd179585c510fb83452cb82276a7756529 版本。算法
Query 層主要框架以下所示:數據庫
主要分爲 4 個子模塊微信
下面講下 nebula-graph 的代碼層次結構,以下所示數據結構
|--src |--context // 校驗期和執行期上下文 |--daemons |--executor // 執行算子 |--mock |--optimizer // 優化規則 |--parser // 詞法語法分析 |--planner // 執行計劃結構 |--scheduler // 調度器 |--service |--util // 基礎組件 |--validator // 語句校驗 |--vistor
自 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 層的數據流以下所示:框架
主要流程以下:分佈式
第一階段:首先通過 Flex 和 Bison 組成的詞法語法解析器模塊 Parser 生成對應的 AST, 結構以下: ide
在此階段 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
目錄下,其邏輯結構以下:
執行流:該執行計劃是一個有向無環圖,其中節點間的依賴關係在 Validator 中每一個模塊的 toPlan() 函數中肯定,在這個例子中 Project 依賴 Filter, Filter 依賴 GetNeighbor,依次類推直到 Start 節點爲止。
在執行階段執行器會對每一個節點生成一個對應的算子,而且從根節點(這個例子中是 Project 節點)開始調度,此時發現此節點依賴其餘節點,就先遞歸調用依賴的節點,一直找到沒有任何依賴的節點(此時爲 Start 節點),而後開始執行,執行此節點後,繼續執行此節點被依賴的其餘節點(此時爲 GetNeighbor 節點),一直到根節點爲止。
數據流:每一個節點的輸入輸出也是在 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"
兩邊同時擴展,因此中間的 GetNeighbors
、BFSShortest
、 Project
、 Dedup
分別有兩個算子,經過 PassThrough
算子鏈接輸入,由 ConjunctPath
算子拼接路徑。而後由 LOOP
算子控制向外擴展的步數,能夠看到 DataCollect
算子的輸入實際上是從 ConjuctPath
算子的輸出變量中取值的。
各個算子的信息在 nebula-graph 倉下的 src/executor
目錄下。
做者有話說:Hi,我是明泉,是圖數據 Nebula Graph 研發工程師,主要工做和數據庫查詢引擎相關,但願本次的經驗分享能給你們帶來幫助,若有不當之處也但願能幫忙糾正,謝謝~
喜歡這篇文章?來來來,給咱們的 GitHub 點個 star 表鼓勵啦~~ ????♂️????♀️ [手動跪謝]
交流圖數據庫技術?交個朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你進交流羣~~