图系统-HgraphDB分析

今年年底来看, 图这方面最大的两个新闻, 一个是Tigergraph的单机开发者版免费放出, 已经正式登录国内 ; 另一个就是baidu的Hugegraph的开源, 它参考Titan/JanusGraph, 整体重写了全部代码, 大幅完善了不少图周边的生态. 最后还有个比较陌生的开源图HgraphDB, 阿里云目前有在Hbase里当做一个插件使用, 也就顺带简单看看吧…

0x00.目标

最早从NOSQL漫谈的这篇文章我们就可以知道, 当前JanusGraph存在以下几个核心改进点:

  1. 拆分表结构, 把原来的顶点&边混在一起改为各自单独存储. (甚至于进一步拆开系统和非系统图)
  2. 针对全序列化兼容后端这一块, 去掉不必要的序列化.
  3. 针对后端的Hbase/Cassandra, 利用它们自身的特性做定制和优化. 可以很大改善IO的效率.

但是因为华为并没有继续讲他们是如何做这些优化点的, 但是目前来看有两个比较好的参照实现: Hugegraph(主)和HgraphDB(次). 接下来文章后面主要关注HgraphDB的整体结构, 与JanusGraph的区别 ,其次简单梳理一下阿里云使用的相关情况, 最后部署测试一下看看实际效果.

0x01.存储结构

HgraphDB的全称是Hbase graph DB….(略冗长), 它实现相对简单不少, 只能基于Hbase去使用, 最初源于一个国外作者公司需要, 一个人开发的, 先来看看它. (它也实现了Tinkerpop, 支持Gremlin语法)

HgraphDB采用宽表模式 ,每张表都是key-column模式, 核心是以下几张表: (然后我给出一个demo的数据实例)

表结构按照Schema表和Data表分类:

Schema表(3个)

1.Label元信息表: 主要用于data check

存储的都是ID Type和PK Type

Row Key [label, element type] ID [type] CreatedAt UpdateAt Property1 key [type] Property2 key [type]
person/Vertex Long NA NA String Short
likes/Edge String NA NA String Date

2.Label关联表: (点边关系)

Row Key [from vertex label, edge label, to vertex label] CreatedAt UpdateAt
person/likes/dog NA NA

3.索引元信息表:

rowkey结构: [label, property key, element type]

isUnique字段指定索引value是否唯一,决定了索引数据表中value的存储位置

state表示索引状态,可用于索引修改、更新

Row Key CreatedAt UpdateAt isUnique State
person/name/Vertex NA NA true ACTIVE
likes/time/Edge NA NA true ACTIVE

Data表(4个)

Vertex表和Edge表中多个property都是按column分开存储,便于更新

1.Vertex表:

Row Key(vid) (v)Label CreatedAt UpdateAt PropertyKey1(e.g. name) PropertyKey2(e.g. age) …..
101 person NA NA Jin 22 …..
201 dog NA NA Dove 3

2.Edge表:

Row Key(eid) (e)Label fromVertex toVertex CreatedAt UpdateAt PropertyKey1(e.g relation) PropertyKey2(e.g time) ……
4 likes 101 201 NA NA master 2018

3.Vertex索引表:

rowkey结构: [vertex label, isUnique, property key, property value, vertex ID (if not unique)]

若isUnique = false, 多个vertexID会分别拼接到rowkey的最后,利用HBase的scan去做查询

Row Key(复合) createdAt UpdateAt VertexID
person/true/name,age/Jin NA NA 101

4.Edge索引表:

rowkey结构: [vertex1 ID, direction, isUnique, property key, edge label, property value, vertex2 ID (if not unique), edge ID (if not unique)]

备注:vertex2 ID和edge ID同样拼接到row key 的最后

Row Key CreatedAt UpdateAt VertexID EdgeID
101/方向/true/likes/relation,time/master,2018 NA NA 201 4

0x02. 实际上手

因为hgraphDB项目比较冷门, 所以除了github的README ,几乎没有任何文档, 也没有打包好的包测试环境(类似Janus那样), 只能通过自己新建一个项目去手动启动然后加载到gremlin中.简单两步骤如下:

  1. IDEA中新建一个maven项目, 然后在pom.xml 里添加如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
      <dependencies>
    <dependency>
    <groupId>io.hgraphdb</groupId>
    <artifactId>hgraphdb</artifactId>
    <version>2.0.1</version>
    </dependency>
    </dependencies>

    <!--然后由于引入了大量的hbase库,有不少日志和guava版本的重复引用,如果熟悉引用建议去掉(当然不管它可能也没什么)-->
  2. 接下来新建一个init包, 并新建一个GraphInit类去生成一个实例, 并写一些插入和查询语句. 这是我简单写的例子.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    package init;

    import io.hgraphdb.*;
    import io.hgraphdb.HBaseGraphConfiguration.InstanceType;
    import org.apache.commons.configuration.Configuration;
    import org.apache.tinkerpop.gremlin.structure.T;
    import org.apache.tinkerpop.gremlin.structure.Vertex;
    import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;

    import java.time.LocalDate;
    import java.util.Iterator;

    /**
    * 测试HgraphDB的初始化类
    * 注意 : Hbase2.x上测试是有一些奇怪的报错的, 比如清空表失败..如非必要还是在hbase1.4上测吧
    * */
    public class GraphInit {
    public static void main(String[] args) {
    //配置连接Hbase,启动实例
    HBaseGraph graph = (HBaseGraph) GraphFactory.open(configGraph()); //在Hbase配置类里还有其他许多参数,请注意自行设置

    //清空方法
    clearGraph(graph);
    //初始化方法
    creatSchema(graph);
    //查询操作
    query(graph);
    }

    static Configuration configGraph() {
    Configuration config = new HBaseGraphConfiguration()
    .setUseSchema(true)
    .setInstanceType(InstanceType.DISTRIBUTED)
    .setGraphNamespace("graph_jin") //自己的db-name
    .setCreateTables(true)
    .setRegionCount(3) //最少是3个,否则会终止
    .set("hbase.zookeeper.quorum", "10.xx.xx.xx,10.xx.xx.xx")
    .set("zookeeper.znode.parent", "/hbase"); //这里配置的'/hbase',需要与hbase的配置文件中的"zookeeper.znode.parent"参数配置保持一致
    return config;
    }

    static void query(HBaseGraph graph) {
    // 获得名字是John的点
    Iterator<Vertex> it = graph.verticesByLabel("person", "name", "John");

    // get persons first known by John between 2007-01-01 (inclusive) and 2008-01-01 (exclusive)
    /*Iterator<Edge> it2 = v1.edges(Direction.OUT, "knows", "since", LocalDate.parse("2018-01-01"), LocalDate.parse
    ("2019-01-01"));*/

    System.out.println("检查数据");
    /*分页功能
    * */
    //获得第一页的persons (注意: null is passed as start key)
    final int pageSize = 20;
    Iterator<Vertex> it3 = graph.verticesWithLimit("person", "name", null, pageSize);
    int i = 1;
    while (it3.hasNext()) System.out.println("顶点"+i+it3.next()); //hbase2下暂时无输出

    //用第一页的最后一个人作为start-key获取第二页内容
    it = graph.verticesWithLimit("person", "name", "John", pageSize + 1);

    //获取John最近认识的人的第一页(相当于limit)
    //Iterator<Edge> it4 = v1.edgesWithLimit(Direction.OUT, "knows", "since", null, pageSize, /* reversed */ true);
    }

    static void creatSchema(HBaseGraph graph) {
    //直接插入数据, 可自定义ID
    graph.createLabel(ElementType.VERTEX, "person", ValueType.LONG, "name", ValueType.STRING);
    graph.createLabel(ElementType.EDGE, "knows", ValueType.STRING, "since", ValueType.DATE);
    graph.connectLabels("person", "knows", "person");
    final Vertex v1 = graph.addVertex(T.id, 1L, T.label, "person", "name", "John");
    final Vertex v2 = graph.addVertex(T.id, 2L, T.label, "person", "name", "Sally");
    v1.addEdge("knows", v2, T.id, "edge1", "since", LocalDate.now());

    //用Schema模式来创建点/边,边必须单独连接起来
    graph.createLabel(ElementType.VERTEX, "author", ValueType.STRING, "age", ValueType.INT);
    graph.createLabel(ElementType.VERTEX, "book", ValueType.STRING, "publisher", ValueType.STRING);
    graph.createLabel(ElementType.EDGE, "writes", ValueType.STRING, "since", ValueType.DATE);
    graph.connectLabels("author", "writes", "book");

    //开启Schema模式后,还支持一个计数(类型)功能,通过新增属性的方式加入
    graph.updateLabel(ElementType.VERTEX, "person", "personCount", ValueType.COUNTER);
    HBaseVertex cv1 = (HBaseVertex) graph.addVertex(T.id, 1L, T.label, "person");
    cv1.incrementProperty("personCount", 1L);

    graph.createLabel(ElementType.VERTEX, "author", ValueType.STRING, "bookCount", ValueType.COUNTER);
    HBaseVertex cv2 = (HBaseVertex) graph.addVertex(T.id, "Kierkegaard", T.label, "author");
    cv2.incrementProperty("bookCount", 1L);


    //支持顶点和边两种索引
    graph.createIndex(ElementType.VERTEX, "person", "name");
    graph.createIndex(ElementType.EDGE, "knows", "since");
    System.out.println("Schema创建完成");
    graph.close();
    }


    //clear or drop graph (maybe drop namespace?)
    static void clearGraph(HBaseGraph graph) {
    graph.drop(); //实际底层操作是检查表是否存在-->存在就先disable-->再truncate(情况数据) --> 最后enable打开.
    System.err.println("清空表成功");
    graph.close();
    }
    }

0x03. 核心点探查

零. 代码结构

这是HgraphDB的整体代码结构图 ,如果看过之前Tinkerpop自己作者实现的Tinkergraph , 会发现这二者的结构是非常类似的, 首先只要是实现了Tinkerpop 的图,都必须遵循它得图数据结构定义, 实现Element ,Vertex, Edge ,Property ,VertexProperty ,Graph 这些必要的接口. 然后

hgraphDB04

一. 两种写入

HgraphDB提供两种方式进行写入, 一种是上面例子里的原生方式, 另一种是批量导入, 具体的差别和底层是什么呢? 具体看看代码

它自带有个HbaseBulkLoader类, 这个类简单封装了Hbase-client 的BufferedMutate接口的mutate方法, 与上面的常规插入点边对比: 本质是Hbase单条PUT批量写的区别, 核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*1.这是直接写入的底层实际调用,省略异常*/
public Vertex addVertex(final Object... keyValues) {
//看过Tinkerpop的同学对这个步骤应该很熟悉,在Tinkergraph基本也是类似实现
ElementHelper.legalPropertyKeyValueArray(keyValues);
Object idValue = ElementHelper.getIdValue(keyValues).orElse(null);
final String label = ElementHelper.getLabelValue(keyValues).orElse(Vertex.DEFAULT_LABEL);
//ID生成这也和Tinkergraph基本一致,要么直接使用用户的Long,如果为null就UUID生成一个随机的.(几行代码..)
idValue = HBaseGraphUtils.generateIdIfNeeded(idValue);
long now = System.currentTimeMillis();
HBaseVertex newVertex = new HBaseVertex(this, idValue, label, now, now, HBaseGraphUtils.propertiesToMap(keyValues));
newVertex.validate();
newVertex.writeToIndexModel(); //这里是实际的写入过程,包括写顶点索引表,trace到最后是调用的Mutate.create(),见下
newVertex.writeToModel();
Vertex vertex = findOrCreateVertex(idValue); //这里会检查id是否存在,存在就不重复写了. BulkLoader不会检查
((HBaseVertex) vertex).copyFrom(newVertex);
return vertex;
}

private static void create(Table table, Creator creator, Put put) {
byte[] row = put.getRow();
//可以看到实际是调用了hbase的checkAndPut()方法.
boolean success = table.checkAndPut(row, Constants.DEFAULT_FAMILY_BYTES,creator.getQualifierToCheck(), null, put);
if (!success) {
HBaseElement element = (HBaseElement) creator.getElement();
if (element != null) element.removeStaleIndices();
}



/*2.这是提供的BulkLoader方法写入核心,同样以写顶点为例.其他类似*/
public Vertex addVertex(final Object... keyValues) {
ElementHelper.legalPropertyKeyValueArray(keyValues);
Object idValue = ElementHelper.getIdValue(keyValues).orElse(null);
final String label = ElementHelper.getLabelValue(keyValues).orElse(Vertex.DEFAULT_LABEL);
idValue = HBaseGraphUtils.generateIdIfNeeded(idValue);
long now = System.currentTimeMillis();
HBaseVertex vertex = new HBaseVertex(graph, idValue, label, now, now,HBaseGraphUtils.propertiesToMap(keyValues));
vertex.validate();
Iterator<IndexMetadata> indices = vertex.getIndices(OperationType.WRITE);
indexVertex(vertex, indices);
Creator creator = new VertexWriter(graph, vertex);
if (verticesMutator != null) verticesMutator.mutate(getMutationList(creator.constructInsertions()));//这步是实际写入,见下
return vertex;
}

//hbase的源码方法,简化一下 (去check)
public void mutate(List<? extends Mutation> ms) throws InterruptedIOException, RetriesExhaustedWithDetailsException {
if (this.closed) throw new IllegalStateException("Cannot put when the BufferedMutator is closed.");
long toAddSize = 0L;
int toAddCount = 0;

for(Iterator i$ = ms.iterator(); i$.hasNext(); ++toAddCount) {
Mutation m = (Mutation)i$.next();
if (m instanceof Put) this.validatePut((Put)m);
toAddSize += m.heapSize();
//持续往buffer里写数据,然后等到了一个阈值提一次.
this.currentWriteBufferSize.addAndGet(toAddSize);
this.writeAsyncBuffer.addAll(ms);
this.undealtMutationCount.addAndGet(toAddCount);
//多久刷新, 也就是commit一次.这之后才写到了磁盘的hfile文件
while(this.undealtMutationCount.get() != 0 && this.currentWriteBufferSize.get() > this.writeBufferSize) {
this.backgroundFlushCommits(false);
}}}

从上面自带的两种写入方式来看, 可以简单理解为就是:

  • Hbase单条写(比如从hbase的命令行里插入数据) , 和N条数据一起写插入数据的区别,
  • 批量写入的时候不会检查ID是否存在,节省了一次查的时间.

二. 写入工具

因为官方的HbaseBulkLoader实际就提供了一个口子, 现在让你导入10亿的顶点, 那怎么导呢? 怎么读数据, 中途失败了怎么办, 导了多少怎么看…

这实际就是之前原生JanusGraph 中也存在的问题, 缺乏一个批量导入的Loader工具 ,而因为HgraphDB是个人项目, 作者也没有管这个事, 直到偶然看到12月的一次分享里有阿里云的人分享了一次, 于是我本着一探究竟的精神, 就看看到底是什么回事….

虽然感觉是悄悄上线的, 不过找了半天….还是通过蛛丝马迹找到了阿里云自己写的一个导入工具 地址, 这里就来简单体验一下, 并看看有没有做什么进一步的优化 (功能 / 性能上). 首先来看看整体的代码结构和如何使用 :

(1). 使用

批量导入的准备工作:

  1. 准备图的schema文件
  2. 准备vertex文件
  3. 准备edge文件

最后使用hgraph-loader执行命令, 通过main()传入参数执行相关的方法.

先看看给的数据格式参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//这是schema.json文件,定义图的Schema结构,主要是围绕顶点/边 + 属性 + 属性索引
{
"vertexLabels" : [ {"name" : "T1","properties" : [ {"name" : "id","type" : "Long"}, {"name" : "T1-P1","type" : "String"} ],"indexes"
:[{"propertykey" : "T1-P1","unique" : false} ]},
{"name" : "T2","properties" : [ {"name" : "id","type" : "Long"}, {"name" : "T2-P1","type" : "String"}, {"name" :
"T2-P2","type" : "Integer"} ],"indexes" : [ {"propertykey" : "T2-P1","unique" : false}, {"propertykey" : "T2-P2","unique" : false} ]}],

//接下来是边和边索引/连接关系
"edgeLabels" : [ {"name" : "E1","properties" : [ {"name" : "E1-P1","type" : "Long"} ],} ],
"indexes" : [ {"propertykey" : "E1-P1","unique" : false} ],
"connections" : [ {"outV" : "T1","inV" : "T2"}, {"outV" : "T2","inV" : "T1"} ]} ]}

//这是顶点的对应数据demo
1,T1,scpEXoszLC
2,T1,Cyfxxhphnp
3,T1,SugveBDZOA
1000001,T2,SAuHvagzMi,78115824
1000002,T2,mSKCoZQxwa,61369275
1000003,T2,VdwhEhooeT,78680532

//这是边的对应数据demo
1,1000001,E1,41009407
2,1000002,E1,54003294
3,1000003,E1,8987954

个人觉得这个schema的表示方式的确还是有点容易出问题….虽然json比较好懂..

从启动脚本里可以知道, generator.GraphBench.javaimporter.BatchImport.javaschema.SchemaLoader.java 是三个启动入口, 功能也比较独立:

  • 快速生成不同规模的测试数据集
  • 加载Schema (通过读取JSON文件)
  • 批量导入数据

但是这里发现好像直接复制了IBM的JanusGraph-Utils项目的大部分脚本代码结构 …. (但是没有署名引用, 却把License直接改成阿里的了,貌似这样不太合适吧…)

hgraphDB01

从shell和源码内传参顺序可以得知, 运行脚本方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#生成数据,传入模板,和输出地址即可
sh run.sh gencsv $csvConfPath $outputPath

#加载Schema
sh run.sh loadsch

#整合了一下,这里导入数据也可以加载/跳过Schema
sh run.sh import $zk1,$zk2,$zk3 ./test/schema.json ./test/vertex ./test/edge skipSchema

#然后有个单独的配置文件可以定义导入的一些配置,如下:
workers = 16 #导入线程个数
regionCount = 16 #预创建分片个数
graph.name =hgraph #导入图名
workers.target_record_count = 20000 #一个线程处理多少行数据

接下来主要看看代码部分

(2). 代码结构

更新: 这里发现代码结构也大量复制的IBM的JanusGraph-Utils项目, 只是改了一下适用于HgraphDB…. 这是IBM项目的结构和代码..(见下图对比)但是同样没有看到任何声明和许可, 作为公开的使用/宣讲的项目(个人觉得应该予以说明吧…, 虽然貌似目前国内大家也都互相抄抄改改习惯了….)

HgraphDB03)hgraphDB00

抛开这个, 先来看看代码本身吧,

代码分析未完待续….

1.19更新: 单纯HgraphDB的代码因为毕竟简单, 个人觉得可以先看看TinkerGraph 源码分析, 其实很多地方沿用了Tinkerpop 已有的读写实现, 而且不少功能也没有很好实现, 特别一点的在下面有单独说明 ,但是看完发现也没啥… 其他地方应该不是很需要单独源码分析了.

3.读取

这里主要列出与JanusGraph区别的地方 :

(1) label查询

HgraphDB的索引只针对属性(property)建立,如果要命中索引,

对于vertex,至少需要label、PK两个参数;

对于edge,至少需要Vid1、direction、 label、PK(默认是create_At)

若只传入参数label, 则会scan全表

1
2
3
4
5
6
7
8
9
10
//举例:Iterator<Vertex> it = graph.verticesByLabel("a");
//实际调用方法如下
protected Scan getPropertyScan(String label) {
Scan scan = new Scan();
SingleColumnValueFilter valueFilter = new SingleColumnValueFilter(Constants.DEFAULT_FAMILY_BYTES,
Constants.LABEL_BYTES, CompareFilter.CompareOp.EQUAL, new BinaryComparator(ValueUtils.serialize(label)));
valueFilter.setFilterIfMissing(true);
scan.setFilter(valueFilter);
return scan;
}

(2) 索引查询

定值查询(value):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//简化代码如下
public Iterator<Vertex> vertices(String label, String key, Object value) {
ElementHelper.validateProperty(key, value);
IndexMetadata index = graph.getIndex(OperationType.READ, ElementType.VERTEX, label, key);
if (index != null) {
return graph.getVertexIndexModel().vertices(label, index.isUnique(), key, value);}
final VertexReader parser = new VertexReader(graph);
byte[] val = ValueUtils.serializePropertyValue(graph, ElementType.VERTEX, label, key, value);
final byte[] keyBytes = Bytes.toBytes(key);
Scan scan = getPropertyScan(label, keyBytes, val);
ResultScanner scanner = null;
scanner = table.getScanner(scan);
return HBaseGraphUtils.mapWithCloseAtEnd(scanner, parser::parse);
}

区间查询(range):主要用于int, date等类型的查询,本质利用HBase的Bytes.compareTo()方法实现

limit查询:利用HBase的compareFilter实现, 这里可以设置reverse

(3) 计数功能(COUNTER)

这个功能实际只是利用了HBase的计数器,在valueType中定义了COUNTER,作为一个属性存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//举例
graph.createLabel(ElementType.VERTEX, "c", ValueType.LONG, "key2", ValueType.COUNTER);
HBaseVertex v1 = (HBaseVertex) graph.addVertex(T.id, 10L, T.label, "c", "key2", 11L);
v1.incrementProperty("key2", 1L); //key2值为12L
v1.incrementProperty("key2", -2L); //key2值为10L

//实际是利用PropertyIncrementer类实现
public Iterator<Mutation> constructMutations() {
Increment incr = new Increment(ValueUtils.serializeWithSalt(element.id()));
incr.addColumn(Constants.DEFAULT_FAMILY_BYTES, Bytes.toBytes(key), value);
Put put = new Put(ValueUtils.serializeWithSalt(element.id()));
put.addColumn(Constants.DEFAULT_FAMILY_BYTES, Constants.UPDATED_AT_BYTES,
ValueUtils.serialize(((HBaseElement) element).updatedAt()));
return IteratorUtils.of(incr, put);
}

(4) gremlin接口

TinkerPop提供了一个公共静态接口Graph.Features,用于表示Graph实现的gremlin功能。默认情况下,所有功能方法都会返回true`,也可以禁用不支持的功能,在使用TinkerPop的各种功能之前需要检查。

HGraphDB只是支持了比较基本的gremlin语句查询,包括查点、查边、查属性,对于Computer(path、pagerank等)和Transaction则没有提供支持。

附一些基本的查询语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
graph.createIndex(ElementType.VERTEX, "a", "key1");
graph.addVertex(T.id, id(0), T.label, "a", "key1", 0);
graph.addVertex(T.id, id(1), T.label, "a", "key1", 1);
graph.addVertex(T.id, id(2), T.label, "a", "key1", 2);
graph.addVertex(T.id, id(3), T.label, "a", "key1", 3);
graph.addVertex(T.id, id(4), T.label, "a", "key1", 4);
graph.addVertex(T.id, id(5), T.label, "a", "key1", 5);

//查点
Iterator<Vertex> it = graph.verticesInRange("a", "key1", 1, 4);

//查边
Iterator<Edge> it = ((HBaseVertex) v1).edges(Direction.OUT, "a", "key1", 1);
Iterator<Edge> it = ((HBaseVertex) v0).edgesInRange(Direction.OUT, "b", "key1", 2, 5);
it = ((HBaseVertex) v1).edgesWithLimit(Direction.OUT, "a", "key1", 1, 4);

//Gremlin
GraphTraversalSource g = graph.traversal();
Iterator<Vertex> it = g.V().has("a", "key1", 0);
Iterator<Edge> it = g.V(id(1)).outE().has("b", "key1", 11);

4.图计算

HgraphDB虽然在存储端做的事不多, 但是整合了多个图计算的框架, 有如下:

0x04.性能测试

采用的是文中提到了导入工具, 因为HgraphDB的读取测试几乎没法进行, 太多功能不支持. 主要是给一下写入测试参考: (3台Hbase节点,配置同Hugegraph测试一文.)

整体来看:

写入速度良好, 平均插入点15万/秒,插入边9万/秒. 但不支持事务, 基本的CURD支持很弱,很难满足常用业务需求 .

1
2
3
4
5
--------------------dns-------------------------
time results:
2vertices loading time : 360 秒 --> 56226722点
2edges loading time : 5940 秒 --> 537860000边
2total loading time : 6300 秒

Server端资源消耗

  • CPU: 1427% (us 39.6%)
  • MEM:7.3%
  • 网络:112M/s

注意: 与Hugegraph相比, HgraphDB的写入点边的速度是相反的, 这里后续会进一步研究具体原因. 以及Hugegraph为什么一条数据写两条边比顶点还要快许多.

附:A graph transaction supports

  • Creating vertices, properties and edges
  • Creating types
  • Index-based retrieval of vertices
  • Querying edges and vertices (批量点/边查)
  • Aborting and committing transaction

0x05. 小结

1. 区别

以下是HgraphDB的不同于JanusGraph的特点:

  1. 拆分了点、边的表结构,属性分开存储(不拼接到一个value里)
  2. 允许用户自定义点/边ID(数字/字符串)
  3. 利用Hbase原生特性实现了范围查询和limit查询
  4. 利用Hbse实现了计数器的功能(目前看意义不大)
  5. 无序列化

HgraphDB的结构比较简单, 与Hbase可以深度集成, 如果需要是可以在这基础上做一些更深的定制开发. 配合导入工具后, 导入过程应该还算简单清晰。

2. 思考

目前可以想到的优缺点大致列一下:

优点:

  1. 结构简单清晰
  2. 表结构的设计与janusgraph相比确实合理很多
  3. 类似的提供schema.json 的文件方式(简化)给业务直接使用去新增/修改Schema, 而不是直接让他们用SDK去写Java程序.

缺点:

  1. gremlin支持的功能接口较少 (更新: 是很少, 会严重影响生产环境使用级别.)
  2. 周边工具很少.
  3. 没有完整的测试.
  4. 对于图计算的支持程度还需要进一步确认…

1.27更新: 实际的性能测试已经更新, 后续可能还会更新一下导入工具的相关对比分析… (其他估计不会更新了)


参考资料:
  1. HgraphDB官方实现说明
  2. 作者博客-HgraphDB: HBASE AS A TINKERPOP GRAPH DATABASE
  3. 阿里云借鉴 Janus的的导入工具