本文主要討論圖數據庫背後的設計思路、原理還有一些適用的場景,以及在生產環境中使用圖數據庫的具體案例。
下面這張圖是一個社交網絡場景,每一個用戶能夠發微博、分享微博或評論他人的微博。這些都是最基本的增刪改查,也是大多數研發人員對數據庫作的常見操做。而在研發人員的平常工做中除了要把用戶的基本信息錄入數據庫外,還需找到與該用戶相關聯的信息,方便去對單個的用戶進行下一步的分析,好比說:咱們發現張三的帳戶裏有不少關於 AI 和音樂的內容,那麼咱們能夠據此推測出他多是一名程序員,從而推送他可能感興趣的內容。node
這些數據分析每時每刻都會發生,但有時候,一個簡單的數據工做流在實現的時候可能會變得至關複雜,此外數據庫性能也會隨着數據量的增長而銳減,好比說獲取某管理者下屬三級彙報關係的員工,這種統計查詢在如今的數據分析中是一種常見的操做,而這種操做每每會由於數據庫選型致使性能產生巨大差別。git
傳統解決上述問題最簡單的方法就是創建一個關係模型,咱們能夠把每一個員工的信息錄入表中,存在諸如 MySQL 之類的關係數據庫,下圖是最基本的關係模型:程序員
可是基於上述的關係模型,要實現咱們的需求,就不可避免地涉及到不少關係數據庫 JOIN
操做,同時實現出來的查詢語句也會變得至關長(有時達到上百行):github
(SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.pid AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT manager.pid AS directReportees, count(manager.directly_manages) AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.directly_manages AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT reportee.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT depth1Reportees.pid AS directReportees, count(depth2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM( SELECT reportee.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT L2Reportees.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT L2Reportees.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") )
這種 glue 代碼對維護人員和開發者來講就是一場災難,沒有人想寫或者去調試這種代碼,此外,這類代碼每每伴隨着嚴重的性能問題,這個在以後會詳細討論。sql
性能問題的本質在於數據分析面臨的數據量,假如只查詢幾十個節點或者更少的內容,這種操做是徹底不須要考慮數據庫性能優化的,但當節點數據從幾百個變成幾百萬個甚至幾千萬個後,數據庫性能就成爲了整個產品設計的過程當中最需考慮的因素之一。數據庫
隨着節點的增多,用戶跟用戶間的關係,用戶和產品間的關係,或者產品和產品間的關係都會呈指數增加。設計模式
如下是一些公開的數據,能夠反映數據、數據和數據間關係的一些實際狀況:跨域
以下表所示,開源的圖數據集每每有着上千萬個節點和上億的邊的數據:緩存
Data set name | nodes | edges |
---|---|---|
YahooWeb | 1.4 Billion | 6 Billion |
Symantec Machine-File Graph | 1 Billion | 37 Billion |
104 Million | 3.7 Billion | |
Phone call network | 30 Million | 260 Million |
在數據量這麼大的場景中,使用傳統 SQL 會產生很大的性能問題,緣由主要有兩個:性能優化
下表列出的是一個非官方的性能測試(社交網絡測試集,一百萬用戶,每一個大概有 50 個好友),體現了在關係數據庫裏,隨着好友查詢深度的增長而產生的性能變化:
levels | RDBMS execution time(s) |
---|---|
2 | 0.016 |
3 | 30.267 |
4 | 1543.595 |
索引:SQL 引擎經過索引來找到對應的數據。
常見的索引包括 B- 樹索引和哈希索引,創建表的索引是比較常規的優化 SQL 性能的操做。B- 樹索引簡單地來講就是給每一個人一個可排序的獨立 ID,B- 樹自己是一個平衡多叉搜索樹,這個樹會將每一個元素按照索引 ID 進行排序,從而支持範圍查找,範圍查找的複雜度是 O(logN) ,其中 N 是索引的文件數目。
可是索引並不能解決全部的問題,若是文件更新頻繁或者有不少重複的元素,就會致使很大的空間損耗,此外索引的 IO 消耗也值得考慮,索引 IO 尤爲是在機械硬盤上的 IO 讀寫性能上來講很是不理想,常規的 B- 樹索引消耗四次 IO 隨機讀,當 JOIN 操做變得愈來愈多時,硬盤查找更可能發生上百次。
緩存:緩存主要是爲了解決有具備空間或者時間局域性數據的頻繁讀取帶來的性能優化問題。一個比較常見的使用緩存的架構是 lookaside cache architecture。下圖是以前 Facebook 用 Memcached + MySQL 的實例(現已被 Facebook 自研的圖數據庫 TAO 替代):
在架構中,設計者假設用戶創造的內容比用戶讀取的內容要少得多,Memcached 能夠簡單地理解成一個分佈式的支持增刪改查的哈希表,支持上億量級的用戶請求。基本的使用流程是當客戶端需讀數據時,先查看一下緩存,而後再去查詢 SQL 數據庫。而當用戶須要寫入數據時,客戶端先刪除緩存中的 key,讓數據過時,再去更新數據庫。可是這種架構有幾個問題:
上述關係型數據庫建模失敗的主要緣由在於數據間缺少內在的關聯性,針對這類問題,更好的建模方式是使用圖結構。
假如數據自己就是表格的結構,關係數據庫就能夠解決問題,但若是你要展現的是數據與數據間的關係,關係數據庫反而不能解決問題了,這主要是在查詢的過程當中不可避免的大量 JOIN 操做致使的,而每次 JOIN 操做卻只用到部分數據,既然反覆 JOIN 操做自己會致使大量的性能損失,如何建模才能更好的解決問題呢?答案在點和點之間的關係上。
在咱們以前的討論中,傳統數據庫雖然運用 JOIN 操做把不一樣的表連接了起來,從而隱式地表達了數據之間的關係,可是當咱們要經過 A 管理 B,B 管理 A 的方式查詢結果時,表結構並不能直接告訴咱們結果。
若是咱們想在作查詢前就知道對應的查詢結果,咱們必須先定義節點和關係。
節點和關係先定義是圖數據庫和別的數據庫的核心區別。打個比方,咱們能夠把經理、員工表示成不一樣的節點,並用一條邊來表明他們以前存在的管理關係,或者把用戶和商品看做節點,用購買關係建模等等。而當咱們須要新的節點和關係時,只需進行幾回更新就好,而不用去改變表的結構或者去遷移數據。
根據節點和關聯關係,以前的數據能夠根據下圖所示建模:
經過圖數據庫 Nebula Graph 原生 nGQL 圖查詢語言進行建模,參考以下操做:
-- Insert People INSERT VERTEX person(ID, name) VALUES 1:(2020031601, ‘Jeff’); INSERT VERTEX person(ID, name) VALUES 2:(2020031602, ‘A’); INSERT VERTEX person(ID, name) VALUES 3:(2020031603, ‘B’); INSERT VERTEX person(ID, name) VALUES 4:(2020031604, ‘C’); -- Insert edge INSERT EDGE manage (level_s, level_end) VALUES 1 -> 2: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 3: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 4: ('0', '1')
而以前超長的 query 語句也能夠經過 Cypher / nGQL 縮減成短短的 三、4 行代碼。
下面爲 nGQL 語句
GO FROM 1 OVER manage YIELD manage.level_s as start_level, manage._dst AS personid | GO FROM $personid OVER manage where manage.level_s < start_level + 3 YIELD SUM($$.person.id) AS TOTAL, $$.person.name AS list
下面爲 Cypher 版本
MATCH (boss)-[:MANAGES*0..3]->(sub), (sub)-[:MANAGES*1..3]->(personid) WHERE boss.name = 「Jeff」 RETURN sub.name AS list, count(personid) AS Total
從近百行代碼變成 三、4 行代碼能夠明顯地看出圖數據庫在數據表達能力上的優點。
圖數據庫自己對高度鏈接、結構性不強的數據作了專門優化。不一樣的圖數據庫根據不一樣的場景也作了針對性優化,筆者在這裏簡單介紹如下幾種圖數據庫,BTW,這些圖數據庫都支持原生圖建模。
Neo4j 是最知名的一種圖數據庫,在業界有微軟、ebay 在用 Neo4j 來解決部分業務場景,Neo4j 的性能優化有兩點,一個是原生圖數據處理上的優化,一個是運用了 LRU-K 緩存來緩存數據。
咱們說一個圖數據庫支持原生圖數據處理就表明這個數據庫有能力去支持 index-free adjacency。
index-free adjancency 就是每一個節點會保留鏈接節點的引用,從而這個節點自己就是鏈接節點的一個索引,這種操做的性能比使用全局索引好不少,同時假如咱們根據圖來進行查詢,這種查詢是與整個圖的大小無關的,只與查詢節點關聯邊的數目有關,若是用 B 樹索引進行查詢的複雜度是 O(logN),使用這種結構查詢的複雜度就是 O(1)。當咱們要查詢多層數據時,查詢所須要的時間也不會隨着數據集的變大而呈現指數增加,反而會是一個比較穩定的常數,畢竟每次查詢只會根據對應的節點找到鏈接的邊而不會去遍歷全部的節點。
在 2.2 版本的 Neo4j 中使用了 LRU-K 緩存,這種緩存簡而言之就是將使用頻率最低的頁面從緩存中彈出,青睞使用頻率更高的頁面,這種設計保證在統計意義上的緩存資源使用最優化。
JanusGraph 自己並無關注於去實現存儲和分析,而是實現了圖數據庫引擎與多種索引和存儲引擎的接口,利用這些接口來實現數據和存儲和索引。JanusGraph 主要目的是在原來框架的基礎上支持圖數據的建模同時優化圖數據序列化、圖數據建模、圖數據執行相關的細節。JanusGraph 提供了模塊化的數據持久化、數據索引和客戶端的接口,從而更方便地將圖數據模型運用到實際開發中。
此外,JanusGraph 支持用 Cassandra、HBase、BerkelyDB 做爲存儲引擎,支持使用 ElasticSearch、Solr 還有 Lucene 進行數據索引。
在應用方面,能夠用兩種方式與 JanusGraph 進行交互:
下面簡單地介紹了一下 Nebula Graph 的系統設計。
Nebula Graph 使用了 vertexID + TagID
做爲鍵在不一樣的 partition 間存儲 in-key 和 out-key 相關的數據,這種操做能夠確保在大規模集羣上的高可用,使用分佈式的 partition 和 sharding 也增長了 Nebula Graph 的吞吐量和容錯的能力。
Storage Service 採用 shared-nothing 的分佈式架構設計,每一個存儲節點都有多個本地 KV 存儲實例做爲物理存儲。Nebula 採用多數派協議 Raft 來保證這些 KV 存儲之間的一致性(因爲 Raft 比 Paxo 更簡潔,咱們選用了 Raft)。在 KVStore 之上是圖語義層,用於將圖操做轉換爲下層 KV 操做。
圖數據(點和邊)經過 Hash 的方式存儲在不一樣 partition 中。這裏用的 Hash 函數實現很直接,即 vertex_id 取餘 partition 數。在 Nebula Graph 中,partition 表示一個虛擬的數據集,這些 partition 分佈在全部的存儲節點,分佈信息存儲在 Meta Service 中(所以全部的存儲節點和計算節點都能獲取到這個分佈信息)。
每一個計算節點都運行着一個無狀態的查詢計算引擎,而節點彼此間無任何通訊關係。計算節點僅從 Meta Service 讀取 meta 信息,以及和 Storage Service 進行交互。這樣設計使得計算層集羣更容易使用 K8s 管理或部署在雲上。
計算層的負載均衡有兩種形式,最多見的方式是在計算層上加一個負載均衡(balance),第二種方法是將計算層全部節點的 IP 地址配置在客戶端中,這樣客戶端能夠隨機選取計算節點進行鏈接。
每一個查詢計算引擎都能接收客戶端的請求,解析查詢語句,生成抽象語法樹(AST)並將 AST 傳遞給執行計劃器和優化器,最後再交由執行器執行。
在當今,圖數據庫收到了更多分析師和諮詢公司的關注
Graph analysis is possibly the single most effective competitive differentiator for organizations pursuing data-driven operations and decisions after the design of data capture. --------------Gartner「Graph analysis is the true killer app for Big Data.」 --------------------Forrester
同時圖數據庫在 DB-Ranking 上的排名也呈現出上升最快的趨勢,可見需求之迫切:
Netflix 採用了JanusGraph + Cassandra + ElasticSearch 做爲自身的圖數據庫架構,他們運用這種架構來作數字資產管理。
節點表示數字產品好比電影、紀錄片等,同時這些產品之間的關係就是節點間的邊。
當前的 Netflix 有大概 2 億的節點,70 多種數字產品,每分鐘都有上百條的 query 和數據更新。
此外,Netflix 也把圖數據庫運用在了受權、分佈式追蹤、可視化工做流上。好比可視化 Git 的 commit,jenkins 部署這些工做。
通常而言,新技術每每在開始的時候大都不被大公司所青睞,圖數據庫並無例外,大公司自己有不少的遺留項目,而這些項目自己的用戶體量和使用需求又讓這些公司不敢冒着風險來使用新技術去改變這些處於穩定的產品。Adobe 在這裏作了一個迭代新技術的例子,用 Neo4j 圖數據庫替換了舊的 NoSQL Cassandra 數據庫。
這個被大改的系統名字叫 Behance,是 Adobe 在 15 年發佈的一個內容社交平臺,有大概 1 千萬的用戶,在這裏人們能夠分享本身的創做給百萬人看。
這樣一個巨大的遺留系統原本是經過 Cassandra 和 MongoDB 搭建的,基於歷史遺留問題,系統有很多的性能瓶頸不得不解決。
MongoDB 和 Cassandra 的讀取性能慢主要由於原先的系統設計採用了 fan-out 的設計模式——受關注多的用戶發表的內容會單獨分發給每一個讀者,這種設計模式也致使了網絡架構的大延遲,此外 Cassandra 自己的運維也須要不小的技術團隊,這也是一個很大的問題。
在這裏爲了搭建一個靈活、高效、穩定的系統來提供消息 feeding 並最小化數據存儲的規模,Adobe 決定遷移本來的 Cassandra 數據庫到 Neo4j 圖數據庫。
在 Neo4j 圖數據庫中採用一種所謂的 Tiered relationships 來表示用戶之間的關係,這個邊的關係能夠去定義不一樣的訪問狀態,好比:僅部分用戶可見,僅關注者可見這些基本操做。
數據模型如圖所示
使用這種數據模型並使用 Leader-follower 架構來優化讀寫,這個平臺得到了巨大的性能提高:
在當今的大數據時代,採用圖數據庫能夠用小成本在原有架構上得到巨大的性能提高。圖數據庫不只僅能夠在 5G、AI、物聯網領域發揮巨大的推進做用,同時也能夠用來重構本來的遺留系統。
雖然不一樣的圖數據庫可能有着大相徑庭的底層實現,但這些都徹底支持用圖的方式來構建數據模型從而讓不一樣的組件之間相互聯繫,從咱們以前的討論來看,這一種數據模型層次的改變會極大地簡化不少平常數據系統中所面臨的問題,增大系統的吞吐量而且下降運維的需求。
圖數據庫的介紹就到此爲止了,若是你對圖數據庫 Nebula Graph 有任何想法或其餘要求,歡迎去 GitHub:https://github.com/vesoft-inc/nebula issue 區向咱們提 issue 或者前往官方論壇:https://discuss.nebula-graph.io/ 的 Feedback
分類下提建議 👏;加入 Nebula Graph 交流羣,請聯繫 Nebula Graph 官方小助手微信號:NebulaGraphbot
Systems Design and Implementation, NSDI, 2013.
做者有話說:Hi,我是 Johhan。目前在 Nebula Graph 實習,研究和實現大型圖數據庫查詢引擎和存儲引擎組件。做爲一個圖數據庫及開源愛好者,我在博客分享有關數據庫、分佈式系統和 AI 公開可用學習資源。