En Experiment deen d'Uwendbarkeet vun der JanusGraph Grafik DBMS testen fir de Problem ze léisen fir gëeegent Weeër ze fannen

En Experiment deen d'Uwendbarkeet vun der JanusGraph Grafik DBMS testen fir de Problem ze léisen fir gëeegent Weeër ze fannen

Moien alleguer. Mir entwéckelen e Produkt fir offline Verkéiersanalyse. De Projet huet eng Aufgab am Zesummenhang mat statistesch Analyse vun Visiteur routes ganze Regiounen.

Als Deel vun dëser Aufgab kënnen d'Benotzer d'Systemufroe vun der folgender Zort froen:

  • wéivill Visiteuren aus der Géigend "A" op d'Géigend "B" passéiert sinn;
  • wéivill Visiteuren aus der Géigend "A" an d'Gebitt "B" duerch d'Gebitt "C" an dann duerch d'Gebitt "D" passéiert sinn;
  • wéi laang et gedauert huet fir e bestëmmten Typ vu Besucher vum Gebitt "A" op Gebitt "B" ze reesen.

an eng Rei vun ähnlechen analyteschen Ufroen.

D'Bewegung vum Besucher iwwer Gebidder ass eng geriicht Grafik. Nodeems ech den Internet gelies hunn, hunn ech entdeckt datt Grafik DBMSs och fir analytesch Berichter benotzt ginn. Ech hat e Wonsch ze gesinn wéi Grafik DBMSs mat sou Ufroen këmmeren (TL; DR; schlecht).

Ech hu gewielt den DBMS ze benotzen JanusGraf, als aussergewéinleche Vertrieder vu Grafik Open-Source DBMS, deen op e Stack vu reife Technologien hänkt, déi (menger Meenung no) et anstänneg operationell Charakteristiken ubidden:

  • BerkeleyDB Stockage Backend, Apache Cassandra, Scylla;
  • komplex Indizes kënnen zu Lucene, Elasticsearch, Solr gespäichert ginn.

D'Auteuren vum JanusGraph schreiwen datt et fir OLTP an OLAP gëeegent ass.

Ech hu mat BerkeleyDB, Apache Cassandra, Scylla an ES geschafft, an dës Produkter ginn dacks an eise Systemer benotzt, also war ech optimistesch fir dës Grafik DBMS ze testen. Ech hunn et komesch fonnt fir BerkeleyDB iwwer RocksDB ze wielen, awer dat ass wahrscheinlech wéinst den Transaktiounsufuerderunge. Op alle Fall, fir skalierbar Produktgebrauch, gëtt proposéiert e Backend op Cassandra oder Scylla ze benotzen.

Ech hunn Neo4j net berücksichtegt well Clustering eng kommerziell Versioun erfuerdert, dat heescht, de Produkt ass net Open Source.

Graf DBMSs soen: "Wann et ausgesäit wéi eng Grafik, behandelt se wéi eng Grafik!" - Schéinheet!

Als éischt hunn ech eng Grafik gezeechent, déi genau no de Kanonen vun de Grafik DBMSs gemaach gouf:

En Experiment deen d'Uwendbarkeet vun der JanusGraph Grafik DBMS testen fir de Problem ze léisen fir gëeegent Weeër ze fannen

Et gëtt eng Essenz Zone, responsabel fir de Beräich. Wann ZoneStep derzou gehéiert Zone, dann bezitt hien op et. Op Essenz Area, ZoneTrack, Person Opgepasst net, si gehéieren zum Domain a ginn net als Deel vum Test ugesinn. Am Ganzen géif eng Kette Sich Ufro fir sou eng Grafikstruktur ausgesinn wéi:

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

Wat op Russesch ass sou eppes: Fannt eng Zone mat ID=0, huelt all d'Wénkelen vun deenen e Rand dohinner geet (ZoneStep), trampelt ouni zréck ze goen bis Dir déi ZoneSteps fannt, vun deenen et e Rand an d'Zone ass mat ID = 19, zielt d'Zuel vun esou Ketten.

Ech maachen net wéi wann ech all d'Intricacies vun der Sich op Grafike kennen, awer dës Ufro gouf op Basis vun dësem Buch generéiert (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Ech hunn 50 Tausend Tracks gelueden, rangéiert vun 3 bis 20 Punkten an der Längt an eng JanusGraph Grafik Datebank mam BerkeleyDB Backend, erstallt Indexen laut Féierung.

Python Download Skript:


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)

Mir hunn e VM mat 4 Cores an 16 GB RAM op enger SSD benotzt. JanusGraph gouf mat dësem Kommando ofgesat:

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

An dësem Fall sinn d'Donnéeën an d'Indexen, déi fir exakt Match Sich benotzt ginn, am BerkeleyDB gespäichert. Nodeems ech d'Ufro virdru gemaach hunn, krut ech eng Zäit gläich wéi e puer Zénger Sekonnen.

Andeems ech déi 4 uewe genannte Skripte parallel laafen, hunn ech et fäerdeg bruecht den DBMS an e Kürbis ze maachen mat engem lëschtegen Stroum vu Java Stacktraces (a mir hunn all gär Java Stacktraces ze liesen) an den Docker Logbicher.

No e puer Gedanken hunn ech décidéiert de Grafdiagramm op déi folgend ze vereinfachen:

En Experiment deen d'Uwendbarkeet vun der JanusGraph Grafik DBMS testen fir de Problem ze léisen fir gëeegent Weeër ze fannen

Entscheeden datt d'Sich no Entitéitsattributer méi séier wier wéi d'Sich no Kanten. Als Resultat ass meng Ufro an de folgende verwandelt:

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

Wat op Russesch ass sou eppes: Fannt ZoneStep mat ID=0, stompt ouni zréck ze goen bis Dir ZoneStep mat ID=19 fannt, zielt d'Zuel vun esou Ketten.

Ech hunn och de Luede Skript uewe vereinfacht fir net onnéideg Verbindungen ze kreéieren, mech op Attributer ze limitéieren.

D'Ufro huet nach e puer Sekonnen gedauert fir ze kompletéieren, wat fir eis Aufgab komplett inakzeptabel war, well se guer net gëeegent war fir Zwecker vun AdHoc Ufroe vun iergendenger Aart.

Ech hu probéiert de JanusGraph mat Scylla als déi schnellsten Cassandra Implementatioun z'installéieren, awer dëst huet och keng bedeitend Performance Ännerungen gefouert.

Also trotz der Tatsaach datt "et ausgesäit wéi eng Grafik", konnt ech d'Grafik DBMS net kréien fir se séier ze veraarbecht. Ech huelen ganz un datt et eppes ass wat ech net weess an datt JanusGraph ka gemaach ginn fir dës Sich an enger Fraktioun vun enger Sekonn auszeféieren, awer ech konnt et net maachen.

Well de Problem nach ëmmer geléist muss ginn, hunn ech ugefaang iwwer JOINs a Pivots vun Dëscher ze denken, déi net Optimismus a punkto Eleganz inspiréiert hunn, awer an der Praxis eng komplett funktionabel Optioun kéinte sinn.

Eise Projet benotzt schonn Apache ClickHouse, also hunn ech beschloss meng Fuerschung iwwer dës analytesch DBMS ze testen.

Deployéiert ClickHouse mat engem einfache Rezept:

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

Ech hunn eng Datebank an eng Tabell an et esou erstallt:

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

Ech hunn et mat Daten gefëllt mat dem folgenden Skript:

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
    )

Zënter Inserts a Chargen kommen, war d'Füllung vill méi séier wéi fir JanusGraph.

Konstruéiert zwou Ufroen mat JOIN. Fir vum Punkt A op de Punkt B ze réckelen:

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

Fir duerch 3 Punkten ze goen:

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

D'Ufroen, natierlech, kucken ganz grujeleg; fir real Benotzung, musst Dir e Software Generator Harness erstellen. Wéi och ëmmer, si schaffen a si schaffen séier. Souwuel déi éischt wéi och zweet Ufroe ginn a manner wéi 0.1 Sekonnen ofgeschloss. Hei ass e Beispill vun der Ufro Ausféierung Zäit fir Grof (*) duerch 3 Punkten passéieren:

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

Eng Notiz iwwer IOPS. Beim Bevëlkerungsdaten generéiert JanusGraph eng zimlech héich Zuel vun IOPS (1000-1300 fir véier Datepopulatioun thread) an IOWAIT war zimlech héich. Zur selwechter Zäit generéiert ClickHouse minimal Belaaschtung um Disk Subsystem.

Konklusioun

Mir hunn decidéiert ClickHouse ze benotzen fir dës Zort Ufro ze servéieren. Mir kënnen ëmmer weider Ufroen optimiséieren mat materialiséierte Meenungen a Paralleliséierung andeems Dir den Eventstroum mat Apache Flink virveraarbechtt ier se an ClickHouse lued.

D'Performance ass sou gutt datt mir wahrscheinlech net emol mussen iwwer d'Schwenktabellen programmatesch denken. Virdrun hu mir Pivots vun Daten aus Vertica iwwer Eroplueden op Apache Parquet gemaach.

Leider war en anere Versuch fir eng Grafik DBMS ze benotzen net erfollegräich. Ech hunn de JanusGraph net fonnt fir e frëndlechen Ökosystem ze hunn deen et einfach gemaach huet mat dem Produkt opzemaachen. Zur selwechter Zäit, fir de Server ze konfiguréieren, gëtt den traditionelle Java-Wee benotzt, wat d'Leit, déi net mat Java vertraut sinn, Tréinen vu Blutt maachen:

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}

Ech hunn et fäerdeg bruecht zoufälleg d'BerkeleyDB Versioun vum JanusGraph ze "setzen".

D'Dokumentatioun ass zimmlech kromm wat d'Index ugeet, well d'Gestioun vun Indexen erfuerdert datt Dir e bësse komesche Shamanismus am Groovy ausféiert. Zum Beispill, en Index ze kreéieren muss gemaach ginn andeems Dir Code an der Gremlin Konsole schreift (wat iwwregens net aus der Këscht funktionnéiert). Vun der offizieller JanusGraph Dokumentatioun:

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

Afterword

An engem Sënn ass dat uewe genannte Experiment e Verglach tëscht waarm a mëll. Wann Dir driwwer denkt, mécht eng Grafik DBMS aner Operatiounen fir déiselwecht Resultater ze kréien. Wéi och ëmmer, als Deel vun den Tester hunn ech och en Experiment mat enger Ufro gemaach wéi:

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

wat zu Fouss Distanz reflektéiert. Awer och op esou Donnéeën huet d'Grafik DBMS Resultater gewisen, déi iwwer e puer Sekonnen erausgaange sinn ... Dëst ass natierlech wéinst der Tatsaach datt et Weeër wéi z. 0 -> X -> Y ... -> 1, déi de Grafikmotor och gepréift huet.

Och fir eng Ufro wéi:

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

Ech konnt net eng produktiv Äntwert kréien mat enger Veraarbechtungszäit vu manner wéi enger Sekonn.

D'Moral vun der Geschicht ass datt eng schéi Iddi a paradigmatesch Modellerung net zum gewënschte Resultat féieren, wat mat vill méi héijer Effizienz mam Beispill vum ClickHouse bewisen gëtt. De Benotzungsfall, deen an dësem Artikel presentéiert gëtt, ass e kloert Antimuster fir Grafik DBMSs, obwuel et gëeegent schéngt fir an hirem Paradigma ze modelléieren.

Source: will.com

Setzt e Commentaire