'n Eksperiment wat die toepaslikheid van die JanusGraph grafiek DBBS toets vir die oplossing van die probleem om geskikte paaie te vind

'n Eksperiment wat die toepaslikheid van die JanusGraph grafiek DBBS toets vir die oplossing van die probleem om geskikte paaie te vind

Hi almal. Ons ontwikkel 'n produk vir vanlyn verkeersanalise. Die projek het 'n taak wat verband hou met statistiese ontleding van besoekersroetes oor streke heen.

As deel van hierdie taak kan gebruikers die stelselnavrae van die volgende tipe vra:

  • hoeveel besoekers van area "A" na area "B" oorgegaan het;
  • hoeveel besoekers van area "A" na area "B" deur area "C" en dan deur area "D" gegaan het;
  • hoe lank dit geneem het vir 'n sekere tipe besoeker om van area "A" na area "B" te reis.

en 'n aantal soortgelyke analitiese navrae.

Die besoeker se beweging oor gebiede is 'n gerigte grafiek. Nadat ek die internet gelees het, het ek ontdek dat grafiek-DBBS'e ook vir analitiese verslae gebruik word. Ek het 'n begeerte gehad om te sien hoe grafiek-DBBS'e sulke navrae sou hanteer (TL; DR; swak).

Ek het gekies om die DBMS te gebruik JanusGrafiek, as 'n uitstaande verteenwoordiger van grafiek oopbron DBMS, wat staatmaak op 'n stapel volwasse tegnologieΓ«, wat (na my mening) dit moet voorsien van ordentlike operasionele eienskappe:

  • BerkeleyDB berging backend, Apache Cassandra, Scylla;
  • komplekse indekse kan gestoor word in Lucene, Elasticsearch, Solr.

Die skrywers van JanusGraph skryf dat dit geskik is vir beide OLTP en OLAP.

Ek het met BerkeleyDB, Apache Cassandra, Scylla en ES gewerk, en hierdie produkte word dikwels in ons stelsels gebruik, so ek was optimisties oor die toets van hierdie grafiek-DBMS. Ek het dit vreemd gevind om BerkeleyDB bo RocksDB te kies, maar dit is waarskynlik as gevolg van die transaksievereistes. In elk geval, vir skaalbare produkgebruik, word voorgestel om 'n backend op Cassandra of Scylla te gebruik.

Ek het nie Neo4j oorweeg nie omdat groepering 'n kommersiΓ«le weergawe vereis, dit wil sΓͺ die produk is nie oopbron nie.

Grafiek-DBBS'e sΓͺ: "As dit soos 'n grafiek lyk, behandel dit soos 'n grafiek!" - skoonheid!

Eerstens het ek 'n grafiek geteken, wat presies volgens die kanons van grafiek-DBBS'e gemaak is:

'n Eksperiment wat die toepaslikheid van die JanusGraph grafiek DBBS toets vir die oplossing van die probleem om geskikte paaie te vind

Daar is 'n essensie Zone, verantwoordelik vir die gebied. As ZoneStep hiertoe behoort Zone, dan verwys hy daarna. Op essensie Area, ZoneTrack, Person Moenie aandag gee nie, hulle behoort aan die domein en word nie as deel van die toets beskou nie. In totaal sal 'n kettingsoektog vir so 'n grafiekstruktuur soos volg lyk:

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

Wat in Russies is so iets: vind 'n Sone met ID=0, neem al die hoekpunte waarvandaan 'n rand daarheen gaan (ZoneStep), trap sonder om terug te gaan totdat jy daardie ZoneSteps kry waarvandaan daar 'n rand na die Sone is met ID=19, tel die aantal sulke kettings.

Ek gee nie voor dat ek al die ingewikkeldhede van soek op grafieke ken nie, maar hierdie navraag is gegenereer op grond van hierdie boek (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Ek het 50 duisend snitte wat wissel van 3 tot 20 punte lank in 'n JanusGraph grafiek databasis gelaai deur die BerkeleyDB backend te gebruik, indekse geskep volgens leierskap.

Python aflaai script:


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)

Ons het 'n VM met 4 kerns en 16 GB RAM op 'n SSD gebruik. JanusGraph is ontplooi met behulp van hierdie opdrag:

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

In hierdie geval word die data en indekse wat vir presiese pasmaatsoektogte gebruik word, in BerkeleyDB gestoor. Nadat ek die versoek wat vroeΓ«r gegee is uitgevoer het, het ek 'n tyd gelyk aan 'n paar tientalle sekondes ontvang.

Deur die 4 bogenoemde skrifte parallel te laat loop, het ek daarin geslaag om die DBMS in 'n pampoen te verander met 'n vrolike stroom Java-stacktraces (en ons hou almal daarvan om Java-stacktraces te lees) in die Docker-logboeke.

Na 'n bietjie nagedink het ek besluit om die grafiekdiagram na die volgende te vereenvoudig:

'n Eksperiment wat die toepaslikheid van die JanusGraph grafiek DBBS toets vir die oplossing van die probleem om geskikte paaie te vind

Om te besluit dat soek volgens entiteiteienskappe vinniger sou wees as om volgens rande te soek. Gevolglik het my versoek in die volgende verander:

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

Wat in Russies so iets is: vind ZoneStep met ID=0, trap sonder om terug te gaan totdat jy ZoneStep met ID=19 kry, tel die aantal sulke kettings.

Ek het ook die laaiskrif wat hierbo gegee is vereenvoudig om nie onnodige verbindings te skep nie, en myself beperk tot eienskappe.

Die versoek het nog etlike sekondes geneem om te voltooi, wat heeltemal onaanvaarbaar was vir ons taak, aangesien dit glad nie geskik was vir die doeleindes van AdHoc-versoeke van enige aard nie.

Ek het probeer om JanusGraph te ontplooi deur Scylla as die vinnigste Cassandra-implementering te gebruik, maar dit het ook nie tot beduidende prestasieveranderings gelei nie.

So ten spyte van die feit dat "dit lyk soos 'n grafiek", kon ek nie die grafiek DBMS kry om dit vinnig te verwerk nie. Ek neem ten volle aan dat daar iets is wat ek nie weet nie en dat JanusGraph hierdie soektog binne 'n breukdeel van 'n sekonde kan doen, maar ek kon dit nie doen nie.

Aangesien die probleem nog opgelos moes word, het ek begin dink aan JOINs en Pivots van tabelle, wat nie optimisme in terme van elegansie aangewakker het nie, maar 'n heeltemal werkbare opsie in die praktyk kon wees.

Ons projek gebruik reeds Apache ClickHouse, so ek het besluit om my navorsing oor hierdie analitiese DBBS te toets.

Ontplooi ClickHouse met behulp van 'n eenvoudige resep:

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

Ek het 'n databasis en 'n tabel daarin geskep soos volg:

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

Ek het dit met data gevul deur die volgende skrif te gebruik:

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
    )

Aangesien insetsels in bondels kom, was vul baie vinniger as vir JanusGraph.

Twee navrae opgestel deur JOIN te gebruik. Om van punt A na punt B te beweeg:

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

Om deur 3 punte te gaan:

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

Die versoeke lyk natuurlik nogal skrikwekkend; vir werklike gebruik moet jy 'n sagteware-opwekker harnas skep. Hulle werk egter en hulle werk vinnig. Beide die eerste en tweede versoeke word in minder as 0.1 sekondes voltooi. Hier is 'n voorbeeld van die navraaguitvoeringstyd vir telling(*) wat deur 3 punte gaan:

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

'n Nota oor IOPS. By die invul van data het JanusGraph 'n redelik hoΓ« aantal IOPS (1000-1300 vir vier datapopulasie-drade) gegenereer en IOWAIT was redelik hoog. Terselfdertyd het ClickHouse minimale las op die skyfsubstelsel gegenereer.

Gevolgtrekking

Ons het besluit om ClickHouse te gebruik om hierdie soort versoeke te bedien. Ons kan navrae altyd verder optimaliseer deur gematerialiseerde aansigte en parallellisering te gebruik deur die gebeurtenisstroom vooraf te verwerk met Apache Flink voordat dit in ClickHouse gelaai word.

Die prestasie is so goed dat ons waarskynlik nie eers daaraan hoef te dink om tabelle programmaties te draai nie. Voorheen moes ons spilpunte doen van data wat van Vertica verkry is deur oplaai na Apache Parquet.

Ongelukkig was nog 'n poging om 'n grafiek-DBMS te gebruik onsuksesvol. Ek het nie gevind dat JanusGraph 'n vriendelike ekosisteem het wat dit maklik gemaak het om op hoogte te kom met die produk nie. Terselfdertyd, om die bediener op te stel, word die tradisionele Java-manier gebruik, wat mense wat nie met Java vertroud is nie, sal laat huil met trane van bloed:

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}

Ek het daarin geslaag om per ongeluk die BerkeleyDB-weergawe van JanusGraph te "sit".

Die dokumentasie is nogal krom in terme van indekse, aangesien die bestuur van indekse vereis dat jy nogal vreemde sjamanisme in Groovy uitvoer. Byvoorbeeld, die skep van 'n indeks moet gedoen word deur kode in die Gremlin-konsole te skryf (wat, terloops, nie uit die boks werk nie). Van die amptelike JanusGraph-dokumentasie:

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

nawoord

In 'n sekere sin is die bogenoemde eksperiment 'n vergelyking tussen warm en sag. As jy daaraan dink, voer 'n grafiek-DBMS ander bewerkings uit om dieselfde resultate te verkry. As deel van die toetse het ek egter ook 'n eksperiment uitgevoer met 'n versoek soos:

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

wat stapafstand weerspieΓ«l. Selfs op sulke data het die grafiek DBMS egter resultate getoon wat meer as 'n paar sekondes gegaan het... Dit is natuurlik te wyte aan die feit dat daar paaie soos 0 -> X -> Y ... -> 1, wat die grafiek-enjin ook nagegaan het.

Selfs vir 'n navraag soos:

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

Ek kon nie 'n produktiewe reaksie kry met 'n verwerkingstyd van minder as 'n sekonde nie.

Die moraal van die storie is dat 'n pragtige idee en paradigmatiese modellering nie tot die gewenste resultaat lei nie, wat met veel hoΓ«r doeltreffendheid gedemonstreer word deur die voorbeeld van ClickHouse te gebruik. Die gebruiksgeval wat in hierdie artikel aangebied word, is 'n duidelike anti-patroon vir grafiek-DBBS'e, alhoewel dit geskik lyk vir modellering in hul paradigma.

Bron: will.com

Voeg 'n opmerking