JanusGraph 學習手冊

注1:版權全部,轉載必須註明出處。
注2:此手冊根據2018年10月JanusGraph的官方文檔進行整理,後續會陸續更新新知識
注3:本博文原始地址,兩個博客由同一博主管理。java

JanusGraph做爲Titan的繼承者,汲取了Titan和TinkerPop框架諸多的優勢,成爲了當前熱門的圖數據庫領域的佼佼者,而後,JanusGraph的中文文檔卻相對匱乏, 成爲了JanusGraph入門學習者的障礙之一。本文以JanusGraph中文文檔爲基礎,結合做者我的的思考,總計了JanusGraph中文文檔中的關鍵點,但願給你們帶來幫助。node

Intro

JanusGraph is a graph database engine. JanusGraph itself is focused on compact graph serialization, rich graph data modeling, and efficient query execution. In addition, JanusGraph utilizes Hadoop for graph analytics and batch graph processing. JanusGraph implements robust, modular interfaces for data persistence, data indexing, and client access. JanusGraph’s modular architecture allows it to interoperate with a wide range of storage, index, and client technologies; it also eases the process of extending JanusGraph to support new ones.git

總的來講,JanusGraph的一大特性是和其餘組件的交互能力強,好比像存儲的Hbase, full-text search的elasticsearch等,也能夠很方便地和graphx等olap引擎交互。github

Broadly speaking, applications can interact with JanusGraph in two ways:web

Embed JanusGraph inside the application executing Gremlin queries directly against the graph within the same JVM. Query execution, JanusGraph’s caches, and transaction handling all happen in the same JVM as the application while data retrieval from the storage backend may be local or remote.算法

Interact with a local or remote JanusGraph instance by submitting Gremlin queries to the server. JanusGraph natively supports the Gremlin Server component of the Apache TinkerPop stack.docker

應用程序能夠經過兩種方式使用JanusGraph,shell

  1. 直接把JanusGraph集成到本身的系統中,在同一個JVM中執行Query
  2. 經過將query submit到JanusGraph的instance來執行

架構圖:數據庫

JanusGraph Architecture

Basics

Configuration

  • 要啓動一個Janus Instance,必需要提供一個configuration文件。
  • configuration裏面至少要說明storage backend是什麼,參考此
  • 若是須要高級功能(e.g full-text search, geo search, or range queries),就須要一個indexing backend。

一個基於Cassandra+ElasticSearch的配置例子:

storage.backend=cql
storage.hostname=localhost

index.search.backend=elasticsearch
index.search.hostname=100.100.101.1, 100.100.101.2
index.search.elasticsearch.client-only=true

數據模型(Schema)

數據的Schema是JanusGraph的一個很重要的部分,數據schema主要定義在三個元素上:頂點,邊和屬性。聽起來可能有點繞口,schema是幹什麼呢?其實就是去定義頂點的label,邊的label的Property的key有哪些,再通俗點講,就是有哪些類型的點(好比 godhuman),有哪些類型的邊(好比 tradefriends),而後點和邊的屬性列表都有哪些key(好比 human 有一個property key是nameage)。

須要注意的幾點:

  • schema能夠顯示地定義也不能夠不顯示地定義,但仍是建議提早定義好。
  • Schema能夠在數據庫使用的過程當中更改和進化,這樣並不會影響數據庫正常的服務。

Vertex label

  • vertice label描述的是vertice的語義,不過vertice label是optional的,用來區分不一樣類型的vertice,好比 userproduct
  • 利用 makeVertexLabel(String).make() 來建立vertice label
  • vertice label必須保持全局惟一性

下面是個建立vertex label的例子:

mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
mgmt.commit()
// Create a labeled vertex
person = graph.addVertex(label, 'person')
// Create an unlabeled vertex
v = graph.addVertex()
graph.tx().commit()

注意,須要顯示地調用 buildermake() 函數來完成vertex label的定義。

Edge label

  • Edge label主要描述的是relationship的語義(好比friends)
  • 在一個事務中,用 makeEdgeLabel(String) 來定義一個edge label,注意,edge label必須是惟一的,這個方法會返回一個 builder,這個 builder 能夠用來獲取這種edge label的多度關係multiplicity,這個指標限定了每對(pair)之間edge最大的數量。
    • multiplicity包含的類型有:multi, simple, many2one, one2many, one2one (結合數據庫E-R model這個概念不難理解)。
    • 默認的multiplicityMULTI

下面是建立edge label的一個例子:

mgmt = graph.openManagement()
follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make()
mother = mgmt.makeEdgeLabel('mother').multiplicity(MANY2ONE).make()
mgmt.commit()

注意,須要顯示地調用 buildermake() 函數來完成edge label的定義。

Property keys

屬性Property定義在頂點和邊上,固然也能夠用來定義其餘東西,property是 key-value 對的形式。舉個例子,'name' = 'John' 這就能夠看作一個屬性,它定義了 name 這個屬性,這個屬性的value是 'John'。

經過 makePropertyKey(String) 方法來常見Property key。屬性名應該儘量避免使用空格和特殊字符。

屬性須要關注兩點:

  1. Property Key Data Type

每一個屬性都有本身的數據類型,也就是說它的value必須是某種支持的數據類型,能夠經過 dataType(Class) 方法來定義數據類型。JanusGraph會保證加入進來的屬性數據都知足該數據類型。

屬性數據類型能夠申明爲 Object.class,但這並不推薦,最好控制好數據類型,一來能夠節省空間,二來可讓JanusGraph來幫咱們檢查導入的數據類型是否是符合要求。

數據類型必須使用具體的類,不能是接口或者抽象類。JanusGraph使用的是徹底至關去判斷數據類型是否符合,因此使用子類的數據去導入到父類的屬性中也是不會成功的。

  1. Property Key Cardinality

cardinality(Cardinality) 方法來定義某一個頂點的某個屬性的基底(cardinality)。

基底有三種狀況:

  • SINGLE:每個元素對於這個key只能有一個value,好比 birthDate 就是一個single基底,由於每一個人最多隻能有一個生日。
  • LIST:每一個元素對於這個key能夠有任意數量的值,好比咱們建模傳感器(sensor),其有一個屬性是 sensorRecords,那麼,對於這個屬性,可能有一系列的值。注意,LIST是容許有重複元素的。
  • SET: 與LIST相同,不一樣的是,SET不容許有重複的元素。

默認的cardinality是single。注意,對於邊屬性來講,property key的基底始終是single

下面是在定義property key的時候定義對應的cardinality的例子:

mgmt = graph.openManagement()
birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
sensorReading = mgmt.makePropertyKey('sensorReading').dataType(Double.class).cardinality(Cardinality.LIST).make()
mgmt.commit()

Relation types

Edge label和property key共同地被稱爲relation type

能夠經過 containsRelationType() 方法來檢測relation type是否存在。

下面是個例子:

mgmt = graph.openManagement()
if (mgmt.containsRelationType('name'))
    name = mgmt.getPropertyKey('name')
mgmt.getRelationTypes(EdgeLabel.class)
mgmt.commit()

改變schema的元素(Changing Schema Elements)

一旦咱們建立好Vertex label, edge label和property key後,就不能更改了,咱們惟一能改的是schema的名字,好比下面這個例子:

mgmt = graph.openManagement()
place = mgmt.getPropertyKey('place')
mgmt.changeName(place, 'location')
mgmt.commit()

上面這個例子中,咱們把property key的名字從place改爲了location。

Schema Constraints

多個property能夠綁定到同一個vertex或者edge上面,用 JanusGraphManagement.addProperties(VertexLabel, PropertyKey...) 方法:

// 例子1
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make()
mgmt.addProperties(person, name, birthDate)
mgmt.commit()
// 例子2
mgmt = graph.openManagement()
follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
mgmt.addProperties(follow, name)
mgmt.commit()

有一種很通俗的定義關係的方法:

mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
company = mgmt.makeVertexLabel('company').make()
works = mgmt.makeEdgeLabel('works').multiplicity(MULTI).make()
mgmt.addConnection(works, person, company)
mgmt.commit()

這裏,用到是 addConnection() 方法。

Gremlin

Gremlin是JanusGraph默認的query language。

Gremlin用來從JanusGraph裏面查詢數據,修改數據,Gremlin是一種traversal查詢語言,能夠很方便地查詢此類查詢:

從小明開始,遍歷到他的父親,再從他的父親遍歷到他父親的父親,而後返回他父親的父親的名字

Gremlin是Apache TinkerPop項目的一部分。

關於Gremlin這裏就很少作介紹了,汪文星的文檔裏作了很詳細的說明。

更多關於JanusGraph中Gremlin相關的說明,參考這個文檔

JanusGraph Server

  • JanusGrpah Server其實就是Gremlin server
  • 兩種交互方式:分別是webSocket和HTTP

使用方式

使用預先打好的包

這種方式主要是用來學習使用JanusGraph,生產環境下不建議用這種配置。

  • ./bin/janusgraph.sh start就能夠啓動了,會自動fork cassandra的包,elasticsearch的包,gremlin-server的包,並鏈接到對應的服務器
$ bin/janusgraph.sh start
Forking Cassandra...
Running `nodetool statusthrift`.. OK (returned exit status 0 and printed string "running").
Forking Elasticsearch...
Connecting to Elasticsearch (127.0.0.1:9300)... OK (connected to 127.0.0.1:9300).
Forking Gremlin-Server...
Connecting to Gremlin-Server (127.0.0.1:8182)... OK (connected to 127.0.0.1:8182).
Run gremlin.sh to connect.
  • 使用完畢後關係JanusGraph,使用 ./bin/janusgraph.sh stop命令就能夠了
$ bin/janusgraph.sh stop
Killing Gremlin-Server (pid 59687)...
Killing Elasticsearch (pid 59494)...
Killing Cassandra (pid 58809)...
使用WebSocket的方式

上面pre-package的方式實際上是使用的內置的backend和index backend(在這個例子裏面,分別是cassandra和elasticsearch),其實咱們也能夠把JanusGraph鏈接到本身的HBase等Backend。

使用Http的方式

配置方式和使用WebSocket的方式基本相同,能夠用Curl命令來test服務的可用性:

curl -XPOST -Hcontent-type:application/json -d '{"gremlin":"g.V().count()"}' http://[IP for JanusGraph server host]:8182
同時使用WebSocket和Http的方式

修改gremlin-server.yaml文件,更改channelizer爲:

channelizer: org.apache.tinkerpop.gremlin.server.channel.WsAndHttpChannelizer
一些高級用法

還有一些高級用法,好比認證(Authentication over HTTP),具體這裏就不細說了,能夠參考官方文檔

服務部署

  • JanusGraph Server自己是個服務器服務,這個服務背後不少Backends(es, Hbase等等),客戶端應用(application)主要經過Gremlin查詢語句和JanusGraph instance交互,JanusGraph instance交互而後和對應的後端交互來執行對應的query。
  • 沒有master JanusGraph server的概念,application能夠鏈接任何JanusGraph instance,固然,也能夠用負載均衡的方法來分流到不一樣的JanusGraph instance。
  • 各個JanusGraph instance之間並不相互交互。

部署方式:

  • 方式1:每一個server上起一個JanusGraph Instance,並在同一個server起對應的backend和index
  • 方式2:JanusServe和backend server, index server分離
  • 方法3:直接embedded到客戶端appplication中,至關於引入了一個jar包

ConfiguredGraphFactory

這個概念比較難以理解。

ConfiguredGraphFactory是一個singleton,和JanusGraphFactory同樣。

它們提供了一套API(methods,方法)來動態地操做服務器上的圖。

在gremlin-console下咱們能夠直接用這個接口去操做圖,以下:

gremlin> :remote connect tinkerpop.server conf/remote.yaml
==>Configured localhost/127.0.0.1:8182
gremlin> :remote console
==>All scripts will now be sent to Gremlin Server - [localhost/127.0.0.1:8182] - type ':remote console' to return to local mode
gremlin> ConfiguredGraphFactory.open("graph1")
==>standardjanusgraph[cassandrathrift:[127.0.0.1]]
gremlin> graph1
==>standardjanusgraph[cassandrathrift:[127.0.0.1]]
gremlin> graph1_traversal
==>graphtraversalsource[standardjanusgraph[cassandrathrift:[127.0.0.1]], standard]

先來談一談 JanusGraphFactory,它是咱們在gremlin-console裏面操做一個圖的時候的entry-point,每當咱們訪問一個圖的時候,系統就爲咱們建立了一個Configuration類的實例。

能夠將這個東西和spark-shell裏面的sparkSession作類比來方便理解。

ConfiguredGraphFactory不太同樣,它也是咱們訪問、操做一個圖的時候的entry-point,但配置是經過另外一個singleton來實現的,叫ConfigurationManagementGraph

ConfigurationManagementGraph 使咱們能夠很方便地管理圖的配置。

就像上面例子同樣,咱們能夠經過下面兩種方法來訪問一個圖:

ConfiguredGraphFactory.create("graphName")

或者

ConfiguredGraphFactory.open("graphName")

能夠經過下面的方法來羅列全部配置好了的圖。配置好是指以前有用ConfigurationManagementGraph的API配置過:

ConfiguredGraphFactory.getGraphNames()

用下面的放來來drop一個graph database:

ConfiguredGraphFactory.drop("graphName")

若是想使用ConfiguredGraphFactory這個接口,好比在啓動前JanusGraph server前配置好。修改gremlin-server.yaml文件,在graphs這個section下面,添加一行:

graphs: {
  ConfigurationManagementGraph: conf/JanusGraph-configurationmanagement.properties
}

在這個例子中,ConfigurationManagementGraph這個graph即是使用位於onf/JanusGraph-configurationmanagement.properties下的配置文件來配置,下面是配置文件的一個例子:

gremlin.graph=org.janusgraph.core.JanusGraphFactory
storage.backend=cql
graph.graphname=ConfigurationManagementGraph
storage.hostname=127.0.0.1

具體ConfigurationManagementGraph怎麼用呢?下面是一個例子(在gremlin-console下):

map = new HashMap<String, Object>();
map.put("storage.backend", "cql");
map.put("storage.hostname", "127.0.0.1");
map.put("graph.graphname", "graph1");
ConfiguredGraphFactory.createConfiguration(new MapConfiguration(map));

// then access the graph
ConfiguredGraphFactory.open("graph1");

graph.graphname這個屬性指定了上述配置是針對哪張graph的。

Multi-node

  • 若是咱們但願在JanusGraph server啓動後去動態地建立圖,就要用到上面章節提到的ConfiguredGraphFactory,這要求JanusGraph集羣的每一個server的配置文件裏面都聲明瞭使用JanusGraphManagerConfigurationManagementGraph,具體方法,見這個連接。
  • 爲了保證graph的consistency,每一個server node都要使用ConfiguredGraphFactory(保持集羣上每一個node server的配置一致性)。

Indexing

  • 支持兩類graph indexing: Graph IndexVertex-centric Index
  • graph index包含兩類:Composite IndexMixed Index

Composite Index

經過下面的方法建立Composite Index:

graph.tx().rollback() //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex()
mgmt.buildIndex('byNameAndAgeComposite', Vertex.class).addKey(name).addKey(age).buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()

Composite Index方式不須要特殊地去配置底層的存儲引擎(好比cassandra),主要的底層存儲引擎都支持這種方式。

經過Composite Index能夠來保證Property key的惟一性,用下面這種方法:

graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
mgmt.buildIndex('byNameUnique', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameUnique').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameUnique"), SchemaAction.REINDEX).get()
mgmt.commit()

經過下面的方式來建立Mixed Index:

graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('nameAndAge', Vertex.class).addKey(name).addKey(age).buildMixedIndex("search")
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get()
mgmt.commit()

Mixed Index方式須要特殊地去配置底層的存儲引擎(好比cassandra)的索引。

Mixed Index比Composite Index更加靈活,可是對於含有相等關係的謂語關係的查詢效率更慢。

buildMixedIndex方法的參數string要和properties文件中配置的一致,好比:

index.search.backend

這個配置中間的部分search就是buildMixedIndex方法的參數。

有了Mixed Index,這面這些query就支持用索引來加速了:

g.V().has('name', textContains('hercules')).has('age', inside(20, 50))
g.V().has('name', textContains('hercules'))
g.V().has('age', lt(50))
g.V().has('age', outside(20, 50))
g.V().has('age', lt(50).or(gte(60)))
g.V().or(__.has('name', textContains('hercules')), __.has('age', inside(20, 50)))

總結兩種Graph Index的區別:

  1. Use a composite index for exact match index retrievals. Composite indexes do not require configuring or operating an external index system and are often significantly faster than mixed indexes.

    a. As an exception, use a mixed index for exact matches when the number of distinct values for query constraint is relatively small or if one value is expected to be associated with many elements in the graph (i.e. in case of low selectivity).

  1. Use a mixed indexes for numeric range, full-text or geo-spatial indexing. Also, using a mixed index can speed up the order().by() queries.

Vertex-centric Indexes

graph.tx().rollback()  //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
time = mgmt.getPropertyKey('time')
rating = mgmt.makePropertyKey('rating').dataType(Double.class).make()
battled = mgmt.getEdgeLabel('battled')
mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.decr, rating, time)
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByRatingAndTime', 'battled').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getRelationIndex(battled, 'battlesByRatingAndTime'), SchemaAction.REINDEX).get()
mgmt.commit()

注意上面的Index是有順序的,先對rating作index,再對time作index,所以:

h = g.V().has('name', 'hercules').next()
g.V(h).outE('battled').property('rating', 5.0) //Add some rating properties
g.V(h).outE('battled').has('rating', gt(3.0)).inV() // query-1
g.V(h).outE('battled').has('rating', 5.0).has('time', inside(10, 50)).inV() // query-2
g.V(h).outE('battled').has('time', inside(10, 50)).inV() // query-3

上述3個query中,前兩個被加速了,但第三沒並無。

JanusGraph automatically builds vertex-centric indexes per edge label and property key. That means, even with thousands of incident battled edges, queries like g.V(h).out('mother') or g.V(h).values('age') are efficiently answered by the local index.

Transaction 事務

  • JanusGraph具備事務安全性,能夠在多個並行的線程中同時使用。

一般,使用:

graph.V(...) and graph.tx().commit()

這樣的ThreadLocal接口來執行一次事務。

事務處理的一個例子

根據TinkerPop框架事務機制的描述,每個線程在它執行第一個操做的時候開啓一個事務:

graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph")
juno = graph.addVertex() //Automatically opens a new transaction
juno.property("name", "juno")
graph.tx().commit() //Commits transaction

在上面這個例子中,addVertex()函數執行的時候,事務被開啓,而後當咱們顯示執行graph.tx().commit()的時候,事務關閉。

當事務尚未完成,卻調用了graph.close()關閉了數據庫,那麼這個事務的後期行爲是不得而知的,通常狀況下,事務應該會被回滾。但執行close()的線程所對應的事務會被順利地commit。

事務處理範圍(scope)

根據TinkerPop框架事務機制的描述,每個線程在它執行第一個操做的時候開啓一個事務,全部的圖操做元素(頂點,邊,類型等變量等)均和該事務自動綁定了,當咱們使用commit()或者rollback()關閉/回滾一個事務的時候,這些圖操做元素就會失效,可是,頂點和類型會被自動地轉移到下一個事務中,以下面的例子:

graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph")
juno = graph.addVertex() //Automatically opens a new transaction
graph.tx().commit() //Ends transaction
juno.property("name", "juno") //Vertex is automatically transitioned

可是,邊不能自動轉移到下一個事務中,須要顯式地刷新,以下面的例子:

e = juno.addEdge("knows", graph.addVertex())
graph.tx().commit() //Ends transaction
e = g.E(e).next() //Need to refresh edge
e.property("time", 99)

事務失敗(failure)

當咱們在建立一個事務,並作一系列操做的時候,應該事先考慮到事務失敗的可能性(IO exceptions, network errors, machine crashes or resource unavailability...),因此推薦用下面的方式處理異常:

try {
    if (g.V().has("name", name).iterator().hasNext())
        throw new IllegalArgumentException("Username already taken: " + name)
    user = graph.addVertex()
    user.property("name", name)
    graph.tx().commit()
} catch (Exception e) {
    //Recover, retry, or return error message
    println(e.getMessage())
}

上面的例子描述了一個註冊功能,先檢查名字存不存在,若是不存在,則建立該user,而後commit。

若是事務失敗了,會拋出JanusGraphException異常,事務失敗有不少種可能,JanusGraph區分了兩種常見的failure:

  1. potentially temporary failure
    • potentially temporary failure主要與是IO異常(IO hiccups (e.g. network timeouts))和資源可用狀況(resource unavailability)有關。
    • JanusGrpah會自動去嘗試從temporary failure中恢復,從新嘗試commit事務,retry的次數能夠配置
  2. permanent failure
    • permanent failure主要與徹底的鏈接失敗,硬件故障和鎖掙脫有關(complete connection loss, hardware failure or lock contention)。
    • 鎖的爭奪,好比兩我的同時同時以"juno"的用戶名去註冊,其中一個事務必然失敗。根據事務的語義,能夠經過重試失敗的事務來嘗試從鎖爭奪中恢復(好比使用另外一個用戶名)。
    • 通常有兩種典型的情形
      • PermanentLockingException(Local lock contention):另外一個線程已經被賦予了競爭鎖
      • PermanentLockingException(Expected value mismatch for X: expected=Y vs actual=Z)

        Tips: The verification that the value read in this transaction is the same as the one in the datastore after applying for the lock failed. In other words, another transaction modified the value after it had been read and modified.

多線程事務(Multi-Threaded Transactions)

多線程事務指的是同一個事務能夠充分利用機器的多核架構來多線程執行,見TinkerPop關於threaded transaction的描述

能夠經過createThreadedTx()方法來建立一個線程獨立的事務:

threadedGraph = graph.tx().createThreadedTx();  // Create a threaded transaction
threads = new Thread[10];
for (int i=0; i<threads.length; i++) {
    threads[i]=new Thread({
        println("Do something with 'threadedGraph''");
    });
    threads[i].start();
}
for (int i=0; i<threads.length; i++) threads[i].join();
threadedGraph.tx().commit();

createThreadedTx()方法返回了一個新的Graph對象,tx()對象在建立線程的時候,並無爲每一個線程建立一個事務,也就是說,全部的線程都運行在同一個事務中,這樣咱們就實現了threaded transaction。

JanusGraph能夠支持數百個線程運行在同一個事務中。

經過createThreadedTx()接口能夠很輕鬆的建立並行算法(Concurrent Algorithms),尤爲是那些適用於並行計算的算法。

createThreadedTx()接口的另外一個應用是建立嵌套式的事務(Nested Transactions),具體的例子見這裏,這對於那些long-time running的事務尤爲有做用。

事務處理的一些常見問題

再次強調一下,JanusGraph的邏輯是,當咱們對一個graph進行第一次操做的時候,事務就自動被打開了,咱們並不須要去手動的建立一個事務,除非咱們但願建立一個multi-threaded transaction。

每一個事務都要顯式地用commit()rollback()方法去關閉。

一個事務在開始的時候,就會去維持它的狀態,在多線程應用的環境中,可能會出現不可預知的問題,好比下面這個例子:

v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction
g.V(v).bothE()
>> returns nothing, v has no edges
//thread is idle for a few seconds, another thread adds edges to v
g.V(v).bothE()
>> still returns nothing because the transactional state from the beginning is maintained

這種狀況在客戶端應用端很常見,server會維持多個線程去相應服務器的請求。比較好的一個習慣是,在沒完成一部分工做後,就去顯式地terminate任務,以下面這個例子:

v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction
g.V(v).bothE()
graph.tx().commit()
//thread is idle for a few seconds, another thread adds edges to v
g.V(v).bothE()
>> returns the newly added edge
graph.tx().commit()

多線程事務(Multi-Threaded Transactions)還能夠經過newTransaction()方法來建立,但要注意的是,在該事務中建立的點和邊只在該事務中可用,事務被關閉之後,訪問這些點和邊就會拋出異常,若是還想使用這些點和邊怎麼辦?答案是顯式地去刷新這些點和邊,以下面這個例子:

g.V(existingVertex)
g.E(existingEdge)

事務的相關配置(configuration)

JanusGraph.buildTransaction() 也能夠啓動一個多線程的事務,所以它和上面提到的newTransaction()方法實際上是同樣的功能,只不過buildTransaction()方法提供了附加的配置選項。

buildTransaction()會返回一個TransactionBuilder實例,TransactionBuilder實例能夠配置選項,這裏就詳述了。

配置好後,接着能夠調用start()方法來啓動一個線程,這樣會返回一個JanusGraphTransaction實例。

緩存機制(JanusGraph Cache)

Transaction-level caching

經過graph.buildTransaction().setVertexCacheSize(int)能夠來設置事務的緩存大小(cache.tx-cache-size)。

當一個事務被打開的時候,維持了兩類緩存:

  • Vertex cache
  • Index cache
Vertex cache

Vertex cache主要包含了事務中使用到的點,以及這些點的鄰接點列表(adjacency list)的一個子集,這個子集包括了在同一個事務中被取出的該點的鄰接點。因此,heap中vertex cache使用的空間不只與事務中存在的頂點數有關,還與這些點的鄰接點數目有關。

Vertex cache中能維持的最多的頂點數等於transaction cache size。Vertex cache能顯著地提升iteractive traversal的速度。固然,若是一樣的vertex在後面並無被從新訪問,那vertex cache就沒有起到做用。

Index cache

若是在一個事務中,前面部分的query用到了某個索引(index),那麼後面使用這個query的時候,獲取結果的速度會大大加快,這就是index cache的做用,固然,若是後面並無再使用相同的index和相同的query,那Index query的做用也就沒有體現了。

Index cache中的每一個條目都會被賦予一個權重,這個權重等與 2 + result set size整個index cache的權重不會超過transaction cache size的一半。

Database-level caching

Database-level caching實現了多個transaction之間cache的共享,從空間利用率上來講,database-lvel caching更經濟,但訪問速度比transaction-level稍慢。

Database-level cache不會在一個事務結束後立刻失效,這使得處於多個事務中的讀操做速度顯著地提升。

Cache Expiration Time

Cache expiration time經過 cache.db-cache-time (單位爲:milliseconds)參數來調整。

這裏有一個trick,若是咱們只啓動了一個JanusGraph instance,由於沒有另外一個instance去改變玩咱們的圖(graph),cache expiration time即可以設置爲 0,這樣的話,cache就會無限期保留處於cache中的元素(除非由於空間不足被頂替)。

若是咱們啓動了多個JanusGraph實例,這個時間應該被設置成當一個instance修改了圖,另外一個instance可以看到修改所須要等待的最大時間。若是但願修改被其餘的instance迅速可以看到,那麼應該中止使用Database-level cache。容許的時延越長,cache的效率越高。

固然,一個JanusGraph instance始終可以立刻看到它本身作出的修改。

Cache Size

Cache size經過 cache.db-cache-size 參數來控制Database-level的cache能使用多少heap space,cache越大, 效率越高,但由此帶來的GC性能問題卻不容小覷。

cache size也能夠配置成百分比(佔整個剩餘的heap空間的比例)。

注意cache size的配置是排他性的,也就是說cache會獨佔你配置的空間,其餘資源(e.g. Gremlin Server, embedded Cassandra, etc)也須要heap spave,因此不要把cache size配置得太過度,不然可能會引發out of memory錯誤或者GC問題。

Clean Up Wait Time

還有一個須要注意的參數是 cache.db-cache-clean-wait,當一個頂點被修改後,全部與該頂點有關的Database-level的cache都會失效而且會被驅逐。JanusGraph會從底層的storage backend從新fetch新的頂點數據,並re-populate緩存。

cache.db-cache-clean-wait 這個參數能夠控制,cache會等待 cache.db-cache-clean-wait milliseconds時間再repopulate新的cache。

Storage Backend Caching

底層的storage backend也會有本身的cache策略,這個就要參考對應的底層存儲的文檔了。

事務日誌(Transaction Log)

能夠啓動記錄事務的變化日誌,日誌能夠用來在後期進行處理,或者做爲一個record備份。

在啓動一個事務的時候指定是否須要採集日誌:

tx = graph.buildTransaction().logIdentifier('addedPerson').start()
u = tx.addVertex(label, 'human')
u.property('name', 'proteros')
u.property('age', 36)
tx.commit()

這裏有個 log processor 的概念,其實就是內置的日誌監聽處理器。例如,能夠統計在一個transaction裏面,某一個label下被添加的頂點的數目。

其餘常見問題

  • Accidental type creation

默認地,當遇到一個新的type的時候,janusGrpah會自動地去建立property keys和邊的label。對於schema這個問題,仍是建議用戶本身去定義schema,而不要使用自動發現schema的方式,能夠在配置文件裏面以下申明關閉自動infer schema的功能:

schema.default = none

建立type的操做也不宜放在不一樣的線程中,由於這會引發不可知的後果,最好放到一個batch操做中把全部的type都事先建立好,而後再去作其餘的圖操做。

  • Custom Class Datatype

能夠本身去建立class,做爲value的類別。

  • Transactional Scope for Edges

邊應該先取出來,再操做,每一個transaction都是有scope的,若是超出了這個scope,去訪問以前的邊,會報錯。

  • Ghost Vertices

這個概念比較新奇,指的是:咱們在一個事務中刪除了一個vertex,卻同時在另外一個transaction中修改了它。這種狀況下,這個vertex仍是會依然存在的,咱們稱這種vertex爲ghost vertex。

對於這個問題的解決辦法最好是暫時容許ghost vertices,而後按期地寫腳本去刪除它們。

  • Debug-level Logging Slows Execution

Log level若是被設置成了 DEBUG,輸出可能會很大,日誌中會包括一個query如何被編譯、優化和執行的過程,這會顯著地影響處理的性能,在生產環境下,不建議使用 DEBUG level,建議是用 INFO level。

  • Elasticsearch OutOfMemoryException

當有不少客戶端鏈接到Elasticsearch的時候,可能會報 OutOfMemoryException 異常,一般這不是一個內存溢出的問題,而是OS不容許運行ES的用戶起太多的進程。

能夠經過調整運行es的用戶可運行的進程數(number of allowed processes)也許能夠解決這個問題。

  • Dropping a Database

刪除一個graph instance,可使用:

  • JanusGraphFactory.drop(graph) 接口
graph = JanusGraphFactory.open('path/to/configuration.properties')
JanusGraphFactory.drop(graph);
  • ConfiguredGraphFactory接口
graph = ConfiguredGraphFactory.open('example')
ConfiguredGraphFactory.drop('example');

Note:
0.3.0之前的版本除了須要執行上面的命令,還須要顯示地調用 JanusGraphFactory.close(graph)ConfiguredGraphFactory.close("example")來關閉這個graph,以防cache中還存在這個graph,形成錯誤。

技術上的限制(Technical Limitations)

這個部分能夠理解JanusGrpah還存在的一些可改進或者沒法改進的地方。

設計上的限制

下面這些缺陷是JanusGraph自然設計上的一些缺陷,這些缺陷短時間內是得不到解決的。

  • Size Limitation

JanusGraph can store up to a quintillion edges (2^60) and half as many vertices. That limitation is imposed by JanusGraph’s id scheme.

  • DataType Definitions

當咱們用 dataType(Class) 接口去定義property key的數據類型的時候,JanusGraph會enforce該key的全部屬性都嚴格是該類型。這是嚴格的類型判斷,子類也不可行。好比,若是咱們定義的數據類型是Number.class,使用的倒是Integer或者Long型,這種狀況大多數狀況下會報錯。

Object.class或許能夠解決這個問題,比較靈活,但帶來的問題也顯而易見,那就是性能上會下降,同時數據類型的check也會失效。

  • Type Definitions cannot be changed

Edge label, vertex label和property key一旦建立就不能改變了,固然,type能夠被重命名,新的類型也能夠在runtime中建立,因此schema是支持envolving的。

  • 保留的關鍵字(Reserved Keywords)

下面是JanusGraph保留的關鍵字,不要使用這些關鍵字做爲變量名、函數名等。

  • vertex
  • element
  • edge
  • property
  • label
  • key

臨時的缺陷

下面這些缺陷在未來的release中會被逐漸解決。

  • Limited Mixed Index Support

Mixed Index只支持JanusGraph所支持的數據類型(Data type)的一個子集,Mixed Index目前也不支持 SETLIST 做爲基數(cardinality)的property key。

  • Batch Loading Speed

能夠經過下面的configuration來開啓 batch loading mode

Name Description Datatype Default Value Mutability
storage.batch-loading Whether to enable batch loading into the storage backend Boolean false LOCAL

這個trick其實並無使用底層的backend的batch loading技術。

另外一個限制是,若是同時向一個頂點導入數百萬條邊,這種狀況下極可能failure,咱們稱這種loading爲 supernode loading,這種loading之因此失敗是受到了後端存儲的限制,具體這裏就不細數了。

後端存儲(Storage Backends)

Apache Cassandra

JanusGraph後端經過四種方式來支持整合Cassandra:

  • cql - CQL based driver (推薦使用)
  • cassandrathrift - JanusGraph’s Thrift connection pool driver (v2.3之後退休了,也不建議使用)
  • cassandra - Astyanax driver. The Astyanax project is retired.
  • embeddedcassandra - Embedded driver for running Cassandra and JanusGraph within the same JVM(測試能夠,但生產環境不建議使用這種方式)

Local Server Mode

Cassandra能夠做爲一個standalone的數據庫與JanusGraph同樣運行在localhost,在這種狀況下,JanusGraph和Cassandra之間經過socket通訊。

經過下面的步驟配置JanusGraph on Cassandra:

  1. Download Cassandra, unpack it, and set filesystem paths in conf/cassandra.yaml and conf/log4j-server.properties
  2. Connecting Gremlin Server to Cassandra using the default configuration files provided in the pre-packaged distribution requires that Cassandra Thrift is enabled. To enable Cassandra Thrift open conf/cassandra.yaml and update start_rpc: false to start_rpc: true. If Cassandra is already running Thrift can be started manually with bin/nodetool enablethrift. the Thrift status can be verified with bin/nodetool statusthrift.
  3. Start Cassandra by invoking bin/cassandra -f on the command line in the directory where Cassandra was unpacked. Read output to check that Cassandra started successfully.

Now, you can create a Cassandra JanusGraph as follows

JanusGraph g = JanusGraphFactory.build().
set("storage.backend", "cql").
set("storage.hostname", "127.0.0.1").
open();

Local Container Mode

經過docker安裝Cassandra,去release界面看一下JanusGraph版本測試經過的Cassandra版本,使用下面的docker命令運行Cassandra:

docker run --name jg-cassandra -d -e CASSANDRA_START_RPC=true -p 9160:9160 \
-p 9042:9042 -p 7199:7199 -p 7001:7001 -p 7000:7000 cassandra:3.11

Note: Port 9160 is used for the Thrift client API. Port 9042 is for CQL native clients. Ports 7000, 7001 and 7099 are for inter-node communication.

Remote Server Mode

JanusGraph - Cassandra - Remote Server Mode

集羣模式下,有一個Cassandra集羣,而後全部的JanusGraph的instance經過socket的方式去讀寫Cassandra集羣,客戶端應用程序也能夠和JanusGraph的實例運行在同一個JVM中。

舉個例子,咱們啓動了一個Cassandra的集羣,其中一個機器的IP是77.77.77.77,咱們能夠經過如下方式鏈接:

JanusGraph graph = JanusGraphFactory.build().
set("storage.backend", "cql").
set("storage.hostname", "77.77.77.77").
open();

Remote Server Mode with Gremlin Server

JanusGraph - Cassandra - Remote Server Mode with Gremlin Server

能夠在JanusGraph server外面包上一層Gremlin server,這樣,不只能夠和JanusGraph server交互,也能夠和Gremlin server交互。

經過:

bin/gremlin-server.sh

啓動Gremlin server,而後經過 bin/gremlin.sh 打開Gremlin的終端,而後運行:

:plugin use tinkerpop.server
:remote connect tinkerpop.server conf/remote.yaml
:> g.addV()

即可以了。

JanusGraph Embedded Mode

JanusGraph Embedded Mode

Cassandra也能夠整合到JanusGraph中,在這種狀況下,JanusGraph和Cassandra運行在同一個JVM中,本次經過進程間通訊而不是網絡傳輸信息,這種狀況經過在performance上有很大的幫助。

若是想使用Cassandra的embedded mode,須要配置embeddedcassandra做爲存儲後端。

這種embedded模式推薦經過Gremlin server來暴露JanusGraph。

須要注意的是,embedded方式須要GC調優。

Apache HBase

Local Server Mode

  1. 此處下載一個HBase的stable release。
  2. Start HBase by invoking the start-hbase.sh script in the bin directory inside the extracted HBase directory. To stop HBase, use stop-hbase.sh.

而後經過:

JanusGraph graph = JanusGraphFactory.build()
    .set("storage.backend", "hbase")
    .open();

鏈接到HBase。

Remote Server Mode

集羣模式,JanusGraph的實例經過socket與HBase創建鏈接,並進行讀寫操做。

假設咱們啓動了一個HBase並使用zookeeper做爲協調器,zk的IP是 77.77.77.77, 77.77.77.7877.77.77.79,那麼咱們經過下面的方式鏈接到HBase:

JanusGraph g = JanusGraphFactory.build()
    .set("storage.backend", "hbase")
    .set("storage.hostname", "77.77.77.77, 77.77.77.78, 77.77.77.79")
    .open();

Remote Server Mode with Gremlin Server

和Cassandra章節講的同樣,咱們能夠在JanusGraph server外面再包一層Gremlin server:

http://gremlin-server.janusgraph.machine1/mygraph/vertices/1
http://gremlin-server.janusgraph.machine2/mygraph/tp/gremlin?script=g.v(1).out('follows').out('created')

Gremlin-server的配置文件也要作響應的修改,下面是個例子:

...
graphs: {
  g: conf/janusgraph-hbase.properties
}
scriptEngines: {
  gremlin-groovy: {
    plugins: { org.janusgraph.graphdb.tinkerpop.plugin.JanusGraphGremlinPlugin: {},
               org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {},
               org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {},
               org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]},
               org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}}
...

HBase的配置

配置說明章節,有一系列配置選項,要注意 storage.hbase.table 參數,默認table的名字是janusgraph

Gloabl Graph Operations

JanusGraph over HBase 支持全局的點和邊的遍歷,但這種狀況下,會把全部的點和邊導入到內存中,可能會報 OutOfMemoryException 錯誤。可使用 Gremlin-hadoop 的方式去遍歷。

InMemory Storage Backend

JanusGraph支持使用純內存,能夠經過下面的屬性配置:

storage.backend=inmemory

能夠在Gremlin-console直接打開內存中的圖:

graph = JanusGraphFactory.build().set('storage.backend', 'inmemory').open()

這種狀況下,若是關閉該圖或者中止graph的進程,圖的全部數據都會丟失。這種模式也只支持單點模式,不支持多個JanusGraph的圖實例共享。

這種存儲策略不適合在生產中使用。

後端索引(Index Backends)

查詢謂語和數據類型

比較謂語

  • eq (equal)
  • neq (not equal)
  • gt (greater than)
  • gte (greater than or equal)
  • lt (less than)
  • lte (less than or equal)

文本操做謂語(Text Predicate)

主要能夠用這些operator作full-text search,常見的有兩類:

  1. String中以詞爲粒度的
    • textContains
    • textContainsPrefix
    • textContainsRegex
    • textContainsFuzzy
  2. 以整個String爲粒度的
    • textPrefix
    • textRegex
    • textFuzzy

區間操做謂語(Geo Predicate)

區間操做謂語包括:

  • geoIntersect
  • geoWithin
  • geoDisjoint
  • geoContains

查詢樣例

g.V().has("name", "hercules")
// 2) Find all vertices with an age greater than 50
g.V().has("age", gt(50))
// or find all vertices between 1000 (inclusive) and 5000 (exclusive) years of age and order by increasing age
g.V().has("age", inside(1000, 5000)).order().by("age", incr)
// which returns the same result set as the following query but in reverse order
g.V().has("age", inside(1000, 5000)).order().by("age", decr)
// 3) Find all edges where the place is at most 50 kilometers from the given latitude-longitude pair
g.E().has("place", geoWithin(Geoshape.circle(37.97, 23.72, 50)))
// 4) Find all edges where reason contains the word "loves"
g.E().has("reason", textContains("loves"))
// or all edges which contain two words (need to chunk into individual words)
g.E().has("reason", textContains("loves")).has("reason", textContains("breezes"))
// or all edges which contain words that start with "lov"
g.E().has("reason", textContainsPrefix("lov"))
// or all edges which contain words that match the regular expression "br[ez]*s" in their entirety
g.E().has("reason", textContainsRegex("br[ez]*s"))
// or all edges which contain words similar to "love"
g.E().has("reason", textContainsFuzzy("love"))
// 5) Find all vertices older than a thousand years and named "saturn"
g.V().has("age", gt(1000)).has("name", "saturn")

數據類型(Data Type Support)

Composite index能夠支持任何類型的index,mixed index只支持下面的數據類型:

  • Byte
  • Short
  • Integer
  • Long
  • Float
  • Double
  • String
  • Geoshape
  • Date
  • Instant
  • UUID

Geoshape Data Type

只有mixed indexes支持Geoshape Data Type,支持的數據類型有point, circle, box, line, polygon, multi-point, multi-line 和 multi-polygon。

集合類型(Collections)

若是使用 Elasticsearch,能夠索引cardinality爲 SET 或者 LIST 的屬性,以下面的例子:

mgmt = graph.openManagement()
nameProperty = mgmt.makePropertyKey("names").dataType(String.class).cardinality(Cardinality.SET).make()
mgmt.buildIndex("search", Vertex.class).addKey(nameProperty, Mapping.STRING.asParameter()).buildMixedIndex("search")
mgmt.commit()
//Insert a vertex
person = graph.addVertex()
person.property("names", "Robert")
person.property("names", "Bob")
graph.tx().commit()
//Now query it
g.V().has("names", "Bob").count().next() //1
g.V().has("names", "Robert").count().next() //1

索引參數和全局搜索

當咱們定義一個Mixed index的時候,每個被添加到索引中的property key都有一系列參數能夠設置。

全局索引,這是一個很重要的功能。當咱們去索引字符串類型的property key的時候,咱們能夠選擇從character層面後者text層面去索引,這須要改變 mapping 參數。

當咱們從text層面去索引的時候,字符串會被tokenize成bag of words,用戶即可以去query是否包含一個或多個,這叫作 full-text search

當咱們從char層面去索引的時候,string會直接和char串作match,不會有futher analysis後者tokenize操做。這能夠方便咱們去查找是否包含某個字符序列,這也叫作 string search

下面分開講:

Full-Text Search

默認地,string會使用text層面的索引,能夠經過下面的方式顯示地去建立:

mgmt = graph.openManagement()
summary = mgmt.makePropertyKey('booksummary').dataType(String.class).make()
mgmt.buildIndex('booksBySummary', Vertex.class).addKey(summary, Mapping.TEXT.asParameter()).buildMixedIndex("search")
mgmt.commit()

能夠看到,這和普通的建立索引惟一的一個區別是咱們在調用 addKey() 方法的時候,多添加了一個 Mapping.TEXT 映射參數。

前面咱們提到過,若是是使用text層面的index,JanusGraph會本身去維護一個bag of words,JanusGraph默認的tokenization方案是:它會使用非字母數字的字段去split,而後會移除到全部小於2個字符的token。

當咱們使用text層面的index的時候,只有全局索引的謂語才真正用到了咱們建立的索引,包括textContains方法,textContainsPrefix方法,textContainsRegex方法和textContainsFuzzy方法,注意,full-text search是case-insensitive的,下面是具體的例子:

import static org.janusgraph.core.attribute.Text.*
g.V().has('booksummary', textContains('unicorns'))
g.V().has('booksummary', textContainsPrefix('uni'))
g.V().has('booksummary', textContainsRegex('.*corn.*'))
g.V().has('booksummary', textContainsFuzzy('unicorn'))

首先要明確的是,string search會把數據load到內存中,這實際上是很是costly的。

能夠經過下面的方式去顯示地建立string search:

mgmt = graph.openManagement()
name = mgmt.makePropertyKey('bookname').dataType(String.class).make()
mgmt.buildIndex('booksBySummary', Vertex.class).addKey(name, Mapping.STRING.asParameter()).buildMixedIndex("search")
mgmt.commit()

這種 bookname 會按照 as-is 的方式去分析,包括stop word和no-letter character

當咱們使用string層面的index的時候,只有下面的謂語才真正用到了咱們建立的索引,包括eqneqtextPrefixtextRegextextFuzzy。注意,string search是case-insensitive的,下面是具體的例子:

import static org.apache.tinkerpop.gremlin.process.traversal.P.*
import static org.janusgraph.core.attribute.Text.*
g.V().has('bookname', eq('unicorns'))
g.V().has('bookname', neq('unicorns'))
g.V().has('bookname', textPrefix('uni'))
g.V().has('bookname', textRegex('.*corn.*'))
g.V().has('bookname', textFuzzy('unicorn'))

若是咱們使用elasticsearch做爲後端,這樣就能夠用全部的謂語去作精確或者模糊的查詢了。

經過下面的方式建立這種叫作 Mapping.TEXTSTRING 的full-text search方案:

mgmt = graph.openManagement()
summary = mgmt.makePropertyKey('booksummary').dataType(String.class).make()
mgmt.buildIndex('booksBySummary', Vertex.class).addKey(summary, Mapping.TEXTSTRING.asParameter()).buildMixedIndex("search")
mgmt.commit()

Geo Mapping

默認地,JanusGraph支持索引點(point)的索引,而且去查詢circle或者box類型的property,若是想索引一個非-點類型的property,須要使用 Mapping.PREFIX_TREE

mgmt = graph.openManagement()
name = mgmt.makePropertyKey('border').dataType(Geoshape.class).make()
mgmt.buildIndex('borderIndex', Vertex.class).addKey(name, Mapping.PREFIX_TREE.asParameter()).buildMixedIndex("search")
mgmt.commit()

Direct Index Query

能夠直接向index backend發送query,下面是個例子:

ManagementSystem mgmt = graph.openManagement();
PropertyKey text = mgmt.makePropertyKey("text").dataType(String.class).make();
mgmt.buildIndex("vertexByText", Vertex.class).addKey(text).buildMixedIndex("search");
mgmt.commit();
// ... Load vertices ...
for (Result<Vertex> result : graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertices()) {
   System.out.println(result.getElement() + ": " + result.getScore());
}

須要指明兩個元素:

  1. 想要查詢的index backend的index名字,在上面的例子中是 vertexByText
  2. 查詢語句,在上面的例子中是 v.text:(farm uncle berry)

Elasticsearch

Running elasticsearch

下載包裏面自己包含兼容的elasticsearch的distribution,經過:

elasticsearch/bin/elasticsearch

來運行elasticsearch。要注意的是,es不能使用root運行。

ES配置

JanusGraph支持經過HTTP客戶端鏈接到正在運行的ES集羣。

在配置文件中,Elasticsearch client須要經過下面這一行指明:

index.search.backend=elasticsearch

經過 index.[X].hostname 指明某一個或者一系列es的實例的地址:

index.search.backend=elasticsearch
index.search.hostname=10.0.0.10:9200

能夠經過下面的方式綁定要一段連續的IP:PORT對:

index.search.backend=elasticsearch
index.search.hostname=10.0.0.10, 10.0.0.20:7777
REST Client

Rest client能夠經過 index.[X].bulk-refresh 參數控制改變多久能被索引到。

REST Client 既能夠配置成HTTP的方式也能夠配置成HTTPS的方式。

HTTPS authentification

能夠經過 index.[X].elasticsearch.ssl.enabled 開啓HTTP的SSL支持。注意,這可能須要修改 index.[X].port 參數,由於ES的HTTPS服務的端口號可能和一般意義的REST API端口(9200)不同。

HTTP authentification

能夠經過配置 index.[X].elasticsearch.http.auth.basic.realm 參數來經過HTTP協議作認證。

index.search.elasticsearch.http.auth.type=basic
index.search.elasticsearch.http.auth.basic.username=httpuser
index.search.elasticsearch.http.auth.basic.password=httppassword

tips:

能夠本身實現class來實現認證:

index.search.elasticsearch.http.auth.custom.authenticator-class=fully.qualified.class.Name
index.search.elasticsearch.elasticsearch.http.auth.custom.authenticator-args=arg1,arg2,...

本身實現的class必須實現 org.janusgraph.diskstorage.es.rest.util.RestClientAuthenticator 接口。

高級功能

Advanced Schema

Static Vertex

Vertex label能夠定義爲static的,一旦建立,就不能修改了。

mgmt = graph.openManagement()
tweet = mgmt.makeVertexLabel('tweet').setStatic().make()
mgmt.commit()

Edge and Vertex TTL

邊和頂點能夠配置對應的time-to-live(TTL),這個概念有點相似於數據庫中的臨時表的概念,用這種方式建立的點和邊在使用一段時間之後會被自動移除掉。

Edge TTL

mgmt = graph.openManagement()
visits = mgmt.makeEdgeLabel('visits').make()
mgmt.setTTL(visits, Duration.ofDays(7))
mgmt.commit()

須要注意的是,這種方法後端數據庫必須支持cell level TTL,目前只有Cassandra和HBase支持。

Property TTL

mgmt = graph.openManagement()
sensor = mgmt.makePropertyKey('sensor').cardinality(Cardinality.LIST).dataType(Double.class).make()
mgmt.setTTL(sensor, Duration.ofDays(21))
mgmt.commit()

Vertex TTL

mgmt = graph.openManagement()
tweet = mgmt.makeVertexLabel('tweet').setStatic().make()
mgmt.setTTL(tweet, Duration.ofHours(36))
mgmt.commit()

Undirected Edges

mgmt = graph.openManagement()
mgmt.makeEdgeLabel('author').unidirected().make()
mgmt.commit()

這種undirected edge只能經過out-going的方向去遍歷,這有點像萬維網。

Eventually-Consistent Storage Backends

底層數據的最終一致性問題。

Eventually consistent storage backend有哪些?Apache Cassandra 或者 Apache HBase其實都是這種數據庫類型。

數據的一致性

經過 JanusGraphManagement.setConsistency(element, ConsistencyModifier.LOCK) 方法去定義數據的一致性問題, 以下面的例子:

mgmt = graph.openManagement()
name = mgmt.makePropertyKey('consistentName').dataType(String.class).make()
index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.setConsistency(name, ConsistencyModifier.LOCK) // Ensures only one name per vertex
mgmt.setConsistency(index, ConsistencyModifier.LOCK) // Ensures name uniqueness in the graph
mgmt.commit()

使用鎖其實開銷仍是很大的,在對數據一致性要求不高的情形,最好不用鎖,讓後期數據庫本身在讀操做中去解決數據一致性問題。

當有兩個事務同時對一個元素進行寫操做的時候,怎麼辦呢?咱們能夠先讓寫操做成功,而後後期再去解決一致性問題,具體有兩種思路解決這個問題:

  • Forking Edges

思想就是,每個事務fork一個對應的要修改的edge,再根據時間戳去在後期修改。

下面是個例子:

mgmt = graph.openManagement()
related = mgmt.makeEdgeLabel('related').make()
mgmt.setConsistency(related, ConsistencyModifier.FORK)
mgmt.commit()

這裏,咱們建立了一個edge label,叫作 related,而後咱們把一致性屬性設置成了 ConsistencyModifier.FORK

這個策略只對MULTI類別的邊適用。其餘的multiplicity並不適用,由於其它multiplicity顯式地應用了鎖。

Failure & Recovery

失敗和恢復,主要是兩個部分:

  1. 事務的失敗和恢復
  2. 實例的宕機和恢復

事務的失敗和恢復

事務若是在調用 commit() 以前失敗,是能夠恢復的。commit() 以前的改變也會被回滾。

有時候,數據persist到存儲系統的過程成功了,但建立index的的過程卻失敗了。這種狀況下,該事務會被認爲成功了,由於底層存儲纔是source of graph。

但這樣會帶來數據和索引的不一致性。JanusGraph維護了一份 transaction write-ahead log,對應的有兩個參數能夠調整:

tx.log-tx = true
tx.max-commit-time = 10000

若是一個事務的persistance過程超過了 max-commit-time,JanusGrpah會嘗試從中恢復。與此同時,另外有一個進程去掃描維護好的這份log,去identify那些只成功了一半的事務。建議使用另外一臺機器專門去作失敗恢復,運行:

recovery = JanusGraphFactory.startTransactionRecovery(graph, startTime, TimeUnit.MILLISECONDS);

transaction write-ahead log 自己也有維護成本,由於涉及到大量的寫操做。transaction write-ahead log 自動維護的時間是2天,2天前的數據會被自動刪除。

對於這樣的系統,如何 fine tune log system 也是須要仔細考慮的因素。

實例的恢復

若是某個JanusGraph instance宕機了,其餘的實例應該不能受影響。若是涉及到schema相關的操做,好比建立索引,這就須要不一樣instance保持協做了,JanusGraph會自動地去維護一份running instance的列表,若是某一個實例被意外關閉了,建立索引的操做就會失敗。

在這種狀況下,有一個方案是去手動地remove某一個實例:

mgmt = graph.openManagement()
mgmt.getOpenInstances() //all open instances
==>7f0001016161-dunwich1(current)
==>7f0001016161-atlantis1
mgmt.forceCloseInstance('7f0001016161-atlantis1') //remove an instance
mgmt.commit()

但這樣作有數據不一致的風險,應該儘可能少使用這種方式。

索引的管理

從新索引

通常來說,咱們在建立schema的時候,就應該把索引創建好,若是事先沒有建立好索引,就須要從新索引了。

能夠經過兩種方式來執行重索引:

  • MapReduce
  • JanusGraphManagement

具體的代碼能夠參考:https://docs.janusgraph.org/latest/index-admin.html

刪除索引

刪除索引分兩步:

  1. JanusGraph通知全部其餘的實例,說明索引即將被刪除,索引便會標記成 DISABLED 狀態,此時JanusGraph便會中止使用該索引去回答查詢,或者更新索引,索引相關的底層數據還保留但會被忽略。
  2. 根據索引是屬於composite索引仍是mixed索引,若是是composite索引,能夠直接用 JanusGraphManagement 或者 MapReduce 去刪除,若是是mixed索引就比較麻煩了,由於這涉及到後端存儲的索引,因此須要手動地去後端drop掉對應的索引。

重建索引的相關問題v

當一個索引剛剛被創建,就執行重索引的時候,可能會報以下錯誤:

The index mixedExample is in an invalid state and cannot be indexed.
The following index keys have invalid status: desc has status INSTALLED
(status must be one of [REGISTERED, ENABLED])

這是由於創建索引後,索引信息會被慢慢地廣播到集羣中其餘的Instances,這須要必定的時間,因此,最好不要在索引剛剛創建之後就去執行重索引任務。

大規模導入(Bulk Loading)

大規模導入須要的配置

經過 storage.batch-loading 參數來支持 Bulk loading。

若是打開了 Builk loading,最好關閉自動建立schema的功能(schema.default = none)。由於automatic type creation會不斷地check來保證數據的一致性和完整性,對於Bulk loading的場合,這或許是不須要的。

另一個須要關注的參數是 ids.block-size,能夠經過增大這個參數來減小id獲取過程的數量(id block acquisition process),但這會形成大量的id浪費,這個參數須要根據每臺機器添加的頂點的數量來作調整,默認值已經比較合理了,若是不行,能夠適當地增大這個數值(10倍,100倍,好比)。

對於這個參數,有個技巧:Rule of thumb: Set ids.block-size to the number of vertices you expect to add per JanusGraph instance per hour.

Note:要保證全部JanusGraph instance這個參數的配置都同樣,若是須要調整這個參數,最好先關閉全部的instance,調整好後再上線。

若是有多個實例,這些實例在不斷地分配id,可能會形成衝突問題,有時候甚至會報出異常,通常來講,對於這個問題,能夠調整下面幾個參數:

  1. ids.authority.wait-time:單位是milliseconds,id pool mamager在等待id block應用程序得到底層存儲所須要等待的時間,這個時間越短,越容易出問題。

Rule of thumb: Set this to the sum of the 95th percentile read and write times measured on the storage backend cluster under load. Important: This value should be the same across all JanusGraph instances.

  1. ids.renew-timeout:單位是milliseconds,JanusGraph 的 id pool manager 在獲取新一個id以前會等待的總時間。

Rule of thumb: Set this value to be as large feasible to not have to wait too long for unrecoverable failures. The only downside of increasing it is that JanusGraph will try for a long time on an unavailable storage backend cluster.

還有一些須要注意的讀寫參數:

storage.buffer-size:咱們執行不少query的時候,JanusGraph會把它們封裝成一個個的小batch,而後推送到後端的存儲執行,當咱們在短期內執行大量的寫操做的時候,後端存儲可能承受不了這麼大的壓力。在這種狀況下,咱們能夠增大這個buffer參數,但與此相對的代價是每秒中能夠發送的request數量會減少。這個參數不建議在用事務的方式導入數據的時候進行修改。

storage.read-attemptsstorage.write-attempts 參數,這個參數指的是每一個推送到後端的batch會被嘗試多少次(直至認爲這個batch fail),若是但願在導數據的時候支持 high load,最好調大這幾個參數。

storage.attempt-wait 參數指定了JanusGraph在從新執行一次失敗的操做以前會等待的時間(millisecond),這個值越大,後端能抗住的load越高。

Graph Partitioning

分區策略,主要是兩種:

  1. Edge Cut

砍邊策略,常常一塊兒遍歷到的點儘可能放在同一個機器上。

  1. Vertex Cut

砍點策略。砍邊策略的目的是減少通訊量,砍點策略主要是爲了處理hotspot問題(超級點問題),好比有的點,入度很是大,這種狀況下,用鄰接表的方式+砍邊的方式存儲的話,勢必形成某一個分區上某一個點的存儲量過大(偏移),這個時候,利用砍點策略,把這種點均勻地分佈到不一樣的partition上面就顯得很重要了。

一個典型的場景是 UserProduct 的關係,product 可能只有幾千個,但用戶卻有上百萬個,這種狀況下,product 最好就始終砍點策略。

對與分區這個問題,若是數據量小,就用隨機分區(默認的)就好,若是數據量過大,就要好好地去fine tune分區的策略了。

JanusGraph with TinkerPop’s Hadoop-Gremlin

JanusGraph和TinkerPop的Hadoop框架的整合問題。JanusGraph和Apache Spark還有Hadoop的整合主要是依靠社區的力量。

相關文章
相關標籤/搜索