在討論某個數據庫時,存儲 ( Storage ) 和計算 ( Query Engine ) 一般是討論的熱點,也是愛好者們瞭解某個數據庫不可或缺的部分。每一個數據庫都有其獨有的存儲、計算方式,今天就和圖圖來學習下圖數據庫 Nebula Graph 的存儲部分。git
Nebula 的 Storage 包含兩個部分, 一是 meta 相關的存儲, 咱們稱之爲 Meta Service
,另外一個是 data 相關的存儲, 咱們稱之爲 Storage Service
。 這兩個服務是兩個獨立的進程,數據也徹底隔離,固然部署也是分別部署, 不過二者總體架構相差不大,本文最後會提到這點。 若是沒有特殊說明,本文中 Storage Service 代指 data 的存儲服務。接下來,你們就隨我一塊兒看一下 Storage Service 的整個架構。 Let's go~github
圖一 storage service 架構圖數據庫
如圖1 所示,Storage Service 共有三層,最底層是 Store Engine,它是一個單機版 local store engine,提供了對本地數據的 get
/ put
/ scan
/ delete
操做,相關的接口放在 KVStore / KVEngine.h件裏面,用戶徹底能夠根據本身的需求定製開發相關 local store plugin,目前 Nebula 提供了基於 RocksDB 實現的 Store Engine。安全
在 local store engine 之上,即是咱們的 Consensus 層,實現了 Multi Group Raft,每個 Partition 都對應了一組 Raft Group,這裏的 Partition 即是咱們的數據分片。目前 Nebula 的分片策略採用了 靜態 Hash
的方式,具體按照什麼方式進行 Hash,在下一個章節 schema 裏會說起。用戶在建立 SPACE 時需指定 Partition 數,Partition 數量一旦設置便不可更改,通常來說,Partition 數目要能知足業務未來的擴容需求。微信
在 Consensus 層上面也就是 Storage Service 的最上層,即是咱們的 Storage interfaces,這一層定義了一系列和圖相關的 API。 這些 API 請求會在這一層被翻譯成一組針對相應 Partition 的 kv 操做。正是這一層的存在,使得咱們的存儲服務變成了真正的圖存儲,不然,Storage Service 只是一個 kv 存儲罷了。而 Nebula 沒把 kv 做爲一個服務單獨提出,其最主要的緣由即是圖查詢過程當中會涉及到大量計算,這些計算每每須要使用圖的 schema,而 kv 層是沒有數據 schema 概念,這樣設計會比較容易實現計算下推。架構
圖存儲的主要數據是點和邊,但 Nebula 存儲的數據是一張屬性圖,也就是說除了點和邊之外,Nebula 還存儲了它們對應的屬性,以便更高效地使用屬性過濾。併發
對於點來講,咱們使用不一樣的 Tag 表示不一樣類型的點,同一個 VertexID 能夠關聯多個 Tag,而每個 Tag 都有本身對應的屬性。對應到 kv 存儲裏面,咱們使用 vertexID + TagID 來表示 key, 咱們把相關的屬性編碼後放在 value 裏面,具體 key 的 format 如圖2 所示:分佈式
圖二 Vertex Key Formatide
Type
: 1 個字節,用來表示 key 類型,當前的類型有 data, index, system 等性能
Part ID
: 3 個字節,用來表示數據分片 Partition,此字段主要用於 Partition 從新分佈(balance) 時方便根據前綴掃描整個 Partition 數據
Vertex ID
: 4 個字節, 用來表示點的 ID
Tag ID
: 4 個字節, 用來表示關聯的某個 tag
Timestamp
: 8 個字節,對用戶不可見,將來實現分佈式事務 ( MVCC ) 時使用
在一個圖中,每一條邏輯意義上的邊,在 Nebula Graph 中會建模成兩個獨立的 key-value,分別稱爲 out-key 和in-key。out-key 與這條邊所對應的起點存儲在同一個 partition 上,in-key 與這條邊所對應的終點存儲在同一個partition 上。一般來講,out-key 和 in-key 會分佈在兩個不一樣的 Partition 中。
兩個點之間可能存在多種類型的邊,Nebula 用 Edge Type 來表示邊類型。而同一類型的邊可能存在多條,好比,定義一個 edge type "轉帳",用戶 A 可能屢次轉帳給 B, 因此 Nebula 又增長了一個 Rank 字段來作區分,表示 A 到 B 之間屢次轉帳記錄。 Edge key 的 format 如圖3 所示:
圖三 Edge Key Format
Type
: 1 個字節,用來表示 key 的類型,當前的類型有 data, index, system 等。
Part ID
: 3 個字節,用來表示數據分片 Partition,此字段主要用於 Partition 從新分佈(balance) 時方便根據前綴掃描整個 Partition 數據
Vertex ID
: 4 個字節, 出邊裏面用來表示源點的 ID, 入邊裏面表示目標點的 ID。
Edge Type
: 4 個字節, 用來表示這條邊的類型,若是大於 0 表示出邊,小於 0 表示入邊。
Rank
: 4 個字節,用來處理同一種類型的邊存在多條的狀況。用戶能夠根據本身的需求進行設置,這個字段可_存放交易時間_、交易流水號、或_某個排序權重_
Vertex ID
: 4 個字節, 出邊裏面用來表示目標點的 ID, 入邊裏面表示源點的 ID。
Timestamp
: 8 個字節,對用戶不可見,將來實現分佈式作事務的時候使用。
針對 Edge Type 的值,若若是大於 0 表示出邊,則對應的 edge key format 如圖4 所示;若 Edge Type 的值小於 0,則對應的 edge key format 如圖5 所示
圖4 出邊的 Key Format 圖5 入邊的 Key Format
對於點或邊的屬性信息,有對應的一組 kv pairs,Nebula 將它們編碼後存在對應的 value 裏。因爲 Nebula 使用強類型 schema,因此在解碼以前,須要先去 Meta Service 中取具體的 schema 信息。另外,爲了支持在線變動 schema,在編碼屬性時,會加入對應的 schema 版本信息,具體的編解碼細節在這裏不做展開,後續會有專門的文章講解這塊內容。
OK,到這裏咱們基本上了解了 Nebula 是如何存儲數據的,那數據是如何進行分片呢?很簡單,對 Vertex ID 取模
便可。經過對 Vertex ID 取模,同一個點的全部_出邊_,_入邊_以及這個點上全部關聯的 _Tag 信息_都會被分到同一個 Partition,這種方式大大地提高了查詢效率。對於在線圖查詢來說,最多見的操做即是從一個點開始向外 BFS(廣度優先)拓展,因而拿一個點的出邊或者入邊是最基本的操做,而這個操做的性能也決定了整個遍歷的性能。BFS 中可能會出現按照某些屬性進行剪枝的狀況,Nebula 經過將屬性與點邊存在一塊兒,來保證整個操做的高效。當前許多的圖數據庫經過 Graph 500 或者 Twitter 的數據集試來驗證本身的高效性,這並無表明性,由於這些數據集沒有屬性,而實際的場景中大部分狀況都是屬性圖,而且實際中的 BFS 也須要進行大量的剪枝操做。
爲何要本身作 KVStore,這是咱們無數次被問起的問題。理由很簡單,當前開源的 KVStore 都很難知足咱們的要求:
性能,性能,性能:Nebula 的需求很直接:高性能 pure kv;
以 library 的形式提供:對於強 schema 的 Nebula 來說,計算下推須要 schema 信息,而計算下推實現的好壞,是 Nebula 是否高效的關鍵;
數據強一致:這是分佈式系統決定的;
使用 C++實現:這由團隊的技術特色決定;
基於上述要求,Nebula 實現了本身的 KVStore。固然,對於性能徹底不敏感且不太但願搬遷數據的用戶來講,Nebula 也提供了整個KVStore 層的 plugin,直接將 Storage Service 搭建在第三方的 KVStore 上面,目前官方提供的是 HBase 的 plugin。
Nebula KVStore 主要採用 RocksDB 做爲本地的存儲引擎,對於多硬盤機器,爲了充分利用多硬盤的併發能力,Nebula 支持本身管理多塊盤,用戶只需配置多個不一樣的數據目錄便可。分佈式 KVStore 的管理由 Meta Service 來統一調度,它記錄了全部 Partition 的分佈狀況,以及當前機器的狀態,當用戶增減機器時,只須要經過 console 輸入相應的指令,Meta Service 便可以生成整個 balance plan 並執行。(之因此沒有采用徹底自動 balance 的方式,主要是爲了減小數據搬遷對於線上服務的影響,balance 的時機由用戶本身控制。)
爲了方便對於 WAL 進行定製,Nebula KVStore 實現了本身的 WAL 模塊,每一個 partition 都有本身的 WAL,這樣在追數據時,不須要進行 wal split 操做, 更加高效。 另外,爲了實現一些特殊的操做,專門定義了 Command Log 這個類別,這些 log 只爲了使用 Raft 來通知全部 replica 執行某一個特定操做,並無真正的數據。除了 Command Log 外,Nebula 還提供了一類日誌來實現針對某個 Partition 的 atomic operation,例如 CAS,read-modify-write, 它充分利用了Raft 串行的特性。
關於多圖空間(space)的支持:一個 Nebula KVStore 集羣能夠支持多個 space,每一個 space 可設置本身的 partition 數和 replica 數。不一樣 space 在物理上是徹底隔離的,並且在同一個集羣上的不一樣 space 可支持不一樣的 store engine 及分片策略。
做爲一個分佈式系統,KVStore 的 replication,scale out 等功能需 Raft 的支持。當前,市面上講 Raft 的文章很是多,具體原理性的內容,這裏再也不贅述,本文主要說一些 Nebula Raft 的一些特色以及工程實現。
因爲 Raft 的日誌不容許空洞,幾乎全部的實現都會採用 Multi Raft Group 來緩解這個問題,所以 partition 的數目幾乎決定了整個 Raft Group 的性能。但這也並非說 Partition 的數目越多越好:每個 Raft Group 內部都要存儲一系列的狀態信息,而且每個 Raft Group 有本身的 WAL 文件,所以 Partition 數目太多會增長開銷。此外,當 Partition 太多時, 若是負載沒有足夠高,batch 操做是沒有意義的。好比,一個有 1w tps 的線上系統單機,它的單機 partition 的數目超過 1w,可能每一個 Partition 每秒的 tps 只有 1,這樣 batch 操做就失去了意義,還增長了 CPU 開銷。 實現 Multi Raft Group 的最關鍵之處有兩點,** 第一是共享 Transport 層**,由於每個 Raft Group 內部都須要向對應的 peer 發送消息,若是不能共享 Transport 層,鏈接的開銷巨大;第二是線程模型,Mutli Raft Group 必定要共享一組線程池,不然會形成系統的線程數目過多,致使大量的 context switch 開銷。焦做國醫醫院口碑好嗎___看胃腸到國醫:http://jz.lieju.com/zhuankeyiyuan/37572844.htm
對於每一個 Partition來講,因爲串行寫 WAL,爲了提升吞吐,作 batch 是十分必要的。通常來說,batch 並無什麼特別的地方,可是 Nebula 利用每一個 part 串行的特色,作了一些特殊類型的 WAL,帶來了一些工程上的挑戰。
舉個例子,Nebula 利用 WAL 實現了無鎖的 CAS 操做,而每一個 CAS 操做須要以前的 WAL 所有 commit 以後才能執行,因此對於一個 batch,若是中間夾雜了幾條 CAS 類型的 WAL, 咱們還須要把這個 batch 分紅粒度更小的幾個 group,group 之間保證串行。還有,command 類型的 WAL 須要它後面的 WAL 在其 commit 以後才能執行,因此整個 batch 劃分 group 的操做工程實現上比較有特點。焦做國醫胃腸醫院胃鏡檢查多少錢__良心醫院:http://jz.lieju.com/zhuankeyiyuan/37572711.htm
Learner 這個角色的存在主要是爲了 應對擴容
時,新機器須要"追"至關長一段時間的數據,而這段時間有可能會發生意外。若是直接以 follower 的身份開始追數據,就會使得整個集羣的 HA 能力降低。 Nebula 裏面 learner 的實現就是採用了上面提到的 command wal,leader 在寫 wal 時若是碰到 add learner 的 command, 就會將 learner 加入本身的 peers,並把它標記爲 learner,這樣在統計多數派的時候,就不會算上 learner,可是日誌仍是會照常發送給它們。固然 learner 也不會主動發起選舉。
Transfer leadership 這個操做對於 balance 來說相當重要,當咱們把某個 Paritition 從一臺機器挪到另外一臺機器時,首先便會檢查 source 是否是 leader,若是是的話,須要先把他挪到另外的 peer 上面;在搬遷數據完畢以後,一般還要把 leader 進行一次 balance,這樣每臺機器承擔的負載也能保證均衡。
實現 transfer leadership, 須要注意的是 leader 放棄本身的 leadership,和 follower 開始進行 leader election 的時機。對於 leader 來說,當 transfer leadership command 在 commit 的時候,它放棄 leadership;而對於 follower 來說,當收到此 command 的時候就要開始進行 leader election, 這套實現要和 Raft 自己的 leader election 走一套路徑,不然很容易出現一些難以處理的 corner case。
爲了不腦裂,當一個 Raft Group 的成員發生變化時,須要有一箇中間狀態, 這個狀態下 old group 的多數派與 new group 的多數派老是有 overlap,這樣就防止了 old group 或者新 group 單方面作出決定,這就是論文中提到的 joint consensus
。爲了更加簡化,Diego Ongaro 在本身的博士論文中提出每次增減一個 peer 的方式,以保證 old group 的多數派老是與 new group 的多數派有 overlap。 Nebula 的實現也採用了這個方式,只不過 add member 與 remove member 的實現有所區別,具體實現方式本文不做討論,有興趣的同窗能夠參考 Raft Part class 裏面 addPeer
/ removePeer
的實現。
Snapshot 如何與 Raft 流程結合起來,論文中並無細講,可是這一部分我認爲是一個 Raft 實現裏最容易出錯的地方,由於這裏會產生大量的 corner case。
舉一個例子,當 leader 發送 snapshot 過程當中,若是 leader 發生了變化,該怎麼辦? 這個時候,有可能 follower 只接到了一半的 snapshot 數據。 因此須要有一個 Partition 數據清理過程,因爲多個 Partition 共享一份存儲,所以如何清理數據又是一個很麻煩的問題。另外,snapshot 過程當中,會產生大量的 IO,爲了性能考慮,咱們不但願這個過程與正常的 Raft 共用一個 IO threadPool,而且整個過程當中,還須要使用大量的內存,如何優化內存的使用,對於性能十分關鍵。因爲篇幅緣由,咱們並不會在本文對這些問題展開講述,有興趣的同窗能夠參考 SnapshotManager
的實現。
在 KVStore 的接口之上,Nebula 封裝有圖語義接口,主要的接口以下:
getNeighbors
: 查詢一批點的出邊或者入邊,返回邊以及對應的屬性,而且須要支持條件過濾;
Insert vertex/edge
: 插入一條點或者邊及其屬性;
getProps
: 獲取一個點或者一條邊的屬性;
這一層會將圖語義的接口轉化成 kv 操做。爲了提升遍歷的性能,還要作併發操做。
在 KVStore 的接口上,Nebula 也同時封裝了一套 meta 相關的接口。Meta Service 不但提供了圖 schema 的增刪查改的功能,還提供了集羣的管理功能以及用戶鑑權相關的功能。Meta Service 支持單獨部署,也支持使用多副原本保證數據的安全。
這篇文章給你們大體介紹了 Nebula Storage 層的總體設計, 因爲篇幅緣由, 不少細節沒有展開講, 歡迎你們到咱們的微信羣裏提問,加入 Nebula Graph 交流羣,請聯繫 Nebula Graph 官方小助手微信號:NebulaGraphbot。