本文首發(fā)于 Nebula Graph Community 公眾號
在上次的 nebula-storage on nLive 直播中,來自 Nebula 存儲團隊的負責(zé)人王玉玨(四王)同大家分享了 nebula storage 這塊的設(shè)計思考,也解答了一些來自社區(qū)小伙伴的提問。本文整理自該場直播,按照問題涉及的分類進行順序調(diào)整,并非完全按照直播的時間先后排序。
Nebula 的存儲架構(gòu)
整個 Storage 主要分三層,最下面是 Store Engine,也 就是 RocksDB,中間是 raft 一致性協(xié)議層,最上層 storage service 提供對外的 rpc 接口,比如取點屬性,或者邊屬性,或者是去從某個點去找它的鄰居之類的接口。當(dāng)我們通過語句CREATE SPACE IF NOT EXISTS my_space_2 (partition_num=15, replica_factor=1, vid_type=FIXED_STRING(30)); 創(chuàng)建 space 時,根據(jù)填寫的參數(shù)將 space 劃分為多個邏輯單元成為 partition,各個 partition 會落到不同機器上,同一個 Partition 的多個副本會組成一個邏輯單元,并通過 raft 共識算法 raft 保證一致。
Nebula 的存儲數(shù)據(jù)格式
這里著重講述為何 v2.x 會有這些數(shù)據(jù)格式的改動:在 v1.x 版本中,Nebula VID 主要是 int 類型,所以大家可以看到上圖 v1.x 中不管是點還是邊,它的 VID 是定長的、占 8 個字節(jié)。2.x 版本開始,為了支持 string 類型 VID,VertexID 就變成不定長的 n 個字節(jié)。所以大家創(chuàng)建 Space 的時候需要指定 VID 的長度,這個是最主要的改動,其他的話還有一些小的改動,去掉了時間戳。整體來說,目前的存儲格式更貼近圖的使用場景——從某個點開始找它的鄰居,以 v2.x 這樣 VertexID + EdgeType 存儲格式來保存邊的話,可以迅速地找到某個點出邊。
同時,v2.x 也做了 key(Nebula 底層是 KV 存儲的)編碼格式上的改變,簡單來說就是把點和邊分開。這樣的話,取某一個點所有 tag 時通過一次 prefix 就可以直接掃到,避免了像 v1.x 那樣掃描點的過程中夾雜多個邊的問題。
底層的數(shù)據(jù)存儲
針對用戶提出的“Nebula 底層如何存儲數(shù)據(jù)”的問題,四王了進行了回復(fù):Nebula 的存儲層使用 KV 進行存儲點邊數(shù)據(jù)。對于一個點而言,key 里面存儲 VID 和它的 tag 類型。點的 value 中,會根據(jù) 這個 tag 的 schema,將 schema 中的各個屬性進行編碼并存在 value 中。比如,player 這個 tag 可能會有一個 age 這樣一個整型年齡字段,使用存儲的時候會把 age 字段的值,按某種編碼保存在 value 中。再來說下邊,邊的存儲 key 會多幾個字段,主要是邊的起點 ID、邊類型、ranking 及終點類型,通過這四元組確定唯一的邊。邊的 value 和點的 value 類似,根據(jù)邊的 Schema 字段定義,將各個字段進行編碼存儲。這里要說一下,Nebula 中存儲邊是存儲兩份:Nebula 中的邊是有向邊,存儲層會存儲正向邊和反向邊,這樣的好處在于使用 GO FROM 進行遍歷查找那些點指向點 A 或者點 A 指向哪些點可以快速通過雙向查找實現(xiàn)。
一般來說,圖存儲分為切邊和切點兩種方式,像上面說的 Nebula 其實采用了切邊方式:一條邊存儲兩份 KV。
用戶提問:為什么采用切邊方式,切點和切邊各自有啥利弊?
切邊的話,每一份邊存兩份,數(shù)據(jù)總量會比切點大很多,因為圖數(shù)據(jù)邊的數(shù)量是遠大于點的數(shù)量,造成邊的大量冗余,相對好處是對起點和它的邊進行映射時會映射到同一個 partition 上,這樣進行一些從單個點觸發(fā)的 query 時會很快速得到結(jié)果。切點的話,由于點可能被分在多個機器上,更新數(shù)據(jù)時得考慮數(shù)據(jù)的一致性問題,一般在圖計算里面切點的使用會更廣泛。
你問我答
下面內(nèi)容收集于之前活動預(yù)告的 AMA 環(huán)節(jié),以及直播時彈幕中提出的問題。
問題目錄
- 邊的 value 存儲邊屬性嗎?
- 強 Schema 的設(shè)計原因
- 存一份邊的設(shè)計
- 圖空間如何做物理隔離
- Meta 如何存儲 Schema
- 存儲未來規(guī)劃
- VID 遍歷點和邊的原理
- 數(shù)據(jù)預(yù)校驗
- Nebula 監(jiān)測
- Nebula 的事務(wù)
- 數(shù)據(jù)膨脹問題
- 磁盤容量本身不均怎么處理
- Nebula 的 RocksDB “魔改”
邊的 value 存儲邊屬性嗎?
和上面底層存儲里講的那樣,創(chuàng)建 Edge 的 schema 時候會指定邊類型上的屬性,這些屬性會作為底層 RocksDB key 的 value 存儲起來,這個 value 的占位是定長的,和下面這個問題類似:
強 Schema 的設(shè)計原因
強 schema 是因為技術(shù)原因還是產(chǎn)品原因? 因為考慮到 string 類型是變長的,每行長度本身就不固定,感覺跟無 schema 無區(qū)別。 如果非定長,那查詢時怎么知道該查詢到哪里呢? 是有標志位么?
其實本質(zhì)上原因是用強 Schema 的好處是快,先說下常見的簡單數(shù)據(jù)類型,比如:int 和 double,這樣的數(shù)據(jù)類型長度是固定的,我們會直接在 value 相應(yīng)的位置進行編碼。再說下 string 類型,在 Nebula 中有兩種 string :一種是定長 string,長度是固定,和前面的簡單數(shù)據(jù)類型一樣,在 value 的固定位置進行編碼。另外一種是變長的 string,通常來說大家都會比較傾向于變長 string(靈活),非定長 string 會以指針形式存儲。
舉個例子,schema 中有個屬性是變長 string 類型,我們不會和簡單數(shù)據(jù)類型一樣直接編碼保存,而是在相應(yīng)位置保存一個 offset 指針,實際指向 value 中的第 100 個字節(jié),然后在 100 這個位置才保存這個變長 string。所以讀取變長 string 的時候,我們需要在 value 中讀兩次,第一次獲取 offset,第二次才能真正把 string 讀出來。通過這樣的形式,把所有類型的屬性都轉(zhuǎn)化成"定長",這樣設(shè)計的好處是,根據(jù)要讀取的屬性和它前面所有字段的占用字節(jié)大小,可以直接計算出要讀取的字段在 value 中存儲的位置,并把它讀出來。讀取過程中,不需要讀取無關(guān)的字段,避免了弱 schema 需要對整個 value 進行解碼的問題。
像 Neo4j 這種圖數(shù)據(jù)庫,一般是 No Schema,這樣寫入的時候會比較靈活,但序列化和反序列化時都會消耗一些 CPU,并且讀取的時候需要重新解碼。
追問:如果有變長 string,會不會導(dǎo)致每行數(shù)據(jù)長度不一樣
可能 value 長度會不一樣,因為本身是變長嘛。
追問:如果每行長度不一樣,為什么要強 schema? Nebula 底層存儲用的 RocksDB,以 block 的形式組織,每個 block 可能是 4K 大小,讀取的時候也是按 block 大小進行讀取,而每個 block 中的各個 value 長度可能是不一樣的。強 schema 的好處在于讀單條數(shù)據(jù)的時候會快。
存一份邊的設(shè)計
Nebula 存邊是存儲了兩份,可以只存儲一份邊嗎?存一份邊反向查詢是否存在問題?
其實這是一個比較好的問題,其實在 Nebula 最早期設(shè)計中是只存一份邊的屬性,這適用于部分業(yè)務(wù)場景。舉個例子,你不需要任何的反向遍歷,這種情況下是完全不需要存反向邊。目前來說,存反向邊最大的意義是方便于我們做反向查詢。其實在 Nebula 比較早的版本中,準確說它是只存了反向邊的 key,邊類型的屬性值是沒有存,屬性值只存在正向邊上。它可能帶來一些問題,雙向遍歷或者反向查詢時,整個代碼邏輯包括處理流程都會比較復(fù)雜。 如果只存一份邊,反向查詢的確存在問題。
圖空間如何做物理隔離
大家在用 Nebula 時,首先會建圖空間 CREATE SPACE,在建圖空間時,系統(tǒng)會分配一個唯一圖空間 ID 叫 spaceId,通過 DESCRIBE SPACE 可以獲取 spaceId。然后 Storage 發(fā)現(xiàn)某臺機器要保存 space 部分數(shù)據(jù)時,會先單獨建一個額外的目錄,再建單獨 RocksDB 在這個上面起 Rocks 的 instance(實例)用來保存數(shù)據(jù),通過這樣方式進行物理隔離。這樣設(shè)計的話,有個弊端:雖然 rocksdb 的 instance,或者說整個 space 目錄是互相隔離,但有可能存在同一塊盤上,目前資源隔離還做的不夠好。
Meta 如何存儲 Schema
我們以 CREATE TAG 為例子,當(dāng)我們建 tag 時,首先會往 meta 發(fā)一個請求,讓它把這個信息寫進去。寫入形式非常簡單,先獲取 tagId,再保存 tag name。底層 RocksDB 存儲的 key 便是 tagId 或者是 tag name,value 是它每一個字段里面的定義,比如說,第一個字段是年齡,類型是整型 int;第二個字段是名字,類型是 string。schema 把所有字段的類型和名字全部存在 value 里,以某種序列化形式寫到 RocksDB 中。
這里說下,meta 和 storage 兩個 service 底層都是 RocksDB 采用 kv 存儲,只不過提供了不一樣的接口,比如說,meta 提供的接口,可能就是保存某個 tag,以及 tag 上有哪些屬性;或者是機器或者 space 之類的元信息,包括像用戶權(quán)限、配置信息都是存在 meta 里。storage 也是 kv 存儲,不過存儲的數(shù)據(jù)是點邊數(shù)據(jù),提供的接口是取點、取邊、取某個點所有出邊之類的圖操作。整體上,meta 和 storage 在 kv 存儲層代碼是一模一樣,只不過往上暴露的對外接口是不一樣的。
最后,storage 和 meta 是分開存儲的,二者不是一個進程且存的目錄在啟動的時指定的也不一樣。
追問:meta 機器掛了,該怎么辦?
是這樣,通常來說 Nebula 建議 meta 以三副本方式部署。這樣的話,只掛一臺機器是沒有問題的。如果單副本部署 meta 掛了的話,是無法對 schema 進行任何操作,包括不能創(chuàng)建 space。因為 storage 和 graph 是不強依賴 meta 的,只有在啟動時會從 meta 獲取信息,之后都是定期地獲取 meta 存儲的信息,所以如果你在整個集群跑的過程中,meta 掛了而又不做 schema 修改的話,對 graph 和 storage 是不會有任何影響的。
存儲未來規(guī)劃
Nebula 后面在存儲層有什么規(guī)劃嗎?性能,可用性,穩(wěn)定性方面
性能這塊,Nebula 底層采用了 RocksDB,而它的性能主要取決于使用方式,和調(diào)參的熟練程度,坦白來說,即便是 Facebook 內(nèi)部員工來調(diào)參也是一門玄學(xué)。再者,剛才介紹了 Nebula 的底層 key 存儲,比如說 VID 或者是 EdgeType 在底層存儲的相對位置某種程度上決定了部分 Query 會有性能影響。從拋開 RocksDB 本身來說,其實還有很多性能上的事情可做:一是寫點或者寫邊時,有些索引需要處理,這會帶來額外性能開銷。此外,Compaction 和實際業(yè)務(wù) workload 也會對性能有很大影響。
穩(wěn)定性上,Nebula 底層采用 raft 協(xié)議,這是保證 Nebula Graph 不丟數(shù)據(jù)一個非常關(guān)鍵的點。因為只有這層穩(wěn)定了,再往下面的 RocksDB 寫入數(shù)據(jù)才不會出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)丟失的情況發(fā)生。此外,Nebula 本身是按照通用型數(shù)據(jù)庫來設(shè)計的,會遇到一些通用型數(shù)據(jù)庫共同面臨的問題,比如說 DDL 改變;而本身 Nebula 是一款分布式圖數(shù)據(jù)庫,也會面臨分布式系統(tǒng)所遇到的問題,像網(wǎng)絡(luò)隔離、網(wǎng)絡(luò)中斷、各種超時或者因為某些原因節(jié)點掛了。上面這些問題的話,都需要有應(yīng)對機制,比如 Nebula 目前支持動態(tài)擴縮容,整個流程非常復(fù)雜,需要在 meta 上、以及掛掉的節(jié)點、剩余“活著”的節(jié)點進行數(shù)據(jù)遷移工作。在這個過程中,中間任何一步失敗都要做 Failover 處理。
可用性方面,我們后續(xù)會引入主備架構(gòu)。在有些場景下所涉及的數(shù)據(jù)量會比較少,不太需要存三副本,單機存儲即可。這種全部數(shù)據(jù)就在單機上的情況,可以減去不必要的 RPC 調(diào)用,直接換成本地調(diào)用,性能可能會有很大的提升。因為,Nebula 部署一共起 3 個服務(wù):meta、graph 和 storage,如果是單機部署的話,graph + storage 可以放在同一臺機器上,原先 graph 需要通過 RPC 調(diào)用 storage 接口去獲取數(shù)據(jù)再回到 graph 進行運算。假如你的查詢語句是多跳查詢,從 graph 發(fā)送請求到 storage 這樣的調(diào)用鏈路反復(fù)執(zhí)行多次,這會導(dǎo)致網(wǎng)絡(luò)開銷、序列化和反序列化的這些損耗提高。 當(dāng) 2 個進程(storaged 和 graphd)合在一起,便沒有了 RPC 調(diào)用,所以性能會有個大提升。此外,這種單機情況下 CPU 利用率會很高,這也是目前 Nebula 存儲團隊在做的事情,會在下一個大版本同大家見面。
VID 遍歷點和邊的原理
可以依據(jù) VID 遍歷點和邊?
從上圖你可以看到存儲了個 Type 類型,在 v1.x 版本中無論點和邊 Type 類型都是一樣的,所以就會發(fā)生上面說到過的掃描點會夾雜多個邊的問題。在 v2.x 開始,將點和邊的 Type 進行區(qū)分,前綴 Type 值就不一樣了,給定一個 VID,無論是查所有 tag 還是所有邊,都只需要一次前綴查詢,且不會掃描額外數(shù)據(jù)。
數(shù)據(jù)預(yù)校驗
Nebula 是強 Schema 的,插入數(shù)據(jù)時如何去判斷這個字段是否符合定義?
是否符合定義的話,大概是這樣,創(chuàng)建 Schema 時會要求指定某個字段是 nullable 或者是有默認值,或者既不是 nullable 也不帶默認值。當(dāng)我們插入一條數(shù)據(jù)的時候,插入語句會要求你“寫明”各個字段的值分別是什么。而這條插入 Query 發(fā)到存儲層后,存儲層會檢查是不是所有字段值都有設(shè)置,或者寫入值的字段是否有默認值或者是 nullable。然后程序會去查是不是所有的字段都可以填上值。如果不是的話,系統(tǒng)會報錯,告知用戶 Query 有問題無法寫入。如果沒有報錯,storage 就會對 value 進行編碼,然后通過 raft 最后寫到 RocksDB 里,整個流程大概是這樣的。
Nebula 監(jiān)測
Nebula 可以針對 space來進行統(tǒng)計嗎?因為我記得好像針對機器。
這個是非常好的問題,目前答案是不能。這塊我們在規(guī)劃,這個問題的主要原因是 metrics 較少,目前我們支持的 metrics 只有 latency、qps 還有報錯的 qps 這三類。每個指標有對應(yīng)的平均值、最大值、最小值,sum 和 count,以及 p99 之類參數(shù)。目前是機器級別的 metrics,后續(xù)的話會做兩個優(yōu)化:一個增多 metrics;二是按 space 級別進行統(tǒng)計,對于每個空間來說,我們會提供諸如 fetch、go、lookup 之類語句的 qps。上面是 graph 這邊的 metrics,而 storage 這塊因為沒有強資源隔離能力,還是提供集群或者單個機器級別的 metrics 而不是 space 級別的。
Nebula 的事務(wù)
nebula 2.6.0 的邊事務(wù)是怎么實現(xiàn)的呢?
先說下邊事務(wù)的背景,背景是上面提到的 Nebula 是存了兩份邊 2 個 kv,這 2 個 kv 可能會存在不同的節(jié)點上,這會導(dǎo)致如果有臺機器掛了,其中有一條邊可能是沒有成功寫入。所謂邊事務(wù)或者叫 TOSS,它主要解決的問題就是當(dāng)我們遇到其中有一臺機器宕機時,存儲層能夠保證這兩個邊(出邊和入邊)的最終一致。這個一致性級別是最終一致,沒有選擇強一致是因為研發(fā)過程中碰到一些報錯信息以及數(shù)據(jù)處理流程上的問題,最后選擇了最終一致性。
再來說下 TOSS 處理的整體流程,先往第一個要寫入數(shù)據(jù)的機器發(fā)正向邊信息,在機器上寫個標記,看標記有沒有寫成功,如果成功了進入到下一步,如果失敗直接報錯。第二步的話,把反向邊信息從第一臺機器發(fā)給第二臺機器,能讓存正向邊的機器向第二臺機器發(fā)送反向邊信息的原因是,Nebula 中正反向邊只有起點和終點調(diào)換了一個位置,所以存正向邊的機器是完全可以拼出反向邊。存反向邊的機器收到之后,會直接寫入邊,并將它的寫入結(jié)果成功與否告訴第一臺機器。第一臺機器收到這個寫入結(jié)果之后,假設(shè)它是成功的,它就會把之前第一步寫的標記刪掉,同時換成正常的邊,這時整個邊的正常寫入流程就完成了,這是一個鏈式的同步機制。
簡單說下失敗的流程,一開始第一臺機器寫失敗了直接就報錯;第一臺機器成功之后,第二臺機器寫失敗了,這種情況下機器一會有背景線程,會一直不斷嘗試修復(fù)第二臺機器的邊,保證和第一臺機器一樣。當(dāng)中比較復(fù)雜的是,第一臺機器會根據(jù)第二臺機器返回的錯誤碼進行處理。目前來說,所有的流程都會直接把標記刪掉,直接換成正常的正向邊,同時寫些更額外的標記來表示現(xiàn)在需要恢復(fù)的失敗邊,讓它們最終保持一致。
追問:點沒有事務(wù)嗎?
是這樣,因為點是只存了一份,所以它是不需要事務(wù)的。一般來說,問這個問題的人是想強調(diào)點和邊之間的事務(wù),像插入邊時看點是否存在,或者刪除點時刪除對應(yīng)邊。目前 Nebula 的懸掛點的設(shè)計是出于性能上的考慮。如果要解決上面的問題的話,會引入完整的事務(wù),但這樣性能會有個數(shù)量級的遞減。順便提下,剛說到 TOSS 是鏈式形式同步信息,上面也提到能這樣做的原因是因為第一個節(jié)點能完整拼出第二個節(jié)點的數(shù)據(jù)。但鏈式的話對完整的事務(wù)而言,性能下降會更嚴重,所以未來事務(wù)這塊的設(shè)計不會采納這種方式。
數(shù)據(jù)膨脹問題
首次導(dǎo)入數(shù)據(jù)是怎么存儲的,因為我發(fā)現(xiàn)首次導(dǎo)入數(shù)據(jù)磁盤占用會較多?
大家發(fā)現(xiàn)如果磁盤占用高,一般來說是 WAL 文件比較多。因為我們導(dǎo)入的數(shù)據(jù)量一般比較大,這會產(chǎn)生大量的 wal,在 Nebula 中默認的 wal ttl 是 4 個小時,在這 4 個小時中系統(tǒng)的 WAL 日志是完全不會刪除的,這就導(dǎo)致占用的磁盤空間會非常大。此外,RocksDB 中也會寫入一份數(shù)據(jù),相比后續(xù)集群正常運行一段時間,這時候磁盤占用會很高。對應(yīng)的解決方法也比較簡單,導(dǎo)入數(shù)據(jù)時調(diào)小 wal ttl 時間,比如只存半小時或者一個小時,這樣磁盤占用率就會減少。當(dāng)然磁盤空間夠大你不做任何處理使用默認 4 小時也 ok。因為過了若干個小時后,有一個背景線程會不斷去檢查哪些 wal 可以刪掉了,比如說默認值 4 個小時之后,一旦發(fā)現(xiàn)時過期的 wal 系統(tǒng)便會刪掉。
除了初次導(dǎo)入會有個峰值之外,線上業(yè)務(wù)實時寫入數(shù)據(jù)量并不會很大,wal 文件也相對小。這里不建議手動刪 wal 文件,因為可能會出問題正常按照 ttl 來自動刪除就行。
compact 都做了什么事可以提高查詢,也減小了數(shù)據(jù)存儲占用? 可以看下 RocksDB 介紹和文章,簡單說下 Compaction 主要是多路歸并排序。RocksDB 是 LSM-Tree 樹結(jié)構(gòu),寫入是 append-only 只會追加地寫,這會導(dǎo)致數(shù)據(jù)存在一定的冗余。Compaction 就是用來降低這種冗余,以 sst 作為輸入,進行歸并排序和去除冗余數(shù)據(jù),最后再輸出一些 sst。在這個輸入輸出過程中,Compaction 會檢查同一個 key 是否出現(xiàn)在 LSM 中的不同層,如果同一個 key 出現(xiàn)了多次會只保留最新的 key,老 key 刪掉,這樣提高了 sst 有序的程度,同時 sst 數(shù)量和 LSM-Tree 的層數(shù)可能會減小,這樣查詢時候需要讀取的 sst 數(shù)量就會減少,提高查詢效率。
磁盤容量本身不均怎么處理
不同大小的磁盤是否考慮按百分比占用,因為我使用兩塊不同大小的磁盤,一塊占滿之后導(dǎo)數(shù)就出現(xiàn)問題了
目前是不太好做,主要原因是存儲 partition 分布查找是按照輪循形式進行的,另外一個原因是 Nebula 進行 Hash 分片,各個數(shù)據(jù)盤數(shù)據(jù)存儲大小趨近。這會導(dǎo)致如果兩個數(shù)據(jù)盤大小不一致,一個盤先滿了后面的數(shù)據(jù)就寫入不進去。解決方法可以從系統(tǒng)層進行處理,直接把兩塊盤綁成同一塊盤,以同樣一個路徑掛載。
Nebula 的 RocksDB “魔改”
Nebula 的 RocksDB 存儲中,是通過列 column family 來區(qū)別 vertex 屬性嗎?
目前來說,其實我們完全沒有用 column family,只用了default column family。后續(xù)可能會用,但是不會用來區(qū)分 vertex 屬性,而是把不同 partition 數(shù)據(jù)分到不同 column family,這樣的好處是直接物理隔離。
Nebula 的魔改 wal 好像是全局 multi-raft 的 wal,但是在目錄上體現(xiàn)出來的好像每個圖空間都是單獨的 wal,這個原理是啥? 首先,Nebula 的確是 multi-raft,但沒有全局 wal 的概念。Nebula 的 wal 是針對 partition 級別的,每個 partition 有自己的 wal,并不存在 space 的 wal。至于為啥這么設(shè)計,相對來說現(xiàn)在實現(xiàn)方式比較容易,雖然會存在性能損耗,像多個 wal 的話磁盤寫入就是個隨機寫入。但是對 raft 而言,寫入瓶頸并不是在這而是系統(tǒng)的網(wǎng)絡(luò)開銷,用戶的復(fù)制操作 replication 開銷是最大的。
Nebula 社區(qū)首屆征文活動進行中! 獎品豐厚,全場景覆蓋:擼碼機械鍵盤??、手機無線充、健康小助手智能手環(huán)??,更有數(shù)據(jù)庫設(shè)計、知識圖譜實踐書籍 等你來領(lǐng),還有 Nebula 精致周邊送不停~
歡迎對 Nebula 有興趣、喜鉆研的小伙伴來書寫自己和 Nebula 有趣的故事呀~
交流圖數(shù)據(jù)庫技術(shù)?加入 Nebula 交流群請先填寫問卷系統(tǒng),Nebula 小助手會拉你進群~~






