Janusgraph简介

Janusgraph是一个可扩展的支持大批量数据的图数据库,号称可支持千亿顶点和边。它的前身是Titan,起始于2016年从Titan fork出的一个分支,原因是Titan母公司被DataStax(开发了Cassandra)收购后处于停滞状态且公司不愿意将其放到阿帕奇基金会下,因此开发者们fork了一个分支重命名为Janusgraph,并且置于Linux Software Foundation下,在github上开源合作。360金融貌似有在应用Janusgraph。本篇文章使用的Janusgraph版本为0.3.3 。

架构简述

Janusgraph是一个属性图,有如下核心概念(源自Tinkerpop):

  • Vertex:图中的点
  • Edge:图中的边
  • Vertex Label:节点类型,每个节点只能有一个Label(不同于Neo4j)
  • Edge Label:边类型
  • Property Key:属性名称(属性名称和边类型不能重复)
  • Property:属性,key-value形式,janusgraph中属性有Cardinality的概念,包括SINGLE、LIST、SET三种,即对于一个属性key可以有单个值、列表值、集合值

每一个Janusgraph图都有以边类型、属性key、点类型进行定义的schema模式,可以说Janusgraph是带有约束结构的,例如限定有几种点、几种边、某属性的类型是什么等等,比较独树一帜的是它还支持限定两点之间指定类型的边的数量。默认情况下schema可以在运行时自动定义,同时也支持关闭自动创建采用强制预设。

Janusgraph构建的初衷是“通过为其增加新功能、改善性能和扩展性、增加后端存储系统来增强分布式图系统的功能,从而振兴分布式图系统的开发”。因此Janusgraph很像一个外包雇主,将存储、索引、交互式API等内容交给其他专业的框架工具,架构图如下:

可以看到在存储上,它支持Cassandra、HBase、BerkeleyDB等;在索引上,它支持Elasticsearch、Solr、Lucene等;在查询交互上采用Tinkerpop框架,使用Gremlin作为查询语言。

Tinkerpop

相信大家对于Cassandra、HBase、Elasticsearch、Solr这些或多或少都有些了解,这里重点介绍一下Janusgraph的“门面外包”Tinkerpop。

Tinkerpop的官方文档在开头搞了个童话故事,挺文艺的,不过说实话有点令人摸不着头脑,比较明显的是Tinkerpop有很多组成部分,也就是这几个拟“人”化的代表,核心是Gremlin(小精灵)。

可以将Tinkerpop看作图数据库的抽象层,目标是提供图处理工具的生态环境,有一些SLF4j之于Log系统的感觉,但不仅于此。Tinkerpop提供了统一的界面,包括Gremlin查询语言、Gremlin Server图服务器、Graph Computer图计算器等等,提供了包含OLTP、OLAP的统一界面和工具。对于数据库供应商来说只需要适配相关接口便可享受到生态和普适工具的好处;对于用户来说,选择Gremlin作为应用的启动交互层可以屏蔽底层的差异性,保证应用不受变化和差异的影响。

Tinkerpop内容较多,这里不详细展开,详情参考官方文档。在后续介绍如果有使用到某些部分会进行进一步的介绍。

搭建Janus环境

搭建Janusgraph有多种途径和方式,可以参考官方文档的说明,简单介绍一下最小服务环境(Linux)的搭建过程:

  1. 下载Janusgraph的release版本,本篇采用的是v0.3.3
  2. 解压压缩包(unzip),进入解压后的目录
  3. 启动gremlin server:./bin/gremlin-server.sh ./conf/gremlin-server/gremlin-server-berkeleyje.yaml
  4. 启动gremlin console:./bin/gremlin.sh,成功后会进入控制台界面
  5. 连接服务:gremlin> :remote connect tinkerpop.server conf/remote.yaml
  6. 修改为远程执行模式:gremlin> :remote console,这样所有命令都会提交到server
  7. 导入样本数据:gremlin> GraphOfTheGodsFactory.loadWithoutMixedIndex(graph, true)
  8. 使用gremlin语句进行查询:gremlin> saturn = g.V().has('name', 'saturn').next()

操作说明:如前文所述,在tinkerpop框架下,启动的是gremlin-server。在启动时指定的配置文件中,我们指定了存储实现为Berkeleydb(一种开源的文件数据库)以及它的文件路径,没有指定索引实现,实际内容仅两行:

1
2
storage.backend=berkeleyje
storage.directory=../db/berkeley

极端一点来说,因为存储外包了,Janusgraph就是个翻译执行器,可以不依赖server,启动一个server的意义在于将执行操作独立出来(server集群就是用来负载均衡,server之间基本不交流),并且提供了稳定的graph对象(操作图)和g对象(图遍历)供用户直接使用。接着启动gremlin console这个交互工具,并且指定将所有命令提交至server(当然也可以不这么做,只不过这样就需要手动构造graph和g对象)。接下来使用Janusgraph自带的样例数据加载工具来load示例数据,GraphOfTheGodsFactory这个类是包含在Janus包中的,所以可以直接使用,同时在加载时选择了无索引加载的方式。最后进行了一下简单查询,验证服务和数据均处于正常状态。

存储结构

在存储结构上Janusgraph官方文档提供了较为详细的说明,这点非常棒,一目了然,像这种模型概念层面的东西如果要从代码里面反推出来会令人非常头疼。

很明显,Janusgraph采用了类似邻接表的存储形式,每一行的key是vertex id,包含了点的属性以及它的边(列族的形式),属性又包含了key id、property id、property value,边又包含了边类型、邻接点id、边属性等。你可能会问点类型(label)在哪,答案是它被存为“点”然后与点用边关联。通过存储邻接表格式的图,Janusgraph确保所有顶点的关联边和属性紧密地存储在存储后端,这加快了遍历, 缺点是每条边必须存储两次。

数据处理

作为非关系型数据库,尤其是新事物的图数据库,总是难以避免数据的导入导出。在数据处理方面,Tinkerpop通过其提供的OLAP功能,能够比较好地支持数据的导入、导出以及批处理和算法等等。对于OLAP,Tinkerpop提供了上层抽象概念:GraphComputer、VertexProgram、MapReduce(自定义的名为MapReduce的接口)等,具体的实现通过Hadoop MapReduce、Spark等大数据框架来完成。对于开发者来说,只要编写针对VertexEdge的具体逻辑就能够实现批量高效处理。在导入导出上,Janusgraph并没有给出自定义的解决方案,依然直接依托Tinkerpop提供的工具和工具类。

导入

在导入方面,主要使用的是BulkLoaderVertexProgram,值得注意的是,这个类在新版本的Tinkerpop中已经deprecate,理由是有相当多的图数据库提供商提供了针对自身实现的更加高效的批量导入/导出方法,若是图数据库提供商没有提供相关方法,那么可以选择提供InputFormatOutputFormat来适配CloneVertexProgram ,详情请查看官方更新说明。但是如前所述Janusgraph没有提供针对性的解决方案,同时也没有提供OutputFormat,那么综合考虑当前还是使用BulkLoaderVertexProgram最为合适,而CloneVertexProgram会在导出部分进行相关说明。

上文提到的InputFormatOutputFormat源自于Hadoop,如果有不了解的可以先行了解一番。BulkLoaderVertexProgram所做的工作本质上就是使用InputFormat从数据源读取数据解析为org.apache.tinkerpop.gremlin.structure.Vertex,然后调用图API操作进行导入。理解了这一点,同时了解了Janusgraph的存储结构后,我们可以很容易地知道只要一条数据能够通过某种方法解析出包含完整的点id、点类型、点属性以及邻接边,就能够进行转换导入。为了便于数据文本化,Tinkerpop提供了GraphSON Format(从名称可看出类似于JSON),官网以及软件安装目录下的data文件夹中有很多示例数据可供参考(注意GraphSON也有好几个版本)。此外还提供了Script Format,允许直接编写groovy脚本来解析数据。

使用BulkLoaderVertexProgram的示例代码如下:

1
2
3
4
5
6
gremlin> hadoopGraph = GraphFactory.open('conf/hadoop-graph/hadoop-graphson.properties')
gremlin> blvp = BulkLoaderVertexProgram.build()
.bulkLoader(OneTimeBulkLoader)
.writeGraph('conf/janusgraph-berkeleyje.properties')
.create(hadoopGraph)
gremlin> hadoopGraph.compute(SparkGraphComputer).program(blvp).submit().get()

以上是直接在gremlin-console中进行操作(本地执行),使用java也是差不多的代码,只是语法略微有些区别。代码中使用到的第一个配置文件中的有效内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# the graph class
gremlin.graph=org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph
# 指定输入Format
gremlin.hadoop.graphReader=org.apache.tinkerpop.gremlin.hadoop.structure.io.graphson.GraphSONInputFormat

# 源数据的位置
gremlin.hadoop.inputLocation=data/tinkerpop-modern.json
# gremlin.hadoop.outputLocation=output

# if the job jars are not on the classpath of every hadoop node, then they must be provided to the distributed cache at runtime
gremlin.hadoop.jarsInDistributedCache=true

####################################
# SparkGraphComputer Configuration #
####################################
# spark配置
spark.master=local[4]
spark.serializer=org.apache.spark.serializer.KryoSerializer
spark.kryo.registrator=org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoRegistrator

还是比较清晰明了的,如果要导入其他格式的数据,更改graphReader即可。代码中使用到的第二个配置文件就是导入图的配置文件,指定为需要进行导入的图数据库的配置即可,配置中也可以添加一些额外的优化内容,例如指定storage.batch-loading=true(这需要提前定义好所有点、边、属性类型),具体可参考Janusgraph文档

此外还注意到使用了OneTimeBulkLoader,这是一个Loader类,目前有两种Loader:

  1. OneTimeBulkLoader:一次性导入
  2. IncrementalBulkLoader:增量导入,在导入时会以源数据的id进行查询

通常来说为了性能选择OneTimeBulkLoader较好,除非确实需要去重增量,增量导入会进行大量的查询导致在没有索引的情况下速度基本上不可接受。每种Loader都会有自己的可配参数,例如提交批次等,具体请参考源码。

以上就是导入的基本方法,操作起来还是非常方便的,当然数据量比较大时还是要先进行优化试验。

导出

在导出方面,可以采用CloneVertexProgram(前身是BulkDumperVertexProgram,0.3.3版本的Janusgraph依赖的是3.3.3版本的gremlin-core,依然是BulkDumperVertexProgram)。如前所述,其利用的就是InputFormat输入、OutputFormat输出。看一下基本示例:

1
2
3
4
gremlin> graph = GraphFactory.open('conf/hadoop/hadoop-gryo.properties')
gremlin> graph.configuration().setProperty('gremlin.hadoop.graphWriter', 'org.apache.tinkerpop.gremlin.hadoop.structure.io.graphson.GraphSONOutputFormat')
gremlin> graph.configuration().setProperty('gremlin.hadoop.outputLocation', 'C:/janustest')
gremlin> graph.compute(SparkGraphComputer).program(BulkDumperVertexProgram.build().create()).submit().get()

同样也是在gremlin-console中直接操作,直接使用了Janusgraph提供的模板配置文件然后进行了一些修改(输出格式、输出路径),看一下这个配置文件的有效内容(删除了多余内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gremlin.graph=org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph
# 输入格式Inputformat
gremlin.hadoop.graphReader=org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoInputFormat
# 输出格式Outputformat,在运行时被覆盖
# gremlin.hadoop.graphWriter=org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoOutputFormat
gremlin.hadoop.jarsInDistributedCache=true

# 输入路径,在安装目录下可以看到具体的文件
gremlin.hadoop.inputLocation=data/tinkerpop-modern.kryo
# 输出路径,在运行时被覆盖
# gremlin.hadoop.outputLocation=output

####################################
# SparkGraphComputer Configuration #
####################################
spark.master=local[4]
spark.executor.memory=1g
spark.serializer=org.apache.spark.serializer.KryoSerializer
spark.kryo.registrator=org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoRegistrator

在执行完成后可以在指定的目录下看到输出的内容,默认的文件名为~g,由于采用了文本可读的GraphSON格式,可以直接打开文件查看其中的内容。

以上是OOTB(开箱即用)的导出方法,熟悉Hadoop相关工具的用户可以直接利用OutputFormat来进行定制操作而不需要拘泥于Tinkerpop给定的这个VertexProgram,也不必依赖于gremlin-console。

总结

在批量数据处理上,Tinkerpop借助于Hadoop等工具来实现高效运作,Janusgraph并没有给出特定的批处理工具,直接依托Tinkerpop并且给出了相关存储后端的InputFormat(HBase、Cassandra等,没有提供BerkeleyDB),使得能够直接读取存储端的内容,通过Hadoop、Spark等分布式大数据工具进行处理。Tinkerpop提供的一些通用的InputFormat(GraphSON、Script等)能够较为方便地配合读取外部数据源,实现数据对接、迁移。

在执行操作时,小批量的数据可以直接通过gremlin-console工具快速编写代码进行处理,注意由于涉及了Hadoop,要注意本地文件系统和HDFS文件系统的配置(gremlin-console提供了hdfs的处理接口,详情见Tinkerpop文档)。在数据量较大、运行时间较长或者对接过程中需要进行额外处理时,推荐新建工程项目,引入Janusgraph和Tinkerpop相关的依赖,编写代码进行定制化处理,使用过程中注意参考官方文档中的优化选项以及对Hadoop、Spark等具体执行工具的调优,提高数据处理的效率。

MapReduce任务提交避坑
Spring WebSocket简析