测试 JanusGraph 图 DBMS 解决寻找合适路径问题的适用性的实验

测试 JanusGraph 图 DBMS 解决寻找合适路径问题的适用性的实验

大家好。 我们正在开发一款用于离线流量分析的产品。 该项目的任务是对跨地区的游客路线进行统计分析。

作为此任务的一部分,用户可以询问以下类型的系统查询:

  • 有多少访客从“A”区前往“B”区;
  • 有多少访客从区域“A”通过区域“C”到达区域“B”,然后通过区域“D”;
  • 某种类型的访客从区域“A”前往区域“B”需要多长时间。

以及许多类似的分析查询。

访客跨区域的移动是一个有向图。 上网查了一下,发现图DBMS也可以用来做分析报告。 我很想看看图形 DBMS 如何处理此类查询(TL; DR; 不良)。

我选择使用DBMS 两面神图作为图开源DBMS的杰出代表,它依赖于一堆成熟的技术,(在我看来)应该为其提供不错的操作特性:

  • BerkeleyDB 存储后端、Apache Cassandra、Scylla;
  • 复杂的索引可以存储在Lucene、Elasticsearch、Solr中。

JanusGraph 的作者写道,它同时适用于 OLTP 和 OLAP。

我曾使用过 BerkeleyDB、Apache Cassandra、Scylla 和 ES,这些产品经常在我们的系统中使用,所以我对测试这个图 DBMS 持乐观态度。 我发现选择 BerkeleyDB 而不是 RocksDB 很奇怪,但这可能是由于事务要求所致。 无论如何,为了可扩展的产品使用,建议使用 Cassandra 或 Scylla 上的后端。

我没有考虑Neo4j,因为集群需要商业版本,即该产品不是开源的。

图 DBMS 说:“如果它看起来像图,就将其视为图!” - 美丽!

首先,我画了一个图表,它是完全按照图形 DBMS 的规范制作的:

测试 JanusGraph 图 DBMS 解决寻找合适路径问题的适用性的实验

有一个本质 Zone,负责该地区。 如果 ZoneStep 属于这个 Zone,然后他引用它。 论本质 Area, ZoneTrack, Person 不要注意,它们属于域,不被视为测试的一部分。 总的来说,这种图结构的链式搜索查询如下所示:

g.V().hasLabel('Zone').has('id',0).in_()
       .repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()

俄语中的内容是这样的:找到一个 ID=0 的区域,获取有一条边通往该区域的所有顶点(ZoneStep),不返回地踩踏,直到找到那些有一条边通往该区域的 ZoneSteps ID=19,统计这样的链的数量。

我并不假装知道在图表上搜索的所有复杂性,但这个查询是根据这本书生成的(https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

我使用 BerkeleyDB 后端将 50 个长度从 3 到 20 点不等的曲目加载到 JanusGraph 图形数据库中,根据以下内容创建索引 管理.

Python下载脚本:


from random import random
from time import time

from init import g, graph

if __name__ == '__main__':

    points = []
    max_zones = 19
    zcache = dict()
    for i in range(0, max_zones + 1):
        zcache[i] = g.addV('Zone').property('id', i).next()

    startZ = zcache[0]
    endZ = zcache[max_zones]

    for i in range(0, 10000):

        if not i % 100:
            print(i)

        start = g.addV('ZoneStep').property('time', int(time())).next()
        g.V(start).addE('belongs').to(startZ).iterate()

        while True:
            pt = g.addV('ZoneStep').property('time', int(time())).next()
            end_chain = random()
            if end_chain < 0.3:
                g.V(pt).addE('belongs').to(endZ).iterate()
                g.V(start).addE('goes').to(pt).iterate()
                break
            else:
                zone_id = int(random() * max_zones)
                g.V(pt).addE('belongs').to(zcache[zone_id]).iterate()
                g.V(start).addE('goes').to(pt).iterate()

            start = pt

    count = g.V().count().next()
    print(count)

我们使用了配备 4 核和 16 GB RAM、SSD 的虚拟机。 JanusGraph 使用以下命令部署:

docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest

在这种情况下,用于精确匹配搜索的数据和索引存储在 BerkeleyDB 中。 执行了之前给出的请求后,我收到了相当于几十秒的时间。

通过并行运行上述 4 个脚本,我成功地将 DBMS 变成了一个南瓜,在 Docker 日志中带有令人愉悦的 Java 堆栈跟踪流(我们都喜欢阅读 Java 堆栈跟踪)。

经过一番思考,我决定将图表简化为如下:

测试 JanusGraph 图 DBMS 解决寻找合适路径问题的适用性的实验

决定按实体属性搜索比按边搜索更快。 结果我的要求就变成了这样:

g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()

俄语的意思是这样的:找到ID=0的ZoneStep,踩下去,直到找到ID=19的ZoneStep,数一下这样的链的数量。

我还简化了上面给出的加载脚本,以免创建不必要的连接,将自己限制在属性上。

该请求仍然需要几秒钟才能完成,这对于我们的任务来说是完全不可接受的,因为它根本不适合任何类型的 AdHoc 请求的目的。

我尝试使用 Scylla 作为最快的 Cassandra 实现来部署 JanusGraph,但这也没有导致任何重大的性能变化。

因此,尽管“它看起来像一个图”,但我无法让图 DBMS 快速处理它。 我完全假设有一些我不知道的事情,并且可以让 JanusGraph 在不到一秒的时间内执行此搜索,但是,我无法做到这一点。

由于问题仍然需要解决,我开始考虑表的 JOIN 和 Pivots,这在优雅方面并没有激发乐观情绪,但在实践中可能是一个完全可行的选择。

我们的项目已经使用 Apache ClickHouse,因此我决定在这个分析 DBMS 上测试我的研究。

使用简单的配方部署 ClickHouse:

sudo docker run -d --name clickhouse_1 
     --ulimit nofile=262144:262144 
     -v /opt/clickhouse/log:/var/log/clickhouse-server 
     -v /opt/clickhouse/data:/var/lib/clickhouse 
     yandex/clickhouse-server

我创建了一个数据库和一个表,如下所示:

CREATE TABLE 
db.steps (`area` Int64, `when` DateTime64(1, 'Europe/Moscow') DEFAULT now64(), `zone` Int64, `person` Int64) 
ENGINE = MergeTree() ORDER BY (area, zone, person) SETTINGS index_granularity = 8192

我使用以下脚本填充了数据:

from time import time

from clickhouse_driver import Client
from random import random

client = Client('vm-12c2c34c-df68-4a98-b1e5-a4d1cef1acff.domain',
                database='db',
                password='secret')

max = 20

for r in range(0, 100000):

    if r % 1000 == 0:
        print("CNT: {}, TS: {}".format(r, time()))

    data = [{
            'area': 0,
            'zone': 0,
            'person': r
        }]

    while True:
        if random() < 0.3:
            break

        data.append({
                'area': 0,
                'zone': int(random() * (max - 2)) + 1,
                'person': r
            })

    data.append({
            'area': 0,
            'zone': max - 1,
            'person': r
        })

    client.execute(
        'INSERT INTO steps (area, zone, person) VALUES',
        data
    )

由于插入是批量进行的,因此填充速度比 JanusGraph 快得多。

使用 JOIN 构造了两个查询。 从 A 点移动到 B 点:

SELECT s1.person AS person,
       s1.zone,
       s1.when,
       s2.zone,
       s2.when
FROM
  (SELECT *
   FROM steps
   WHERE (area = 0)
     AND (zone = 0)) AS s1 ANY INNER JOIN
  (SELECT *
   FROM steps AS s2
   WHERE (area = 0)
     AND (zone = 19)) AS s2 USING person
WHERE s1.when <= s2.when

要经过3点:

SELECT s3.person,
       s1z,
       s1w,
       s2z,
       s2w,
       s3.zone,
       s3.when
FROM
  (SELECT s1.person AS person,
          s1.zone AS s1z,
          s1.when AS s1w,
          s2.zone AS s2z,
          s2.when AS s2w
   FROM
     (SELECT *
      FROM steps
      WHERE (area = 0)
        AND (zone = 0)) AS s1 ANY INNER JOIN
     (SELECT *
      FROM steps AS s2
      WHERE (area = 0)
        AND (zone = 3)) AS s2 USING person
   WHERE s1.when <= s2.when) p ANY INNER JOIN
  (SELECT *
   FROM steps
   WHERE (area = 0)
     AND (zone = 19)) AS s3 USING person
WHERE p.s2w <= s3.when

当然,这些请求看起来相当可怕;要真正使用,您需要创建一个软件生成器工具。 然而,它们确实有效,而且工作得很快。 第一个和第二个请求都在不到 0.1 秒的时间内完成。 下面是 count(*) 经过 3 个点的查询执行时间的示例:

SELECT count(*)
FROM 
(
    SELECT 
        s1.person AS person, 
        s1.zone AS s1z, 
        s1.when AS s1w, 
        s2.zone AS s2z, 
        s2.when AS s2w
    FROM 
    (
        SELECT *
        FROM steps
        WHERE (area = 0) AND (zone = 0)
    ) AS s1
    ANY INNER JOIN 
    (
        SELECT *
        FROM steps AS s2
        WHERE (area = 0) AND (zone = 3)
    ) AS s2 USING (person)
    WHERE s1.when <= s2.when
) AS p
ANY INNER JOIN 
(
    SELECT *
    FROM steps
    WHERE (area = 0) AND (zone = 19)
) AS s3 USING (person)
WHERE p.s2w <= s3.when

┌─count()─┐
│   11592 │
└─────────┘

1 rows in set. Elapsed: 0.068 sec. Processed 250.03 thousand rows, 8.00 MB (3.69 million rows/s., 117.98 MB/s.)

关于 IOPS 的说明。 填充数据时,JanusGraph 生成了相当高的 IOPS(四个数据填充线程为 1000-1300)并且 IOWAIT 相当高。 同时,ClickHouse 在磁盘子系统上产生的负载最小。

结论

我们决定使用 ClickHouse 来处理此类请求。 在将事件流加载到 ClickHouse 之前,我们始终可以使用 Apache Flink 预处理事件流,从而使用物化视图和并行化进一步优化查询。

性能非常好,我们甚至可能不需要考虑以编程方式旋转表。 以前,我们必须通过上传到 Apache Parquet 来对从 Vertica 检索的数据进行数据透视。

不幸的是,另一次使用图形 DBMS 的尝试没有成功。 我没有发现 JanusGraph 拥有一个友好的生态系统,可以让您轻松跟上该产品的速度。 同时配置服务器,采用的是传统的Java方式,这会让不熟悉Java的人流下血泪:

host: 0.0.0.0
port: 8182
threadPoolWorker: 1
gremlinPool: 8
scriptEvaluationTimeout: 30000
channelizer: org.janusgraph.channelizers.JanusGraphWsAndHttpChannelizer

graphManager: org.janusgraph.graphdb.management.JanusGraphManager
graphs: {
  ConfigurationManagementGraph: conf/janusgraph-cql-configurationgraph.properties,
  airlines: conf/airlines.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/airline-sample.groovy]}}}}

serializers:
# GraphBinary is here to replace Gryo and Graphson
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: true }}
  # Gryo and Graphson, latest versions
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { serializeResultToString: true }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  # Older serialization versions for backwards compatibility:
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { serializeResultToString: true }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoLiteMessageSerializerV1d0, config: {ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV2d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}
  - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}

processors:
  - { className: org.apache.tinkerpop.gremlin.server.op.session.SessionOpProcessor, config: { sessionTimeout: 28800000 }}
  - { className: org.apache.tinkerpop.gremlin.server.op.traversal.TraversalOpProcessor, config: { cacheExpirationTime: 600000, cacheMaxSize: 1000 }}

metrics: {
  consoleReporter: {enabled: false, interval: 180000},
  csvReporter: {enabled: false, interval: 180000, fileName: /tmp/gremlin-server-metrics.csv},
  jmxReporter: {enabled: false},
  slf4jReporter: {enabled: true, interval: 180000},
  gangliaReporter: {enabled: false, interval: 180000, addressingMode: MULTICAST},
  graphiteReporter: {enabled: false, interval: 180000}}
threadPoolBoss: 1
maxInitialLineLength: 4096
maxHeaderSize: 8192
maxChunkSize: 8192
maxContentLength: 65536
maxAccumulationBufferComponents: 1024
resultIterationBatchSize: 64
writeBufferHighWaterMark: 32768
writeBufferHighWaterMark: 65536
ssl: {
  enabled: false}

我意外地“放置”了 BerkeleyDB 版本的 JanusGraph。

该文档在索引方面相当扭曲,因为管理索引需要您在 Groovy 中执行一些相当奇怪的萨满教。 例如,创建索引必须通过在 Gremlin 控制台中编写代码来完成(顺便说一句,这不能开箱即用)。 来自 JanusGraph 官方文档:

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()

后记

从某种意义上来说,上面的实验是温暖与柔软的对比。 如果您考虑一下,图形 DBMS 会执行其他操作以获得相同的结果。 然而,作为测试的一部分,我还进行了一个实验,请求如下:

g.V().hasLabel('ZoneStep').has('id',0)
    .repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()

反映步行距离。 然而,即使在这样的数据上,图形 DBMS 显示的结果也超过了几秒钟......这当然是因为存在像这样的路径 0 -> X -> Y ... -> 1,图形引擎也检查了这一点。

即使对于像这样的查询:

g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()

我无法在不到一秒的处理时间内得到有效的答复。

这个故事的寓意是,美丽的想法和范式建模并不能带来期望的结果,使用 ClickHouse 的例子以更高的效率证明了这一点。 本文中介绍的用例是图 DBMS 的明显反模式,尽管它似乎适合在其范例中进行建模。

来源: habr.com

添加评论