如何 build NebulaGraph?如何為 NebulaGraph 核心做貢獻?即便是新手也能快速上手,從本文作為切入點就夠了。
為了方便對 NebulaGraph 尚未了解的讀者也能快速直接從貢獻程式碼為起點了解它,我把開發、貢獻核心程式碼入手所需要的基本架構知識在這裡以最小資訊量的形式總結一下。作為前導知識,請資深的 NebulaGraph 玩家直接跳過這一章節。
NebulaGraph 的架構和 Google Spanner、TiDB 很相似,核心部分只有三種服務程序:Graph 服務、Meta 服務和 Storage 服務。它們之間彼此通過 TCP 之上的 Thrift RPC 協定進行通訊。
NebulaGraph 是儲存與計算分離的架構,Meta 服務和 Storage 服務共同組成了儲存層,Graph 服務是核心提供的計算層。
這樣的設計使得 NebulaGraph 的叢集部署可以靈活按需分配計算、儲存的資源。比如,在同一個叢集中建立不同設定的兩組 Graph 服務範例用來面向不同型別的業務。
同時,計算層解耦於儲存層使得在 NebulaGraph 之上的構建不同的特定計算層成為可能。比如,NebulaGraph Algorithm、NebulaGraph Analytics 就是在 NebulaGraph 之上構建了異構的另一個計算層。任何人都可以按需客製化專屬計算層,從而滿足統一圖基礎儲存之上的複合、多樣的計算需求。
Graph 服務是對外接收相簿登入、圖查詢請求、叢集管理操作、Schema 定義所直接連線的服務,它的程序名字叫 graphd,表示 nebula graph daemon。
Graph 服務的每一個程序是無狀態的,這使得橫向擴縮 Graph 服務的範例非常靈活、簡單。
Graph 服務也叫 Query Engine,其內部和傳統的資料庫系統的設計非常相似,分為:解析、校驗、計劃、執行幾部分。
Meta 服務顧名思義負責後設資料管理,程序名字叫 metad。這些後設資料包括:
Meta 服務的程序可以單範例部署。在非單機部署的場景下,為了資料、服務的高 SLA ,以奇數個範例進行部署。通常來說 3 個 nebula-metad 就足夠了,3 個 nebula-metad 通過 Raft 共識協定構成一個叢集提供服務。
Storage 服務儲存所有的圖資料,程序名字叫 storaged。storaged 分散式地儲存圖資料,為 Graph 內部的圖查詢執行期提供底層的圖語意儲存介面,方便 Storage 使用者端通過 Thrift RPC 協定面向涉及的 storaged 範例進行圖語意的讀寫。
當 NebulaGraph 中圖空間的副本數大於 1 的時候,每一個分割區都會在不同 storaged 範例上有副本,副本之間則通過 Raft 協定協調同步與讀寫。
在 NebulaGraph 中 graphd、metad、storaged 之間通過 Thrift 協定進行遠端呼叫(RPC),下邊給一些例子:
當然,有狀態的儲存引擎內部也有叢集同步的流量與通訊。比如,storaged 與其他 storaged 有 Raft 連線;metad 與其他 metad 範例有 Raft 連線。
接下來,我們開始 NebulaGraph 的構建、開發環境的部分。
NebulaGraph 只支援在 GNU/Linux 分支中構建。目前來說,最方便的方式是在社群預先提供好了依賴的容器映象的基礎上在容器內部構建、偵錯 NebulaGraph 程式碼的更改和 Debug。
為了更方便地偵錯程式碼,我習慣提前建立一個 NebulaGraph Docker 環境。推薦使用官方的 Docker-Compose 方式部署,也可以使用我在官方 Docker-Compose 基礎之上弄的一鍵部署工具:nebula-up。
下面以 nebula-up 為例:
在 Linux 開發伺服器中執行 curl -fsSL nebula-up.siwei.io/install.sh | bash
就可以了。
NebulaGraph 的程式碼倉庫託管在 GitHub 之上,在聯網的情況下直接克隆:
git clone [email protected]:vesoft-inc/nebula.git
cd nebula
有了 NebulaGraph 叢集,我們可以藉助 nebula-dev-docker 提供的開箱即用開發容器映象,搭建開發環境:
export TAG=ubuntu2004
docker run -ti \
--network nebula-net \
--security-opt seccomp=unconfined \
-v "$PWD":/home/nebula \
-w /home/nebula \
--name nebula_dev \
vesoft/nebula-dev:$TAG \
bash
其中,-v "$PWD"
表示當前的 NebulaGraph 程式碼原生的路徑會被對映到開發容器內部的 /home/nebula
,而啟動的容器名字是 nebula_dev
。
待這個容器啟動後,會自動進入到這個容器的 bash shell 之中。如果我們輸入 exit
退出容器,它會被關閉。如果我們想再次啟動容器,只需要執行:
docker start nebula_dev
之後的編譯、Debug、測試工作都在 nebula_dev
容器內部進行。在容器是執行狀態的情況下,可以隨時新建一個容器內部的 bash shell 程序:
docker exec -ti nebula_dev bash
為了保持編譯環境是最新版,可以定期刪除、拉取、重建這個開發容器,以保持環境與程式碼相匹配。
在 nebula_dev
這個容器內部,我們可以進行程式碼編譯。進入編譯容器:
docker exec -ti nebula_dev bash
用 CMake 準備 makefile。第一次構建時,為了節省時間、記憶體,我關閉了測試 -DENABLE_TESTING=OFF
:
mkdir build && cd build
cmake -DCMAKE_CXX_COMPILER=$TOOLSET_CLANG_DIR/bin/g++ -DCMAKE_C_COMPILER=$TOOLSET_CLANG_DIR/bin/gcc -DENABLE_WERROR=OFF -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=OFF ..
開始編譯,根據伺服器的空閒 CPU 個數和記憶體量力而行。比如,我在 72 核心的伺服器上準備允許同時執行 64 個 job,則執行:
make -j64
第一次構建的時間會慢一些,在 make 成功之後,我們也可以執行 make install
把二進位制安裝到像生產安裝時候一樣的路徑:
root@1827b82e88bf:/home/nebula/build# make install
root@1827b82e88bf:/home/nebula/build# ls /usr/local/nebula/bin
db_dump db_upgrader meta_dump nebula-graphd nebula-metad nebula-storaged
root@1827b82e88bf:/home/nebula/build# ls /usr/local/nebula/
bin etc pids scripts share
以 graphd 偵錯為例。
安裝一些後邊會方便 Debug 額外用到的依賴:
# 裝一個 ping,測試一下 nebula-up 安裝的叢集可以存取
apt update && apt install iputils-ping -y
# ping graphd 試試看
ping graphd -c 4
# 安裝 gdb gdb-dashboard
apt install gdb -y
wget -P ~ https://git.io/.gdbinit
pip install pygments
準備一個 NebulaGraph 的命令列使用者端:
# 新開一個 nebula_dev 的 shell
docker exec -ti nebula_dev bash
# 下載 nebula-console 二進位制檔案,並賦予可執行許可權,命名為 nebula-console 並安裝到 /usr/bin/ 下
wget https://github.com/vesoft-inc/nebula-console/releases/download/v3.2.0/nebula-console-linux-amd64-v3.2.0
chmod +x nebula-console*
mv nebula-console* /usr/bin/nebula-console
連線到前邊我們 nebula-up 準備的叢集之上,載入 basketballplayer 這個測試資料:
nebula-console -u root -p nebula --address=graphd --port=9669
:play basketballplayer;
exit
用 gdb 執行剛剛編譯的 nebula-graphd 二進位制,讓它成為一個新的 graphd 服務,名字就叫 nebula_dev
。
首先啟動 gdb:
# 新開一個 nebula_dev 的 shell
docker exec -ti nebula_dev bash
cd /usr/local/nebula/
mkdir -p /home/nebula/build/log
gdb bin/nebula-graphd
在 gdb 內部執行設定必要的引數,跟隨 fork 的子程序:
set follow-fork-mode child
設定待偵錯 graphd 的啟動引數(設定):
meta_server_addrs
填已經啟動的叢集的所有 metad 的地址;local_ip
和 ws_ip
填本容器的域名,port
是 graphd 監聽埠;log_dir
是輸出紀錄檔的目錄,v
和 minloglevel
是紀錄檔的輸出等級;set args --flagfile=/usr/local/nebula/etc/nebula-graphd.conf.default \
--meta_server_addrs=metad0:9559,metad1:9559,metad2:9559 \
--port=9669 \
--local_ip=nebula_dev \
--ws_ip=nebula_dev \
--ws_http_port=19669 \
--log_dir=/home/nebula/build/log \
--v=4 \
--minloglevel=0
如果我們想加斷點在 src/common/function/FunctionManager.cpp
2783 行,可以再執行:
b /home/nebula/src/common/function/FunctionManager.cpp:2783
設定前邊安裝的 gdb-dashboard,一個開源的 gdb 介面外掛。
# 設定在 gdb 介面上展示 程式碼、歷史、回撥棧、變數、表達幾個部分,詳細參考 https://github.com/cyrus-and/gdb-dashboard
dashboard -layout source history stack variables expressions
最後我們讓程序通過 gdb 跑起來吧:
run
之後,我們就可以在這個視窗/shell 對談下偵錯 graphd 程式了。
這裡,我以 issue#3513 為例子,快速介紹一下程式碼修改的過程。
這個 issue 表達的內容是在有一小部分使用者決定把 JSON 以 String 的形式儲存在 NebulaGraph 中的屬性裡。因為這種方式比較罕見且不被推崇,NebulaGraph 沒有直接支援對 JSON String 解析。
由於不是一個通用型需求,這個功能是希望熱心的社群使用者自己來實現並應用在他的業務場景中。但在該 issue 中,剛好有位新手貢獻者在裡邊回覆、求助如何開始參與這塊的功能實現。藉著這個契機,我去參與討論看了一下這個功能可以實現成什麼樣子。最終討論的結果是可以做成和 MySQL 中的 JSON_EXTRACT
函數那樣,改為只接受 JSON String、無需處理輸出路徑引數。
一句話來說就是,為 NebulaGraph 引入一個解析 JSON String 為 Map 的函數。那麼,如何實現這個功能呢?
顯然,引入新的函數,專案變更肯定有很多。所以,我們只需要找到之前增加新函數的 PR 就可以快速知道在哪些地方修改了。
一般情況下,可以自底向上地瞭解 NebulaGraph 整體的程式碼結構,再一點點找到函數處理的位置。這時候,除了程式碼本身,一些面向貢獻者的文章可能會幫助大家事半功倍對整體有一個瞭解。NebulaGraph 官方也除了一個系列文章,大家做專案貢獻前不妨閱讀了解下,參見:延伸閱讀 5。
具體的實操起來呢?我從 pr#4526 瞭解到所有函數入口都被統一管理在 src/common/function/FunctionManager.cpp 之中。通過搜尋、理解當中某個函數的關鍵詞之後,可以很容易理解一個函數實體的關鍵詞、輸入/輸出資料型別、函數體處理邏輯的程式碼在哪裡實現。
此外,在同一個根目錄下,src/common/function/test/FunctionManagerTest.cpp
之中則是所有這些函數的單元測試程式碼。用同樣的方式也可以知道新加的一個函數需要如何在裡邊實現基於 gtest 的單元測試。
在修改程式碼之前,確保在最新的 master 分支之上建立一個單獨的分支。在這裡的例子中,我把分支名字叫 fn_JSON_EXTRACT
:
git checkout master
git pull
git checkout -b fn_JSON_EXTRACT
通過 Google 瞭解與交叉驗證 NebulaGraph 內部使用的 utils 庫,知道應該用 folly::parseJson
把字串讀成 folly::dynamic
。再 cast 成 NebulaGraph 內建的 Map()
型別。最後,藉助於 Stack Overflow/GitHub Copilot,我終於完成了第一個版本的程式碼修改。
我興沖沖地改好了第一版的程式碼,信心滿滿地開始編譯!實際上,因為我是 CPP 新手,即使在 Copilot 加持下,我的程式碼還是花了好幾次修改才通過編譯。
編譯之後,我用 gdb 把修改了的 graphd 啟動起來。用 console 發起 JSON_EXTRACT
的函數呼叫。先調通了期待中的效果,並試著跑幾種異常的輸入。在發現新問題、修改、編譯、偵錯的幾輪迴圈下讓程式碼達到了期望的狀態。
這時候,就該把程式碼提交到遠端 GitHub 請專案的資深貢獻者幫忙 review 啦!
PR(Pull Request)是 GitHub 中方便多人程式碼共同作業、程式碼審查中的一種方式。它通過把一個 repo 下的分支與這個審查共同作業的範例(PR)做對映,得到一個專案下唯一的 PR 號碼之後,生成單獨的網頁。在這個網頁下,我們可以做不同貢獻者之間的交流和後續的程式碼更新。這個過程中,程式碼提交者們可以一直在這個分支上不斷提交程式碼直到程式碼的狀態被各方同意 approve,再合併 merge 到目的分支中。
這個過程可以分為:
在這一步驟裡,我們要把當前的本地提交的 commit 提交到自己的 GitHub 分叉之中。
首先,確認原生的修改是否都是期待中的:
# 先確定修改的檔案
$ git status
# 再看看修改的內容
$ git diff
再 commit,這時候是在本地倉庫提交 commit:
# 新增所有當前目錄(. 這個點表示當前目錄)修改過的檔案為待 commit
$ git add .
# 然後我們可以看一下狀態,這些修改的檔案狀態已經不同了
$ git status
# 最後,提交在本地倉庫,並用 -m 引數指定單行的 commit message
$ git commit -m "feat: introduce function JSON_EXTRACT"
在提交之前,要確保自己的 GitHub 賬號之下確實存在 NebulaGraph 程式碼倉庫的分叉 fork。比如,我的 GitHub 賬號是 wey-gu,那麼我對 https://github.com/vesoft-inc/nebula 的分叉應該就是 https://github.com/wey-gu/nebula 。
如果還沒有自己的分叉,可以直接在 https://github.com/vesoft-inc/nebula 上點選右上角的 Fork,建立自己的分叉倉庫。
當遠端的個人分叉存在之後,我們可以把程式碼提交上去:
# 新增一個新的遠端倉庫叫 wey
git remote add wey [email protected]:wey-gu/nebula.git
# 提交 JSON_EXTRACT 分支到 wey 這個 remote 倉庫
git push wey JSON_EXTRACT
這時候,我們存取這個遠端分支:https://github.com/wey-gu/nebula/tree/fn_JSON_EXTRACT,就能找到 Open PR 的入口:
點選 Open pull request
按鈕,進入到建立 PR 的介面了,這和在一般的論壇裡提交一個貼文是很類似的:
提交之後,我們可以等待、或者邀請其他人來做程式碼的審查 review。往往,開源專案的貢獻者們會從他們的各自角度給出程式碼修改、優化的建議。經過幾輪的程式碼修改、討論後,這時候程式碼會達到最佳的狀態。
在這些審查者中,除了社群的貢獻者(人類)之外,還有自動化的機器人。它們會在程式碼庫中自動化地通過持續整合 CI 的方式執行自動化的審查工作,可能包括以下幾種:
通常來說,所有自動化審查機器人執行的任務全都通過後,貢獻的程式碼狀態才能被認為是可合併的。不出意外,我首次提交的程式碼果然有測試的失敗提示。
NebulaGraph 裡所有的 CI 測試程式碼都能在本地被觸發。當然,它們都有被單獨觸發的方式。我們需要掌握如何單獨觸發某個測試,而不是在每次修改一個小的測試修復、提交到伺服器,就等著 CI 做全量的執行,這樣會浪費掉幾十分鐘。
本次 PR 提交中,我修改的函數程式碼同一層級下的單元測試 CTest 就有問題。問題發生的原因有多種,可能是測試程式碼本身、程式碼變更破壞了原來的測試用例、測試用例發現程式碼修改本身的問題。
我們要根據 CTest 失敗的報錯進行排查和程式碼修改。再編譯程式碼,在本地執行一下這個失敗的用例:
# 我們需要進入到我們的編譯容器內部的 build 目錄下
$ docker exec -ti nebula_dev bash
$ cd build
# 在 -DENABLE_TESTING=ON 之中編譯,如果之前的編譯 job 數下記憶體已經跑滿了的話,這次可以把 job 數調小一點,因為開啟測試會佔用更多記憶體
$ cmake -DCMAKE_CXX_COMPILER=$TOOLSET_CLANG_DIR/bin/g++ -DCMAKE_C_COMPILER=$TOOLSET_CLANG_DIR/bin/gcc -DENABLE_WERROR=OFF -DCMAKE_BUILD_TYPE=Debug -DENABLE_TESTING=ON ..
$ make -j 48
# 可以看到編譯成功了 CTest 的單元測試二進位制可執行檔案
# [100%] Linking CXX executable ../../../../bin/test/function_manager_test
# [100%] Built target function_manager_test
# 執行重新修改過的單元測試!
$ bin/test/function_manager_test
[==========] Running 11 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 11 tests from FunctionManagerTest
[ RUN ] FunctionManagerTest.testNull
[ OK ] FunctionManagerTest.testNull (0 ms)
[ RUN ] FunctionManagerTest.functionCall
W20221020 23:35:18.579897 28679 Map.cpp:77] JSON_EXTRACT nested layer 1: Map can be populated only by Bool, Double, Int, String value and null, now trying to parse from: object
[ OK ] FunctionManagerTest.functionCall (2 ms)
[ RUN ] FunctionManagerTest.time
[ OK ] FunctionManagerTest.time (0 ms)
[ RUN ] FunctionManagerTest.returnType
[ OK ] FunctionManagerTest.returnType (0 ms)
[ RUN ] FunctionManagerTest.SchemaRelated
[ OK ] FunctionManagerTest.SchemaRelated (0 ms)
[ RUN ] FunctionManagerTest.ScalarFunctionTest
[ OK ] FunctionManagerTest.ScalarFunctionTest (0 ms)
[ RUN ] FunctionManagerTest.ListFunctionTest
[ OK ] FunctionManagerTest.ListFunctionTest (0 ms)
[ RUN ] FunctionManagerTest.duplicateEdgesORVerticesInPath
[ OK ] FunctionManagerTest.duplicateEdgesORVerticesInPath (0 ms)
[ RUN ] FunctionManagerTest.ReversePath
[ OK ] FunctionManagerTest.ReversePath (0 ms)
[ RUN ] FunctionManagerTest.DataSetRowCol
[ OK ] FunctionManagerTest.DataSetRowCol (0 ms)
[ RUN ] FunctionManagerTest.PurityTest
[ OK ] FunctionManagerTest.PurityTest (0 ms)
[----------] 11 tests from FunctionManagerTest (5 ms total)
[----------] Global test environment tear-down
[==========] 11 tests from 1 test suite ran. (5 ms total)
[ PASSED ] 11 tests.
成功!
將新的更改提交到遠端分支上,在 PR 的網頁中,我們可以看到 CI 已經在新的提交的觸發下重新編譯、執行了。過一會兒全部 pass,我開始興高采烈地等待著 2 位以上的審查者幫忙批准程式碼,最後合併它!
但是,我收到了新的建議:
另一位貢獻者請我新增 TCK 的測試用例。
TCK 的全稱是 The Cypher Technology Compatibility Kit,它是 NebulaGraph 從 openCypher 社群繼承演進而來的一套測試框架,並用 Python 做測試用例格式相容的實現。
它的優雅在於,我們可以像寫英語一樣去描述我們想實現的端到端功能測試用例,像這樣!
# tests/tck/features/function/json_extract.feature
Feature: json_extract Function
Background:
Test json_extract function
Scenario: Test Positive Cases
When executing query:
"""
YIELD JSON_EXTRACT('{"a": "foo", "b": 0.2, "c": true}') AS result;
"""
Then the result should be, in any order:
| result |
| {a: "foo", b: 0.2, c: true} |
When executing query:
"""
YIELD JSON_EXTRACT('{"a": 1, "b": {}, "c": {"d": true}}') AS result;
"""
Then the result should be, in any order:
| result |
| {a: 1, b: {}, c: {d: true}} |
When executing query:
"""
YIELD JSON_EXTRACT('{}') AS result;
"""
Then the result should be, in any order:
| result |
| {} |
在新增了自己的一個新的 tck 測試用例文字檔案之後,我們只需要在測試檔案中臨時增加標籤,並在執行的時候指定標籤,就可以單獨執行新增的 tck 測試用例了:
# 還是在編譯容器內部,進入到 tests 目錄下
cd ../tests
# 安裝 tck 測試所需依賴
python3 -m pip install -r requirements.txt
python3 -m pip install nebula3-python==3.1.0
# 執行一個單獨為 tck 測試準備的叢集
make CONTAINERIZED=true ENABLE_SSL=true CA_SIGNED=true up
# 給 tests/tck/features/function/json_extract.feature 以@開頭第一行加上標籤,比如 @wey
vi tests/tck/features/function/json_extract.feature
# 執行 pytest (包含 tck 用例),因為制定了 -m "wey",只有 tests/tck/features/function/json_extract.feature 會被執行
python3 -m pytest -m "wey"
# 關閉 pytest 所依賴的叢集
make CONTAINERIZED=true ENABLE_SSL=true CA_SIGNED=true down
待我們把需要的測試調通、再次提交 PR 並且 CI 用例全都通過之後,我們可以再次邀請之前幫助審查程式碼的同學做做最後的檢視,如果一切都順利,程式碼就會被合併了!
就這樣,我的第一個 CPP PR 終於被合併成功,大家能看到我留在 NebulaGraph 中的程式碼了。
謝謝你讀完本文 (///▽///)
如果你想嚐鮮圖資料庫 NebulaGraph,記得去 GitHub 下載、使用、(з)-☆ star 它 -> GitHub;和其他的 NebulaGraph 使用者一起交流圖資料庫技術和應用技能,留下「你的名片」一起玩耍呀~