圖數(shù)據(jù)庫(kù)的性能和 schema 的設(shè)計(jì)息息相關(guān),但是 Nebula Graph 官方本身對(duì)圖 schema 的設(shè)計(jì)其實(shí)沒(méi)有一個(gè)定論,唯一的共識(shí)就是面向性能去做 schema 設(shè)計(jì)。?
背景知識(shí)
先來(lái)講解下存儲(chǔ)背景,再講 schema 設(shè)計(jì)中會(huì)遇到的問(wèn)題,最后講下實(shí)踐過(guò)程中我們能達(dá)成一致的最佳實(shí)踐。
在使用圖數(shù)據(jù)庫(kù)之前,先了解下圖數(shù)據(jù)庫(kù)這個(gè) NoSQL 數(shù)據(jù)庫(kù)同關(guān)系型數(shù)據(jù)庫(kù)不一樣的地方。
關(guān)系型數(shù)據(jù)庫(kù)存儲(chǔ)結(jié)構(gòu)
以上圖為例,存一個(gè) ID 作為一個(gè)主鍵,然后它有個(gè)特征 k,我們對(duì) k 創(chuàng)建索引進(jìn)行查詢(xún),對(duì)于左下角這份列表數(shù)據(jù),內(nèi)存中存儲(chǔ)的話,會(huì)以一個(gè) B+ 樹(shù)進(jìn)行存儲(chǔ)(上圖右側(cè)):一個(gè)主索引 ID 和一個(gè)從索引 k。舉個(gè)例子,現(xiàn)在我們要查詢(xún) k=3 的數(shù)據(jù),它就先查詢(xún) ID=100 然后經(jīng)過(guò)回表后回到具體的值。
這體現(xiàn)了關(guān)系型數(shù)據(jù)庫(kù)的一個(gè)特點(diǎn),如果你要查詢(xún)速度快,那就需要?jiǎng)?chuàng)建一個(gè)索引。假如你不創(chuàng)建索引,那數(shù)據(jù)庫(kù)就會(huì)掃全表。
我們?cè)賮?lái)看下寫(xiě)過(guò)程。數(shù)據(jù)一般先寫(xiě)到內(nèi)存 Mem(這是一個(gè)常規(guī)的優(yōu)化減小磁盤(pán)壓力),寫(xiě)到一定程度再同步到磁盤(pán)中,這個(gè)過(guò)程我們叫原位刷盤(pán),刷盤(pán)就是說(shuō)找到這個(gè)地方的數(shù)據(jù),然后修改掉數(shù)據(jù),即原位修改。
如果你之前熟悉 MySQL 或者是其他關(guān)系型數(shù)據(jù)庫(kù),這套原理應(yīng)該是比較熟悉的。
而相對(duì)應(yīng)的,用傳統(tǒng)的數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)圖功能的話,代價(jià)比較大,下圖便展示了它的實(shí)現(xiàn)弊端:
現(xiàn)在有個(gè)場(chǎng)景,現(xiàn)在我們有某個(gè)人(上圖 Person 表),我們要找朋友的朋友(上圖的 PersonFriend 表),在關(guān)系型數(shù)據(jù)庫(kù)中便是兩級(jí)索引,先查 Person 表索引找到 Person ID,再查 PersonFriend 表通過(guò) ID 找到對(duì)應(yīng)的人,就是一個(gè) JOIN 查詢(xún)過(guò)程。如果這里使用的是 B+ 樹(shù),那么程序復(fù)雜度便是 O(logn);如果是這里的多級(jí)大小表,在笛卡爾積上即 O(n*m),都加索引有一定程度優(yōu)化,但查詢(xún)這種多級(jí)關(guān)系的話,到了一定程度會(huì)遇到系統(tǒng)“爆炸”,無(wú)法進(jìn)行相關(guān)查詢(xún)。
LSM 存儲(chǔ)模型
本文主題是圖的高性能設(shè)計(jì),主要基于 Nebula Graph 來(lái)講解。這里部分存儲(chǔ)細(xì)節(jié)同 Neo4j 會(huì)略有不同。
Nebula Graph 存儲(chǔ)模型采用了 LSM 存儲(chǔ)模型,同上面我們講的原位修改不同,LSM 模型是先寫(xiě)內(nèi)存,寫(xiě)到一定程度之后再寫(xiě)入到對(duì)應(yīng)磁盤(pán)中,每次都是增量順序?qū)憽SM 模型是一個(gè)多級(jí)模型,第一層是 L0,第二層是 L1,一般默認(rèn)是 7 層。
這里引用網(wǎng)圖來(lái)講解下 LSM 層級(jí)結(jié)構(gòu):
上圖的 L0 層其實(shí)有重復(fù)數(shù)據(jù),像上圖的 1-68 的 key 在 L0 層的 2-37,以及 23-48,其實(shí)這兩步數(shù)據(jù)是存在重疊的;但 L1 層的數(shù)據(jù)就不存在重疊情況了,1-12、15-25…要最大地發(fā)揮圖性能的話,先得了解它的寫(xiě)入過(guò)程。LSM 模型的寫(xiě)是順序?qū)?,即不?huì)進(jìn)行上文說(shuō)到的原位修改。不管是寫(xiě)入新數(shù)據(jù)還是更新原來(lái)數(shù)據(jù),永遠(yuǎn)是在后面插入新的數(shù)據(jù)(參考上圖右側(cè)深藍(lán)色數(shù)據(jù))。這樣設(shè)計(jì)的好處在于,寫(xiě)入數(shù)據(jù)就不需要找之前的數(shù)據(jù),一旦涉及查找數(shù)據(jù)就會(huì)慢,這樣設(shè)計(jì)提升了寫(xiě)速度。
但這也會(huì)帶來(lái)一個(gè)問(wèn)題:我們寫(xiě)入重復(fù)的數(shù)據(jù),或是寫(xiě)入的數(shù)據(jù)越來(lái)越多,查詢(xún)會(huì)不會(huì)受影響呢?我們來(lái)看看 LSM 是如何查數(shù)據(jù)的。LSM 進(jìn)行數(shù)據(jù)查詢(xún)時(shí),先查內(nèi)存,內(nèi)存里沒(méi)有數(shù)據(jù)再查不可變區(qū)域(Immutable Memtable),沒(méi)有的話,再往下一級(jí)級(jí)地查(參考上圖左側(cè)部分)。所以,重復(fù)的數(shù)據(jù)越多,或者磁盤(pán)數(shù)據(jù)越多,便會(huì)越慢。
所以為了保證寫(xiě)入和查詢(xún)性能,無(wú)論我們?cè)O(shè)計(jì)屬性還是其他 schema,都要控制寫(xiě)入量,也就是 LSM 的寫(xiě)入不能是無(wú)限制追加,它有一個(gè)定時(shí)的合并操作,定期地將重復(fù)數(shù)據(jù)進(jìn)行合并,叫做 Compaction。
Compaction 過(guò)程也需要控制。合并數(shù)據(jù)能減小數(shù)據(jù)量,但同時(shí) Compaction 會(huì)帶來(lái)磁盤(pán)壓力,磁盤(pán)壓力過(guò)大,讀操作速度也會(huì)變慢。綜合來(lái)看,Compaction 是一個(gè)寫(xiě)入平衡的過(guò)程。
Nebula Graph 存儲(chǔ)結(jié)構(gòu)和索引
下面再來(lái)了解下 Nebula Graph?本身的存儲(chǔ)結(jié)構(gòu)和索引。
Nebula Graph 本身是分布式數(shù)據(jù)庫(kù),因?yàn)楸阌诶斫膺@里剔除了相關(guān)的分布式結(jié)構(gòu)。簡(jiǎn)單來(lái)了解下 Nebula Graph 的結(jié)構(gòu),上面提到過(guò)的 LSM 其實(shí)是 KV(key value)存儲(chǔ),所以我們圖里存儲(chǔ)點(diǎn)、邊、索引在磁盤(pán)上都是 KV 結(jié)構(gòu)。我們可以看到上圖左側(cè)(紫色部分)有個(gè) vid 帶著出邊(out)和入邊(in)以及相關(guān)屬性。再看下上圖右側(cè)部分(紫色部分),可以看到一條邊的兩個(gè)點(diǎn)是存儲(chǔ)在一起的,對(duì)應(yīng)的點(diǎn)屬性序列化保存。相當(dāng)于說(shuō),KV 結(jié)構(gòu)中的 key 便是我們的點(diǎn)的 vid,然后 value 便是屬性的序列化結(jié)構(gòu)。因?yàn)槭切蛄谢慕Y(jié)構(gòu),所以你的屬性名是什么便會(huì)存成什么,比如這里原始數(shù)據(jù) name 字段,它改命名為 family_name,實(shí)際存儲(chǔ)就是序列化后的 family_name,也就是屬性名越長(zhǎng),存儲(chǔ)量越大。除了屬性名之外,其實(shí)屬性值也會(huì)導(dǎo)致存儲(chǔ)量增大。舉個(gè)例子,現(xiàn)在有個(gè)人(點(diǎn)),他的生平介紹要不要放在屬性里進(jìn)行存儲(chǔ)?答案是:不應(yīng)該。因?yàn)槟愕纳浇榻B會(huì)很長(zhǎng),這就會(huì)導(dǎo)致 LSM 的存儲(chǔ)壓力會(huì)很大。無(wú)論是 Compaction 還是讀寫(xiě),都會(huì)有很大的壓力。類(lèi)似比如存儲(chǔ)進(jìn)程實(shí)體,對(duì)應(yīng)的進(jìn)程描述文本也較大,會(huì)帶來(lái)較大存儲(chǔ)壓力。
再來(lái)說(shuō)下我們的邊,Nebula Graph?中出邊和入邊保存在一個(gè) KV 結(jié)構(gòu)中(參考上圖右側(cè)橙色部分)。Nebula Graph 中有個(gè)詞叫做前綴掃描,具體來(lái)說(shuō)便是現(xiàn)在要查找某個(gè) vid 對(duì)應(yīng)的邊,它是如何查找的呢?先按照 vid 來(lái)前綴掃描,在內(nèi)存中這個(gè)過(guò)程是個(gè)二分查找,所以 Nebula Graph 查詢(xún)快就是在這里。在 Neo4j 里面這種叫做“免索引鄰接”。像上面的朋友的朋友的場(chǎng)景,傳統(tǒng)數(shù)據(jù)庫(kù)是通過(guò)索引進(jìn)行查找的,而在這里直接掃描找尋某個(gè)人便可。在物理存儲(chǔ)這塊,點(diǎn)(人和相關(guān)的人)都是存儲(chǔ)在一起的,找到了某個(gè)人便找到了他的朋友。查詢(xún)上速度非???,這也是原生圖數(shù)據(jù)庫(kù)帶來(lái)的好處。
除了上面的存儲(chǔ)結(jié)構(gòu),索引也是高性能 schema 設(shè)計(jì)的一個(gè)作用因素。像上圖的右側(cè)部分,上面的紫色部分存儲(chǔ)著點(diǎn),這里有 2 個(gè)點(diǎn):第一個(gè)點(diǎn)是 vid1,name 是 wen,age 是 20;另外一個(gè)是 vid2,name 是 wei,age 是 20。這里我們創(chuàng)建了 2 個(gè)索引,一個(gè)是針對(duì) name,一個(gè)是針對(duì) age。這兩個(gè)索引的存儲(chǔ)結(jié)構(gòu)參考上圖右側(cè)下方的白色部分,查找 name 為 wen 的數(shù)據(jù)時(shí),按照上面我們科普過(guò)的會(huì)進(jìn)行二分查找,掃描到對(duì)應(yīng)的 name 索引的 wen 數(shù)據(jù),然后再?gòu)乃饕龜?shù)據(jù)中找到對(duì)應(yīng)的點(diǎn)(vid1)數(shù)據(jù),再借助 vid 數(shù)據(jù)來(lái)找尋它的相關(guān)信息。這里 vid 找關(guān)聯(lián)數(shù)據(jù)的原理同上面的存儲(chǔ)結(jié)構(gòu)描述。
小結(jié)
小結(jié)下 Nebula Graph 存儲(chǔ)結(jié)構(gòu)和索引,在這里關(guān)系是一等公民,索引輔助查詢(xún)(并非用來(lái)提速),重要的是抽象關(guān)系。
schema 設(shè)計(jì)
進(jìn)入本文的重點(diǎn)——schema 的設(shè)計(jì),schema 設(shè)計(jì)的三大基本原則:
尊重領(lǐng)域?qū)嶓w關(guān)系
以性能為目標(biāo)
考慮可視化分析
而三者并不沖突,上面三點(diǎn)其中某一點(diǎn)做得很好,另外兩點(diǎn)也會(huì)做的不錯(cuò)。
Talking is cheap,下面我們來(lái)結(jié)合具體的例子來(lái)了解下三大原則。這些 case 圖主要引用自 Neo4j,但是對(duì)于 Nebula Graph 相關(guān)的 schema 設(shè)計(jì)也有參考意義。
實(shí)體和關(guān)系的選擇
上圖是 Neo4j 圖數(shù)據(jù)庫(kù)書(shū)籍中的示例圖。簡(jiǎn)單描述下這個(gè)場(chǎng)景,Bob 和 Charlie 等人在發(fā)郵件。那你設(shè)計(jì)這么一個(gè)場(chǎng)景的 schema 是否很自然就會(huì)將發(fā)郵件變成關(guān)系邊?因?yàn)?Bob 同 Charlie 發(fā)郵件,不是很明顯就是發(fā)郵件關(guān)系嗎?那我們來(lái)回顧下上面說(shuō)的三大原則第一點(diǎn):尊重領(lǐng)域?qū)嶓w關(guān)系。Bob 和 Charlie 建立聯(lián)系自然不是通過(guò)發(fā)郵件這個(gè)行為,而是通過(guò)郵件本身來(lái)建立聯(lián)系,所以這里便缺少了一個(gè)實(shí)體。在考慮可視化分析原則這邊,你要分析實(shí)體之間的關(guān)系,你思考它們是通過(guò)什么來(lái)建立的聯(lián)系。這時(shí)候就會(huì)發(fā)生之前提到過(guò)的發(fā)郵件設(shè)置為邊的情況(把郵件放置在邊上),單看 Bob 的話(左圖),我們可以清楚地看到發(fā)郵件這個(gè)動(dòng)作。左圖上面部分,Bob Emailed Charlie。但如果這時(shí)候,要查看這個(gè)郵件抄送給了誰(shuí),還有這封郵件有哪些相關(guān)人,像左圖的 schema 就不能很好地進(jìn)行查詢(xún)。因?yàn)槿鄙倭?Email 這個(gè)實(shí)體。而上圖右側(cè)部分便能可以方便地找尋相關(guān)信息。
下面再來(lái)講下如何進(jìn)行實(shí)體和屬性選擇。
實(shí)體和屬性選擇
在這個(gè)部分,我將結(jié)合青藤云的情況來(lái)講一個(gè)我們的 case——進(jìn)程之間的父子關(guān)系。
如上圖左側(cè)所示,md5 為 1 的 pid 100 進(jìn)程起了一個(gè) pid 102 的子進(jìn)程,這個(gè)子進(jìn)程的 md5 是 2。同時(shí),md5 也為 1 的 pid 101 也起了進(jìn)程,pid 為 103、md5 為 3。按照我們之前的實(shí)現(xiàn)方法,是在 md5 上創(chuàng)建索引,繼而建立起跟 pid 102、pid 103 的聯(lián)系。但這種做法,上面講過(guò)性能并不高,免索引復(fù)雜是 O(1),而這種做法的復(fù)雜度是 O(logn)。所以說(shuō),我們這時(shí)候應(yīng)該基于 ProcessFile 進(jìn)程文件 md5 來(lái)建立關(guān)系(進(jìn)程間是基于 md5 聯(lián)系起來(lái)的):我們先抽取 md5 建立一個(gè)名叫 ProcessFile 的實(shí)體,屬性是 md5。如果我們要查詢(xún)指定進(jìn)程所關(guān)聯(lián)的進(jìn)程,很直觀地去找尋和這個(gè) ProcessFile 關(guān)聯(lián)的進(jìn)程就可以分析出來(lái)我們要的結(jié)果。舉個(gè)例子,pid 102 的進(jìn)程是一個(gè)木馬,我想找尋是哪個(gè)父進(jìn)程釋放的它,或者是同它父進(jìn)程同 md5 文件的進(jìn)程,該怎么找?
上圖的展示了兩種形式,第一種(左側(cè))的話就需要找索引;第二種(右側(cè))通過(guò) CREATE_PROCESS 就可以直接找到 pid 102 的父進(jìn)程 pid 100,再通過(guò) PFILE_OF 關(guān)系你可以找到它同 md 文件的進(jìn)程 pid 101。
好的,簡(jiǎn)單結(jié)合 schema 設(shè)計(jì)三大原則來(lái)回顧下這個(gè) case:
屬性上創(chuàng)建索引會(huì)影響寫(xiě)入,此外屬性放在 ProcessFile 還是放在 Process 中,存儲(chǔ)性能是不一樣的。這里主要涉及到寫(xiě)入量,因?yàn)?Process 進(jìn)程是一直可以不停地啟動(dòng),但是 md5 文件可能本身并不多。如果是放在 Process 中,進(jìn)程起得越多,數(shù)據(jù)寫(xiě)入量也就會(huì)越大,進(jìn)而查詢(xún)壓力也會(huì)增大查詢(xún)變慢。
可視化探索這塊主要和不定需求有關(guān)。因?yàn)橐婚_(kāi)始我們?cè)O(shè)計(jì) schema 的時(shí)候可能并沒(méi)有全方位考量,或者說(shuō)像是一些安全、防作弊規(guī)則并未擬定,不知道它會(huì)是什么樣。而這時(shí)你要根據(jù)這種不確定來(lái)設(shè)計(jì) schema,就需要將圖“釋放”給相關(guān)業(yè)務(wù)人員,讓他在圖里點(diǎn)擊,設(shè)計(jì)他的關(guān)系,所以相對(duì)應(yīng)的我們就不能通過(guò)索引來(lái)實(shí)現(xiàn)這種需求,因?yàn)闃I(yè)務(wù)人員可能沒(méi)有相關(guān)的技術(shù)背景。
添加屬性
上圖左邊描述文字截自 Nebula Graph v2.0 的官方文檔:
https://docs.nebula-graph.com.cn/2.0/3.ngql-guide/1.nGQL-overview/2.graph-modeling/#_3。
在合理設(shè)置邊屬性的第二部分提到,“為邊創(chuàng)建屬性時(shí)請(qǐng)勿使用長(zhǎng)字符串”。這個(gè)和我們之前提到過(guò)的,屬性名和屬性值都應(yīng)該短,不應(yīng)該長(zhǎng)是一個(gè)意思。像上圖右側(cè)部分,很明顯可以看到 vid 重復(fù)寫(xiě)多次的話,每次寫(xiě)就是重復(fù)的流量和存儲(chǔ),這會(huì)大大增大內(nèi)存占用和磁盤(pán)容量。如果我們把 session_guid 變成 sid 會(huì)節(jié)約很多存儲(chǔ)。而后面的描述信息,也有兩種處理方式。第一種,直接刪除描述;第二種,將過(guò)長(zhǎng)的描述存儲(chǔ)在外部,比如放置在 Elasticsearch,然后將 ES 存儲(chǔ)這塊內(nèi)容的 eid 存儲(chǔ)在上圖的 value 中。這樣也可以大大減少存儲(chǔ)量,提升寫(xiě)入 / 查詢(xún)性能。
除了這點(diǎn)之外,我們還要注意合理設(shè)置分組標(biāo)簽。青藤云暫時(shí)沒(méi)遇到類(lèi)似 case,所以這里講下這句話什么意思。簡(jiǎn)單來(lái)說(shuō),就是寫(xiě)入這邊需要做一個(gè) tag 的區(qū)分,結(jié)合上文提到的二分查找,你就比較好理解了。舉個(gè)簡(jiǎn)單例子,這里有個(gè)人,他的公司相關(guān)信息,或者年齡相關(guān)的信息,或者是個(gè)人喜好之類(lèi)的信息用相關(guān)的 tag 區(qū)分開(kāi),這樣查詢(xún)時(shí)可以更快地找到對(duì)應(yīng)的信息。
最后回到文檔「合理設(shè)置邊屬性」中第一部分中的“深度圖遍歷的性能較低,為了減少遍歷深度,請(qǐng)使用點(diǎn)屬性代替邊。例如,模型 a 包括姓名、年齡、眼睛顏色三種屬性,建議您創(chuàng)建一個(gè)標(biāo)簽 person,然后為它添加姓名、年齡、眼睛顏色的屬性。”,按照官方舉的例子,固然是這樣的。但實(shí)際應(yīng)用中,并非一定要遵循這一原則——屬性用點(diǎn)屬性而不是用邊,該用實(shí)體的時(shí)候還是得用實(shí)體。所以我這里下面?zhèn)渥?xiě)了:描述實(shí)體本身特性。像實(shí)體本身的特性 age / status,邊的 time / count 這些屬性會(huì)變成相對(duì)應(yīng)的屬性,這樣能更好地描述本質(zhì)特性,也能起到比較好的輔助效果。
添加索引
借助之前我們的實(shí)踐經(jīng)驗(yàn),來(lái)講下索引這塊內(nèi)容。在 Nebula Graph 的官方文檔中提及了:盡量少用索引。那么問(wèn)題來(lái)了,到底什么時(shí)候應(yīng)該用索引呢?我們先從原理上來(lái)解釋下索引。在上圖的例子中,value 中存儲(chǔ)了 2 樣?xùn)|西:一個(gè)是 status,狀態(tài);另外一個(gè)是 ip。右側(cè)的表格是對(duì)應(yīng)的 KV 存儲(chǔ)結(jié)構(gòu),key 是個(gè)點(diǎn)結(jié)構(gòu)。給點(diǎn)加索引之后,它便會(huì)變成左側(cè)表格的結(jié)構(gòu),idx-x-vid1。如果我們要查詢(xún) status 等于 0 的這列值的時(shí)候,由于加了索引之后數(shù)據(jù)結(jié)構(gòu)是以 0(status)為前綴,vid 放在 0 后面;如果我們要查詢(xún) ip 的話,存儲(chǔ)結(jié)構(gòu)則將 ip 變成前綴,vid 存儲(chǔ)在后面。這樣會(huì)產(chǎn)生何種問(wèn)題呢?status 如果只有 1 和 0,現(xiàn)在你有 1 萬(wàn)億的點(diǎn),這樣添加索引是沒(méi)有意義的。而且,因?yàn)?Nebula Graph 的查找是二分查找,復(fù)雜度收斂到 O(n),相當(dāng)于有多少數(shù)據(jù)就查多少數(shù)據(jù)。即便你添加了一個(gè) limit,但是在 Nebula Graph 這邊(注:本次分享時(shí),Nebula Graph 的最新版本為 v2.0.1)limit 并沒(méi)有下推,所以所有數(shù)據(jù)會(huì)先撈上來(lái)到計(jì)算層,在內(nèi)存中使用 limit 進(jìn)行數(shù)據(jù)過(guò)濾。
正是由于這種情況,所以在 v2.5 之前的 Nebula Graph 用戶會(huì)經(jīng)常在論壇反饋 OOM 問(wèn)題,其實(shí)就是內(nèi)存爆炸。
所以說(shuō),索引應(yīng)該是盡可能和業(yè)務(wù)相關(guān)的標(biāo)識(shí)。
細(xì)粒度關(guān)系和通用關(guān)系
通過(guò)上面的 Neo4j 這個(gè) case 我們來(lái)講解下顆粒度問(wèn)題。
像上面的人有 2 個(gè)地址,一個(gè)是收件地址,另外一個(gè)是付款地址。如果此時(shí),我們想找尋這個(gè)人的地址,如果沒(méi)有 ADDRESS 這個(gè)通用標(biāo)簽的話,DELIVERY_ADDRESS 和 BILLING_ADDRESS 這兩個(gè)關(guān)系都得查下。這時(shí)候如果用的是二分查找,如果這堆關(guān)系本身存儲(chǔ)在一起還好,可以一次性查找出來(lái);但,如果關(guān)系不在一起,就需要分 2 次查詢(xún),這會(huì)降低它的查詢(xún)速率。
因此,我們可以再創(chuàng)建一個(gè)通用標(biāo)簽,但是要注意的是,標(biāo)簽的建立是基于對(duì)某個(gè)業(yè)務(wù)有強(qiáng)需求。像上面的例子,需要知道用戶的所有地址,也要知道他的單獨(dú)地址,比如:收件地址。這種情況下,建立一個(gè)通用標(biāo)簽才是一個(gè)加速的方法,但注意要謹(jǐn)慎使用。同樣的,通用標(biāo)簽設(shè)計(jì)時(shí),也需要考慮可視化的情況。
加速查詢(xún)
之前我們講過(guò)一個(gè)發(fā)郵件的例子,但是現(xiàn)在場(chǎng)景有所變化了,我現(xiàn)在不關(guān)心發(fā)郵件這個(gè)事情,我只關(guān)心人和人之間的關(guān)系,比如,wen 這個(gè)人的聯(lián)系關(guān)系,有誰(shuí)和他聯(lián)系過(guò),而這個(gè)聯(lián)系方式可能是 Email,也可能是手機(jī)(Phone),或者是微信。這時(shí)候我應(yīng)該如何設(shè)計(jì) schema 呢?當(dāng)然之前的設(shè)計(jì)是可以沿用的,但為了加速查詢(xún),滿足業(yè)務(wù)上的需求。這里加了 CONTACT 屬性,用來(lái)加速查找。
小結(jié) schema 設(shè)計(jì)
講到這里,我們總結(jié)下上面的例子,其實(shí)我們的例子都是圍繞著三大原則來(lái)展開(kāi)的,即:性能、可視化、領(lǐng)域關(guān)系。
?
典型 schema 設(shè)計(jì)
下面來(lái)我們來(lái)講下有些典型場(chǎng)景下的 schema 設(shè)計(jì)。
時(shí)間設(shè)計(jì)
現(xiàn)在有個(gè)場(chǎng)景,有一堆發(fā)生過(guò)的事件,現(xiàn)在想查詢(xún)?cè)谀硞€(gè)月,或者是某個(gè)時(shí)間段內(nèi),發(fā)生了哪些事件,我們?cè)撊绾卧O(shè)計(jì) schema 呢?也許我們可以在時(shí)間屬性上創(chuàng)建個(gè)索引,把這個(gè)時(shí)間當(dāng)作索引來(lái)存儲(chǔ),但這樣的話,查詢(xún)速度不會(huì)很快,尤其是數(shù)據(jù)量較大的情況下。那我們應(yīng)該怎么做呢?Neo4j 給了一種設(shè)計(jì)思路叫做時(shí)間樹(shù),就是說(shuō)時(shí)間本身是有層級(jí)關(guān)系的。如上圖所示,時(shí)間有個(gè)層級(jí),想要查詢(xún)某個(gè)事件同時(shí)間段內(nèi)的其他事件,可以通過(guò)這個(gè)層級(jí)快速找到。
上圖右側(cè)則是一個(gè)時(shí)序關(guān)系,可以快速找尋某事件發(fā)生的時(shí)間前后有哪些事情發(fā)生,而在 Nebula Graph 中,你可以通過(guò) rank 來(lái)實(shí)現(xiàn)時(shí)序圖功能。
上面的例子只是給大家一個(gè)參考,并不代表會(huì)應(yīng)用在青藤云實(shí)際業(yè)務(wù)中。
地址設(shè)計(jì)
上面這個(gè)是地址的設(shè)計(jì),可能大家都會(huì)遇到。假如,現(xiàn)在我們要查詢(xún)北京朝陽(yáng)太陽(yáng)宮在發(fā)生事件 A 時(shí),同一個(gè)地理位置有多少用戶 / IP 在這。傳統(tǒng)的設(shè)計(jì)方法中,添加屬性是無(wú)法滿足該業(yè)務(wù)需求的。那怎么實(shí)現(xiàn)呢?其實(shí)這些地址劃分可以作為實(shí)體,而且地址之間是有關(guān)系的。以上述的物流為例,上面的例子:中國(guó)-北京-朝陽(yáng)-太陽(yáng)宮,就可以通過(guò)集散中心-派送點(diǎn)-派送區(qū)域-派送段形式進(jìn)行查詢(xún)。如果你要查詢(xún)同一個(gè)街道或者是同個(gè)市,也可以按照這個(gè)關(guān)系快速進(jìn)行查詢(xún)。
像我們遇到的地址位置,或者是網(wǎng)絡(luò)層問(wèn)題,都可以參考這種設(shè)計(jì)。之前在 BOSS 直聘(分享嘉賓曾就職 BOSS 直聘)中,我們就是參考了類(lèi)似的實(shí)現(xiàn)來(lái)找尋某個(gè)區(qū)域的相關(guān)用戶。
?
圖最佳實(shí)踐
上面講述的內(nèi)容主要是圍繞 schema 設(shè)計(jì),下面這塊當(dāng)作補(bǔ)充資料,主要講的是圖的最佳實(shí)踐。
命名規(guī)范
如果你要編寫(xiě)一個(gè)比較長(zhǎng)的語(yǔ)句,不知道你有沒(méi)有注意過(guò),這個(gè)語(yǔ)句該如何快速區(qū)分哪些是實(shí)體,哪些是關(guān)系,哪些又是屬性。所以,這里就要提一下命名規(guī)范問(wèn)題。一旦命名規(guī)范了,一條長(zhǎng)查詢(xún)語(yǔ)句也可能快速辨別實(shí)體、關(guān)系、屬性。
你可以參考下面的命名規(guī)范:
實(shí)體采用駝峰方式,例如:User、Email、Process;
關(guān)系采用全部大寫(xiě),包含動(dòng)詞和副詞,例如:HAS_IP;
屬性采用英文小寫(xiě)簡(jiǎn)寫(xiě),例如:title、sid、pid
圖計(jì)算
上圖給出了圖數(shù)據(jù)庫(kù)和圖計(jì)算的工作流,可以直觀地查看到二者的區(qū)別。圖數(shù)據(jù)庫(kù)的工作流相對(duì)簡(jiǎn)單,拿我們常見(jiàn)的一個(gè)場(chǎng)景舉例,已知某個(gè)有問(wèn)題的進(jìn)程 A,要溯源找尋它的源頭。對(duì)應(yīng)到圖這邊,圖數(shù)據(jù)庫(kù)的查詢(xún)一般會(huì) GO / LOOKUP / MATCH / FETCH 錨定某個(gè)起始點(diǎn),比如這里的進(jìn)程 A,然后管道 / WITCH 進(jìn)行下一步的處理,最后用 RETURN / YIELD 來(lái)返回基本結(jié)構(gòu)。但,注意,這個(gè)基本結(jié)構(gòu)會(huì)進(jìn)行二次加工。剛設(shè)計(jì) schema 的時(shí)候提到過(guò),并不是所有的屬性都會(huì)設(shè)計(jì)進(jìn)去,只有和業(yè)務(wù)相關(guān)的核心屬性才會(huì)設(shè)計(jì)進(jìn)入。像請(qǐng)求接口之類(lèi)的操作,都會(huì)在下一步過(guò)濾 / 擴(kuò)展處理時(shí)完成。
上面說(shuō)的是圖的直接業(yè)務(wù)簡(jiǎn)單查詢(xún),但還有一種場(chǎng)景是用圖來(lái)進(jìn)行機(jī)器學(xué)習(xí),比如 GNN 和 GCN 用圖來(lái)做特征,這塊本文就不展開(kāi)講述,流程和上面有所不同。
那,什么時(shí)候用圖數(shù)據(jù)庫(kù),什么時(shí)候用圖計(jì)算呢?
如上圖所示,有限點(diǎn)的拓展就比較適合用圖數(shù)據(jù)庫(kù),或者說(shuō) Nebula Graph 來(lái)實(shí)現(xiàn);而全局挖掘就比較適合用圖計(jì)算。從圖計(jì)算的流程上來(lái)看,簡(jiǎn)單粗暴地講,圖計(jì)算就是把一批數(shù)據(jù)撈到內(nèi)存中,一次性計(jì)算完,然后“吐”出來(lái),再進(jìn)行下一步的過(guò)濾和處理。至于它是如何計(jì)算的,圖計(jì)算里面配有計(jì)算引擎。
現(xiàn)在我們來(lái)問(wèn)個(gè)問(wèn)題,如果要找全圖點(diǎn)度 Top10 的點(diǎn),應(yīng)該用什么?
自然是圖計(jì)算,圖計(jì)算也就是 OLAP 主打的是吞吐,即一次性能處理多少數(shù)據(jù);而圖數(shù)據(jù)庫(kù),主要是應(yīng)對(duì) OLTP 場(chǎng)景,側(cè)重低延遲,就是查詢(xún)有多快,以及支持多大量的并發(fā)請(qǐng)求 QPS。
只要我們記住圖數(shù)據(jù)庫(kù)和圖計(jì)算各自的擅長(zhǎng)場(chǎng)景,就比較好處理相關(guān)的業(yè)務(wù)。
大圖優(yōu)化
像傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)中,業(yè)務(wù)無(wú)限膨脹的話,就需要做分庫(kù)分表。圖也是類(lèi)似的,在大圖上做某些查詢(xún)時(shí),你會(huì)發(fā)現(xiàn)性能很差,這時(shí)候你就需要進(jìn)行分圖處理。像上面說(shuō)到過(guò)的關(guān)系細(xì)化和加速查詢(xún),比如我現(xiàn)在只關(guān)心進(jìn)程關(guān)系,在特定業(yè)務(wù)場(chǎng)景下就需要將進(jìn)程關(guān)系單獨(dú)設(shè)計(jì)成一張圖。這就是圖的一個(gè)優(yōu)化手段?;蛘?,你也可以進(jìn)行業(yè)務(wù)隔離。像現(xiàn)在的業(yè)務(wù)是針對(duì)推薦場(chǎng)景,剩下的安全場(chǎng)景是否要放置在同一個(gè)圖空間下呢?如果業(yè)務(wù)量不大的情況下,是可以的。但是如果是數(shù)據(jù)量大的話,還是需要同傳統(tǒng)數(shù)據(jù)庫(kù)一樣進(jìn)行業(yè)務(wù)隔離,什么業(yè)務(wù)進(jìn)入什么圖。
這里延伸一下,分圖場(chǎng)景下如何進(jìn)行多圖查詢(xún)呢?簡(jiǎn)單來(lái)說(shuō)就是進(jìn)程一張圖,網(wǎng)絡(luò)是一張圖,這時(shí)候要查詢(xún)進(jìn)程和網(wǎng)絡(luò)的關(guān)系。業(yè)界的話,管這個(gè)叫做查詢(xún)端融合。雖然你要查詢(xún)的數(shù)據(jù)是 2 張圖,但是我假裝你是在一張圖上進(jìn)行查詢(xún)。
編輯:黃飛
?
評(píng)論
查看更多