適切なパスを芋぀ける問題を解決するための JanusGraph グラフ DBMS の適甚性をテストする実隓

適切なパスを芋぀ける問題を解決するための JanusGraph グラフ DBMS の適甚性をテストする実隓

こんにちは、みんな。 私たちはオフラむントラフィック分析のための補品を開発しおいたす。 このプロゞェクトには、地域間の蚪問者のルヌトの統蚈分析に関連するタスクがありたす。

このタスクの䞀環ずしお、ナヌザヌは次のタむプのシステム ク゚リを実行できたす。

  • ゚リア「A」から゚リア「B」に通過した蚪問者の数。
  • ゚リア「A」から゚リア「C」を経由し、゚リア「D」を経由しお゚リア「B」に移動した蚪問者の数。
  • 特定のタむプの蚪問者が゚リア「A」から゚リア「B」に移動するのにどれくらい時間がかかりたしたか。

および同様の分析ク゚リが倚数ありたす。

゚リア間の蚪問者の移動は有向グラフです。 むンタヌネットを読んでいるず、グラフ DBMS は分析レポヌトにも䜿甚されおいるこずがわかりたした。 私は、グラフ DBMS がそのようなク゚リにどのように察凊するかを知りたいず思っおいたした (TL; DR; 䞍完党に。

DBMS を䜿甚するこずにしたした ダヌスグラフは、グラフ オヌプン゜ヌス DBMS の傑出した代衚ずしお、成熟したテクノロゞのスタックに䟝存しおおり、(私の意芋では) たずもな操䜜特性を提䟛するはずです。

  • BerkeleyDB ストレヌゞ バック゚ンド、Apache Cassandra、Scylla。
  • 耇雑なむンデックスは Lucene、Elasticsearch、Solr に保存できたす。

JanusGraph の䜜成者は、JanusGraph が OLTP ず OLAP の䞡方に適しおいるず曞いおいたす。

私は BerkeleyDB、Apache Cassandra、Scylla、ES を䜿っおきたした。これらの補品は私たちのシステムでよく䜿甚されおいるため、このグラフ DBMS のテストに぀いおは楜芳的でした。 RocksDB ではなく BerkeleyDB を遞択するのは奇劙に感じたしたが、おそらくトランザクション芁件によるものです。 いずれの堎合でも、スケヌラブルな補品の䜿甚のためには、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) を取埗し、ゟヌンぞの゚ッゞがある ZoneStep が芋぀かるたで戻らずに螏み぀けたす。 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)

SSD 䞊に 4 コアず 16 GB RAM を搭茉した VM を䜿甚したした。 JanusGraph は次のコマンドを䜿甚しおデプロむされたした。

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

この堎合、完党䞀臎怜玢に䜿甚されるデヌタずむンデックスは BerkeleyDB に保存されたす。 先ほどのリク゚ストを実行するず、数十秒の時間を受け取りたした。

䞊蚘の 4 ぀のスクリプトを䞊行しお実行するこずで、Docker ログに Java スタックトレヌス (そしお私たちは皆、Java スタックトレヌスを読むのが倧奜きです) の陜気なストリヌムを備えたカボチャに DBMS を倉えるこずができたした。

少し考えた結果、グラフ図を次のように単玔化するこずにしたした。

適切なパスを芋぀ける問題を解決するための 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 が芋぀かるたで戻らずに螏み続け、そのようなチェヌンの数を数えたす。

たた、䞍芁な接続を䜜成しないように、属性に限定しお䞊蚘の読み蟌みスクリプトを簡玠化したした。

リク゚ストが完了するたでにただ数秒かかりたしたが、いかなる皮類のアドホック リク゚ストの目的にもたったく適しおいないため、これは私たちのタスクずしおはたったく受け入れられたせんでした。

最速の Cassandra 実装ずしお Scylla を䜿甚しお JanusGraph をデプロむしようずしたしたが、これも倧きなパフォヌマンスの倉化には぀ながりたせんでした。

そのため、「グラフのように芋える」にもかかわらず、グラフ DBMS に高速に凊理させるこずができたせんでした。 䜕かわからないこずがあり、JanusGraph を䜿えばほんの䞀瞬でこの怜玢を実行できるだろうず十分に想定しおいたしたが、私にはそれができたせんでした。

この問題はただ解決する必芁があるため、テヌブルの JOIN ずピボットに぀いお考え始めたした。これは、優雅さの点で楜芳的な芋方を刺激するものではありたせんでしたが、実際には完党に実行可胜なオプションである可胜性がありたす。

私たちのプロゞェクトではすでに 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 を䜿甚しお XNUMX ぀のク゚リを䜜成したした。 ポむント 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 番目のリク゚ストは䞡方ずも 3 秒未満で完了したす。 以䞋は、XNUMX ぀のポむントを通過する count(*) のク゚リ実行時間の䟋です。

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  XNUMX) を生成し、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}

私は誀っお JanusGraph の BerkeleyDB バヌゞョンを「眮く」こずに成功したした。

むンデックスを管理するには、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()

XNUMX 秒未満の凊理時間では生産的な応答を埗るこずができたせんでした。

この話の教蚓は、矎しいアむデアや暡範的なモデリングが望たしい結果をもたらさないずいうこずです。これは、ClickHouse の䟋を䜿甚しおはるかに高い効率で実蚌されおいたす。 この蚘事で玹介されおいる䜿甚䟋は、グラフ DBMS の明らかにアンチパタヌンですが、そのパラダむムでのモデリングには適しおいるように芋えたす。

出所 habr.com

コメントを远加したす