圖解圖庫JanusGraph系列-一文知曉“圖數據“底層存儲結構

大家好,我是洋仔,JanusGraph圖解系列文章,實時更新~

圖數據庫文章總目錄:

源碼分析相關可查看github(求star~~)https://github.com/YYDreamer/janusgraph

下述流程高清大圖地址:https://www.processon.com/view/link/5f471b2e7d9c086b9903b629

版本:JanusGraph-0.5.2

轉載文章請保留以下聲明:

作者:洋仔聊編程 微信公衆號:匠心Java 原文地址:https://liyangyang.blog.csdn.net/

一:存儲模式

留言或私信我,邀請你加入“圖數據庫交流”微信羣!

1、圖內容

本文以下所有內容基於:JanusGraph基於屬性圖來進行構造圖數據:

屬性圖: 屬性圖是由 頂點(Vertex),邊(Edge),屬性(Property)組成的有向圖

Vertex可以包含Properties;Edge也可以包含Properties;

2、存儲方法

圖存儲的方式常用的有兩種:鄰接列表鄰接矩陣

JanusGraph採用鄰接列表進行圖數據的存儲,如下圖所示:(此處將圖中節點抽象爲 只有節點,沒有屬性)

在Janusgraph中一個頂點的鄰接列表包含該節點對應的屬性關聯的邊,下述會詳細說明 Janusgraph中鄰接列表是如何實現的;

3、圖切割方式

圖的切割方式分爲兩種:按節點切割(Vertex Cut)按邊切割(Edge Cut)

  • Vertex Cut:根據點進行切割,每個邊只存儲一次,只要是節點對應的邊便會多一份該節點的存儲
  • Edge Cut:根據邊進行切割,以節點爲中心,邊會存儲兩次,源節點的鄰接列表存儲一次,目標節點的鄰接列表存儲一次

在Janusgraph中既存在Edge Cut,也存在Vertex Cut的情況;

在默認的情況下使用邊切割,而針對熱點節點可以通過配置makeVertexLabel('product').partition()來將節點類型爲product類型的節點進行Vertex Cut

也就是說,在沒有上述makeVertexLabel('product').partition()配置的話,JanusGraph所有的圖數據都是以Edge Cut的方式來進行切割存儲的;

具體可以查看文章:《JanusGraph-分區》中自定義分區部分中關於圖切割部分的介紹;

我們例子來說明一下:

如下圖: 張三用戶節點通過手機號關聯出來李四用戶節點

  • 張三 和 李四 代表Vertex;指向的name、age、gender代表張三的屬性
  • edgeA 和edgeB 代表Edge;也可以包含邊的屬性,例如下圖中邊包含屬性create_time

按邊切割後:

節點
張三 name(property) age(property) gender(property) edgeA(edge)
phone phone(property) edgeA(edge) edgeB(edge)
李四 name(property) age(property) gender(property) edgeB(edge)

上述可以看到,按照邊切割後每一條邊會存儲兩次!

二:BigTable模型

在JanusGraph的存儲中, JanusGraph將圖形的鄰接列表的表示存儲在支持Bigtable數據模型的任何存儲後端中

BigTable模型如下圖:

在Bigtable數據模型中,每個表是行的集合,由一個key唯一標識。

每行由任意(可以很大數量但是必須有限數量)數量的cell組成;cell由column和value組成,column唯一標識某一個cell。

上述圖中,有兩部分需要排序的支持:sorted by keysorted by column

  • sorted by key:標識存儲後端存儲的數據時按照key的大小進行排序存儲的
  • sorted by column:這是JanusGraph對Bigtable數據模型有一個額外要求,存儲edge(邊)的單元格必須按column排序,並且列範圍指定的單元格子集必須是有效可檢索的; 這句話詳細解答在下述文章中有體現

在Bigtable模型中的行稱爲“寬行”,因爲它們支持大量cell,並且不必像關係數據庫中那樣預先定義這些cell的column。

在關係型數據庫中我們必須先定義好表的schema,纔可以存儲數據,如果存儲過程中想要改變表結構,則所有的數據都要對變化的列做出變化。但是Bigtable模型存儲中就不必如此,每個行的column不同,我們可以隨時僅對某一行進行變化,也不許預先定義行的schema,只需要定義圖的schema即可。

此外,特定的Bigtable實現可以使行按其鍵的順序排序。JanusGraph可以利用這樣的鍵序來有效地劃分圖形,從而爲非常大的圖形提供更好的加載和遍歷性能。

JanusGraph是如何基於BigTable數據模型針對於自身的圖數據特性進行設計的呢?

下面我們看下JanusGraph的邏輯存儲結構

三:存儲邏輯結構

JanusGraph基於使用BigTable模型的存儲後端 實現了自己的存儲的邏輯結構

ps:爲了更好的理解,下面部分知識點會基於HBase存儲後端進行進一步的解釋!

1、整體結構

在JanusGraph中,以節點爲中心,按切邊的方式存儲數據的。比如在Hbase中節點的ID作爲HBase的Rowkey,節點上的每一個屬性和每一條邊,作爲該Rowkey行的一個個獨立的Cell。即每一個屬性、每一條邊,都是一個個獨立的KCV結構(Key-Column-Value)

上圖中,我們可以發現圖的存儲整體分爲三部分:vertex idpropertyedge

  • vertex id: 對應節點的唯一id,如果底層存儲使用的是Hbase則代表着當前行的Rowkey,唯一代表某一個節點

  • property: 代表節點的屬性

  • edge: 代表節點的對應的邊

排序方式分爲三種:sorted by idsorted by typesorted by sort key

  • sorted by id: 依據vertex id在存儲後端進行順序存儲

  • sorted by type:此處的個人理解爲針對於property 和 edge的類型進行排序,保證同種類型的屬性或者邊連續存儲在一塊便於遍歷查找; // TODO​ 深層次理解​

  • sorted by sort key: sort key是邊組成以的一部分,主要作用是,在同種類型的edge下,針對於sort key進行排序存儲,提升針對於指定sort key的檢索速度;下面edge結構部分有詳細介紹

2、Vertex id 的結構

此處的Vertex id唯一標識圖中的某一個節點;節點vertex id的組成結構我們在源碼類IDManager的一段註釋中可以發現:

     /*		--- JanusGraphElement id bit format ---
      *  [ 0 | count | partition | ID padding (if any) ]
     */

這是在Janusgraph在生成所有的id時統一的格式包含vertex id\edge id\property id的時候,這個順序也 就是標識我們再使用gremlin查詢出節點時,節點上標識的vertex id; 這個id值的順序不同於hbase真實存儲Rowkey的順序!!!!!!!

在對vertex id進行序列化存儲時,位置有所調整爲:[ partition | 0 | count | ID padding (if any) ] 如下圖:

從圖中可以看出:

  1. Vertex ID共包含一個字節、8位、64個bit

  2. Vertex ID由partition id、count、ID padding三部分組成

  3. 最高位5個bit是partition id。partition是JanusGraph抽象出的一個概念。當Storage Backend是HBase時,JanusGraph會根據partition數量,自動計算並配置各個HBase Region的split key,從而將各個partition均勻映射到HBase的多個Region中。然後通過均勻分配partition id最終實現數據均勻打散到Storage Backend的多臺機器中

  4. 中間的count部分是流水號,其中最高位比特固定爲0;出去最高位默認的0,count的最大值爲2的(64-5-1-3)=55次冪大小:3 6028 7970 1896 3968,總共可以生成30000兆個id,完全滿足節點的生成

  5. 最後幾個bit是ID padding, 表示Vertex的類型。具體的位數長度根據不同的Vertex類型而不同。最常用的普通Vertex,其值爲'000'

爲什麼在序列化存儲vertex id時,需要調整順序序列化作爲RowKey存儲到Hbase呢?

我們通過下面的3個問題來回答:

  1. 爲什麼JausGraph分配的邏輯區間值,可以影響hbase物理存儲呢? 可以將分區相同的數據存放的更近呢?

在上述描述中,hbase使用vertex id作爲rowkey,hbase根據rowkey順序排序存儲; 每個hbase region存儲是一段連續的Rowkey行;

janusgraph的vertex id的設計中,可以發現將分區值放到了64位的前5位存儲! 在存儲數據到hbase時,對rowkey進行排序,因爲partition id在前5位,所以同一個分區的vertex id對應的rowkey值相差較小,所以會存儲在一塊;

  1. 如何快速的查詢到不同類型的節點呢? 換個說法如何快速的確定當前的行就是我們需要的節點類型的行呢?

在JanusGraph的vertex id中包含的 ID padding就代表當前的節點類型(注意此處的類型!=lable)。000標識爲普通節點,在id的組成部分中,我們經過前面的分析,最前面是partition id,只有把 ID padding放在最後幾個字節便於查找了;

  1. 爲什麼查詢出的節點顯示的vertex id要把0|count放在最前面、partiton和id padding放在後面呢?

這裏我們猜測一下:count佔用55位數據! 試想如果把count不放在最前面,那麼id的最小值比2的55次冪還大,顯示不友好! 如果把0|count放在最前面呢?就會有兩個效果:

0在有符號表示中標識當前id始終爲正整數!

count是趨勢遞增的,所以id值也是從小到大趨勢遞增的,所以節點id的最小值在2的8次冪周邊大小; 比把count放在後面顯示的id值友好多了~~~

vertex id是如何保證全局唯一性的呢?

主要是基於數據庫 + 號段模式進行分佈式id的生成;

體現在圖中就是partition id + count 來保證分佈式全局唯一性; 針對不同的partition都有自己的0-2的55次冪的範圍的id; 每次要生成vertex id時,首先獲取一個partition,獲取對應partition對應的一組還未使用的id,用來做count;

janusgraph在底層存儲中存儲了對應的partition使用了多少id,從而保證了再生成新的分佈式vertex id時,不會重複生成!

ps : JanusGraph中分佈式唯一vertex id、edge id、property id的生成分析,請看《圖解JanusGraph系列-分佈式唯一id的生成機制》

3、edge 和 property的結構

在上述的JanusGraph的整體結構中,propertyedge都是作爲cell存儲在底層存儲中;其中cell又分爲columnvalue兩部分,下圖展示了這兩部分的邏輯結構:

下面我們詳細分析一下 property 和 edge對應的邏輯結構;

3.1 edge的結構

Edge的Column組成部分:

  • label id:邊類型代表的id,在創建圖schema的時候janusgraph自動生成的label id,不同於邊生成的唯一全局id
  • direction:圖的方向,out:0、in:1
  • sort key:可以指定邊的屬性爲sort key,可多個;在同種類型的edge下,針對於sort key進行排序存儲,提升針對於指定sort key的檢索速度;
    • 該key中使用的關係類型必須是屬性非唯一鍵或非唯一單向邊標籤;
    • 存儲的爲配置屬性的value值,可多個(只存property value是因爲,已經在schema的配置中保存有當前Sort key對應的屬性key了,所以沒有必要再存一份)
  • adjacent vertex id:target節點的節點id,其實存儲的是目標節點id和源節點id的差值,這也可以減少存儲空間的使用
  • edge id:邊的全局唯一id

Edge的value組成部分:

  • signature key:邊的簽名key
    • 該key中使用的關係類型必須是屬性非唯一鍵或非唯一單向邊標籤;
    • 存儲壓縮後的配置屬性的value值,可多個(只存property value是因爲,已經在schema的配置中保存有當前signature key對應的屬性key了,所以沒有必要再存一份)
    • 主要作用提升edge的屬性的檢索速度,將常用檢索的屬性設置爲signature key,提升查找速度
  • other properties:邊的其他屬性
    • 注意! 不包含配置的sort key和signature key屬性值,因爲他們已經在對應的位置存儲過了,不需要多次存儲!
    • 此處的屬性,要插入屬性key label id和屬性value來標識是什麼屬性,屬性值是什麼;
    • 此處的property的序列化結構不同於下述所說的vertex節點的property結構,edge中other properties這部分存儲的屬性只包含:proeprty key label id + property value;不包含property全局唯一id

詳細解釋及思考:

在進行詳細分析前,請大家思考幾個問題,如下:

  1. 基於上述的edge邏輯結構,JanusGraph是如何構造鄰接列表的 或者 是如何獲取源節點的鄰接節點的?
  2. 上述的Edge邏輯結構中的,每部分的排列的順序的含義是什麼?

1、基於上述的edge邏輯結構,JanusGraph是如何構造鄰接列表的 或者 是如何獲取源節點的鄰接節點的?

從上述的整體結構部分中,我們可以知道,vertexId行後跟着當前的節點關聯的所有的edge;

而在上述的edge的邏輯結構中,有一個adjacent vertex id字段,通過這個字段就可以獲取到target節點的vertex id,就相當於指向了target節點,整理一下:

如上圖,通過上述的條件,就可以構造一個VertexA指向VertexB 和 VertexC的鄰接鏈表;

其實,JanusGraph可以理解爲構造的是雙向鄰接列表, 依據上圖,我們知道vertexA 和 vertexB 和 vertexC存在邊關係; 當我們構造vertexB的鄰接列表時,會包含指向vertexA的節點,只是說在edge對應的邏輯結構中邊的方向不同而已:

總結:JanusGraph通過vertex id行中包含所有關聯的edge,edge邏輯結構中包含指向target節點的數據來組成雙向鄰接列表的結構;

2、上述的Edge邏輯結構中的,每部分的排列的順序的含義是什麼?

首先,在查詢的時候爲了提升查詢速度,我們首先要過濾的是什麼,針對於edge毋庸置疑是邊的類型邊的方向

所以,爲了我們可以更快的拿到類型和方向,所以在edge的存儲結構中,我們發現作者將類型和方向存放在了column中,並且是column的最前面部分;這樣我們可以直接通過判斷column的第一部分字節就可以對邊類型方向進行過濾!

ps:雖然我們在寫Gremlin語句的時候,可能是語句寫的是先過濾邊的屬性或者其他,但是JanusGraph會針對我們的gremlin語句進行優化爲先過濾邊類型方向

接下來,我們可能對邊的屬性進行過濾,我們怎樣提升經常要過濾的屬性的查詢速度呢? 我們將經常用於範圍查詢的屬性配置爲sort key,然後就可以在過濾完邊類型和方向後快速的進行屬性的範圍過濾(此處快速的指過濾配置爲sort key的屬性);

3.2 property的結構

property的存儲結構十分的簡單,只包含key idproperty idvalue三部分:

  • key id:屬性label對應的id,有創建schema時JanusGraph創建; 不同於屬性的唯一id
  • property id:屬性的唯一id,唯一代表某一個屬性
  • value:屬性值

注意:屬性的類型包含SINGLELISTSET三種Cardinality;當屬性被設置爲LIST類型時,因爲LIST允許當前的節點存在多個相同的屬性kv對,僅通過key id也就是屬性的label id是無法將相同的屬性label區分出來的

所以在這種情況下,JanusGraph的property的存儲結構有所變化, property id也將會被存儲在column中,如下圖:

四:index存儲結構

1、Composite Index-vertex index結構

圖一(唯一索引Composite Index結構圖):

圖二(非唯一索引Composite Index結構圖):

Rowkey由index label idproperties value兩大部分組成:

  • index label id:標識當前索引類型
  • properties value:索引中包含屬性的所有屬性值,可多個; 存在壓縮存儲,如果超過16000個字節,則使用GZIP對property value進行壓縮存儲!

Column由第一個字節0vertex id組成:

  • 第一個字節0:無論是唯一索引,還是非唯一索引此部分都會存在;如圖一
  • vertex id:非唯一索引纔會在column中存在,用於分別多個相同索引值對應的不同節點;如圖二

value由vertex id組成:

  • vertex id:針對於rowkey + column查詢到的value是vertex id,然後通過vertex id查詢對應的節點

2、Composite Index-edge index結構

圖一(唯一索引Composite Index結構圖):

圖二(非唯一索引Composite Index結構圖):

Rowkey同Vertex index部分

Column由第一個字節0edge id組成:

  • 第一個字節0:無論是唯一索引,還是非唯一索引此部分都會存在;如圖一
  • edge id:非唯一索引纔會在column中存在,用於分別多個相同索引值對應的不同節點;如圖二

value由以下4部分組成:

  • edge id:邊id
  • out vertex id:邊對應的出邊id
  • type id:edge 的label type id
  • in vertex id:邊對應的入邊id

2、Mixed Index結構

這裏以ES作爲第三方索引庫爲例,這裏只介紹普通的範圍查找的mixed index的構造:

ES的概念爲:index 包含多個 type;每個type包含多個document id,每個document id包含多個field name 和對應的field value;

Jausgraph

  • index:包含兩種,janusgraph_edgejanusgraph_vertex兩種

  • type:可自定義

  • document id:edge id或者 vertex id

  • field name:索引對應屬性的label string

  • field value:屬性對應的property value

基於倒排索引的查詢順序爲,給定過一個property label 和 property value查詢對應的Vertex id 或者 edge id,則查詢滿足要求的field name 和 field value,就可以獲取到對應的document id即Vertex id 或者 edge id;

五:序列化數據案例

以序列化實例來看下上述所說的整體結構

測試節點數據:

{
    "label":"user",
    "propertyMap":{
        "create_time":"2016-12-09 02:29:26",
        "user_name":"張三",
        "user_id":"test110"
    },
    "vertexId":4152
}

測試邊數據:

{
    "edgeId":17514510,
    "label":"user_login_phone_number",
    "propertyMap":{
        "productid":"2"
    },
    "sourceId":4152,
    "targetId":40964120
}

跟蹤Janusgraph源碼,獲取其序列化信息,後端存儲使用Hbase

節點序列化後數據(不包含索引):

邊序列化後數據(不包含索引):

節點的vertex id序列化後的數據爲56 0 0 0 0 0 0 -128;一個節點對應的屬性和邊的Rowkey相同,依據qualifier也就是column來進行區分;

在邊的序列化結果中,包含兩部分:一部分是節點4152的kcv,一個是節點40964120的kcv;這地方也可以說明JanusGraph是採用的雙向鄰接鏈表進行圖存儲的

五:Schema的使用

從上述來看,我們可以知道,JanusGraph圖的schema該怎樣定義主要是由edge labels 、property keys 和vertex labels 組成(Each JanusGraph graph has a schema comprised of the edge labels, property keys, and vertex labels used therein

JanusGraph的schema可以顯式或隱式創建,推薦用戶採用顯式定義的方式。JanusGraph的schema是可以在使用過程中修改的,而且不會導致服務宕機,也不會拖慢查詢速度。

比如一個簡單的顯示定義的銷售圖的scheme:

<propertyKey value="salesman_id" explain="銷售人員id" index="" type="java.lang.String" />
<propertyKey value="real_name" explain="姓名" index="" type="java.lang.String" />
<propertyKey value="role" explain="角色" type="" />
<propertyKey value="city_code" explain="所處城市代碼" index="" type="" />
<propertyKey value="create_time" explain="創建時間" index="" type="" />

<edgeLabel value="saleman_service_for" explain="銷售引導">
    <propertys>
        <property value="create_time"/>
    </propertys>
</edgeLabel>
<edgeLabel value="own_salaman_Idcard" explain="銷售身份">
    <propertys>
        <property value="create_time"/>
    </propertys>
</edgeLabel>

<index elementType="vertex" indexType="compositeIndex" name="salesman_id_I"  >
    <propertyKeys>
        <propertyKey value="salesman_id" />
    </propertyKeys>
</index>

<vertexLabel value="salesman" explain="銷售"  >
    <propertys>
        <property value="salesman_id"  />
        <property value="real_name" />
        <property value="role"  />
        <property value="city_code"  />
    </propertys>
    <edges>
        <edge value="saleman_service_for" direction="out" />
        <edge value="own_salaman_Idcard" direction="out" />
    </edges>
</vertexLabel>

當然,我們也可以添加一些其他的可以組成schema的元素,上述三個是必須的,另外的比如索引(index)等,主要的結構還是:

JanusGraph Schema
        |-----------Vertex Lables
        |-----------Property Keys
        |-----------Edge Labels

和關係型數據庫不同,圖數據的schema是定義一張圖,而非定義一個vertex的。在Mysql中,我們通常將建立一張表定義爲創建一個schema,而在JanusGraph中,一個Graph用於一個schema。

六:源碼分析

源碼分析已經push到github:https://github.com/YYDreamer/janusgraph

七:總結

  • JanusGraph採用Edge cut的方式進行圖切割,並且按照雙向鄰接列表的形式進行圖存儲
  • JanusGraph每個節點都是對應的kcv結構; vertex id唯一標識節點;對應的行cell存儲節點屬性和對應的邊
  • 節點id的分佈式唯一性採用數據庫+號段模式進行生成;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章