Eksperiment, ki preizkuša uporabnost JanusGraph graph DBMS za reševanje problema iskanja ustreznih poti

Eksperiment, ki preizkuša uporabnost JanusGraph graph DBMS za reševanje problema iskanja ustreznih poti

Pozdravljeni vsi skupaj. Razvijamo produkt za offline analizo prometa. Projekt ima nalogo, povezano s statistično analizo poti obiskovalcev po regijah.

Kot del te naloge lahko uporabniki postavljajo sistemske poizvedbe naslednje vrste:

  • koliko obiskovalcev je prešlo iz območja "A" v območje "B";
  • koliko obiskovalcev je šlo iz območja "A" v območje "B" skozi območje "C" in nato skozi območje "D";
  • koliko časa je določena vrsta obiskovalcev potrebovala, da je potovala od območja "A" do območja "B".

in številne podobne analitične poizvedbe.

Gibanje obiskovalca po področjih je usmerjen graf. Po branju interneta sem odkril, da se grafični DBMS uporabljajo tudi za analitična poročila. Imel sem željo videti, kako bodo grafični DBMS-ji kos takšnim poizvedbam (TL; DR; slabo).

Odločil sem se za uporabo DBMS JanusGraph, kot izjemen predstavnik graph open-source DBMS, ki sloni na kupu zrelih tehnologij, ki naj bi mu (po mojem mnenju) zagotovile spodobne operativne lastnosti:

  • Zaledje za shranjevanje BerkeleyDB, Apache Cassandra, Scylla;
  • kompleksne indekse je mogoče shraniti v Lucene, Elasticsearch, Solr.

Avtorji JanusGraph pišejo, da je primeren tako za OLTP kot OLAP.

Delal sem z BerkeleyDB, Apache Cassandra, Scylla in ES in ti izdelki se pogosto uporabljajo v naših sistemih, zato sem bil optimističen glede testiranja te grafične DBMS. Zdelo se mi je nenavadno, da sem izbral BerkeleyDB namesto RocksDB, vendar je to verjetno zaradi zahtev transakcije. V vsakem primeru je za razširljivo uporabo izdelka predlagana uporaba zaledja na Cassandri ali Scylli.

Neo4j nisem upošteval, ker grozdenje zahteva komercialno različico, torej izdelek ni odprtokoden.

Graf DBMS pravi: "Če je videti kot graf, ga obravnavajte kot graf!" - lepotica!

Najprej sem narisal graf, ki je bil narejen natančno po kanonih grafičnih DBMS:

Eksperiment, ki preizkuša uporabnost JanusGraph graph DBMS za reševanje problema iskanja ustreznih poti

Obstaja bistvo Zone, odgovoren za področje. če ZoneStep pripada temu Zone, potem se sklicuje na to. Na bistvu Area, ZoneTrack, Person Ne bodite pozorni, spadajo v domeno in se ne upoštevajo kot del testa. V celoti bi verižna iskalna poizvedba za takšno strukturo grafa izgledala takole:

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

V ruščini je nekaj takega: poiščite cono z ID=0, vzemite vsa oglišča, iz katerih gre rob do nje (Korak cone), stopite, ne da bi se vrnili nazaj, dokler ne najdete tistih korakov cone, od katerih je rob do cone z ID=19, preštejte število takih verig.

Ne pretvarjam se, da poznam vse zapletenosti iskanja po grafih, vendar je bila ta poizvedba ustvarjena na podlagi te knjige (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Naložil sem 50 tisoč sledi z dolžino od 3 do 20 točk v bazo podatkov grafov JanusGraph z uporabo zaledja BerkeleyDB, ustvaril indekse glede na vodstvo.

Skript za prenos 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)

Uporabili smo VM s 4 jedri in 16 GB RAM-a na SSD. JanusGraph je bil nameščen s tem ukazom:

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

V tem primeru so podatki in indeksi, ki se uporabljajo za iskanje natančnih ujemanj, shranjeni v BerkeleyDB. Ko sem izvršil prej dano zahtevo, sem prejel čas, ki je enak nekaj deset sekund.

Z vzporednim izvajanjem 4 zgornjih skriptov mi je uspelo DBMS spremeniti v bučo z veselim tokom sledi skladov Java (in vsi radi beremo sledi skladov Java) v dnevnikih Docker.

Po premisleku sem se odločil poenostaviti grafični diagram na naslednje:

Eksperiment, ki preizkuša uporabnost JanusGraph graph DBMS za reševanje problema iskanja ustreznih poti

Odločitev, da bo iskanje po atributih entitete hitrejše kot iskanje po robovih. Posledično se je moja zahteva spremenila v naslednje:

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

V ruščini je nekaj takega: poiščite ZoneStep z ID=0, stopite, ne da bi se vrnili nazaj, dokler ne najdete ZoneStep z ID=19, preštejte število takih verig.

Prav tako sem poenostavil zgornji skript za nalaganje, da ne bi ustvarjal nepotrebnih povezav in se omejil na atribute.

Zahteva je še vedno trajala nekaj sekund, kar je bilo za našo nalogo popolnoma nesprejemljivo, saj sploh ni bilo primerno za namene kakršnega koli AdHoc zahtevka.

Poskušal sem namestiti JanusGraph z uporabo Scylle kot najhitrejše implementacije Cassandra, vendar tudi to ni privedlo do bistvenih sprememb zmogljivosti.

Torej kljub dejstvu, da "je videti kot graf", nisem mogel pripraviti DBMS-ja za graf, da bi ga hitro obdelal. Popolnoma domnevam, da nekaj ne vem in da je JanusGraph mogoče narediti, da izvede to iskanje v delčku sekunde, vendar mi tega ni uspelo.

Ker je bilo problem še treba rešiti, sem začel razmišljati o JOIN-ih in Pivotih tabel, ki sicer niso vlivale optimizma v smislu elegance, a bi lahko bile v praksi povsem izvedljiva opcija.

Naš projekt že uporablja Apache ClickHouse, zato sem se odločil preizkusiti svoje raziskave na tej analitični DBMS.

Namestite ClickHouse po preprostem receptu:

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

Ustvaril sem bazo podatkov in tabelo v njej takole:

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

Napolnil sem ga s podatki z naslednjim skriptom:

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
    )

Ker so vložki v serijah, je bilo polnjenje veliko hitrejše kot pri JanusGraphu.

Izdelal dve poizvedbi z uporabo JOIN. Za premik od točke A do točke 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

Če želite iti skozi 3 točke:

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

Zahteve so seveda videti precej strašljive; za resnično uporabo morate ustvariti snop programskega generatorja. Vendar delujejo in delujejo hitro. Tako prva kot druga zahteva sta izpolnjeni v manj kot 0.1 sekunde. Tukaj je primer časa izvajanja poizvedbe za count(*), ki poteka skozi 3 točke:

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

Opomba o IOPS. Pri polnjenju podatkov je JanusGraph ustvaril dokaj visoko število IOPS (1000–1300 za štiri niti polnjenja podatkov), IOWAIT pa je bil precej visok. Hkrati je ClickHouse ustvaril minimalno obremenitev diskovnega podsistema.

Zaključek

Odločili smo se, da bomo za storitev te vrste zahtev uporabili ClickHouse. Vedno lahko dodatno optimiziramo poizvedbe z materializiranimi pogledi in paralelizacijo s predhodno obdelavo toka dogodkov z uporabo Apache Flink, preden jih naložimo v ClickHouse.

Zmogljivost je tako dobra, da nam najbrž sploh ne bo treba razmišljati o programskem vrtenju tabel. Prej smo morali izvajati pivote podatkov, pridobljenih iz Vertice prek nalaganja v Apache Parquet.

Na žalost je bil še en poskus uporabe grafične DBMS neuspešen. Nisem ugotovil, da ima JanusGraph prijazen ekosistem, ki bi omogočal preprosto seznanjanje z izdelkom. Hkrati se za konfiguracijo strežnika uporablja tradicionalni način Jave, zaradi katerega bodo ljudje, ki ne poznajo Jave, jokali do krvavih solz:

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}

Uspelo mi je pomotoma "vstaviti" BerkeleyDB različico JanusGraph.

Dokumentacija je glede indeksov precej ukrivljena, saj upravljanje indeksov zahteva, da v Groovyju izvedete nekaj precej čudnega šamanizma. Na primer, ustvarjanje indeksa je treba izvesti s pisanjem kode v konzoli Gremlin (ki, mimogrede, ne deluje takoj). Iz uradne dokumentacije 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()

spremna beseda

V nekem smislu je zgornji poskus primerjava med toplim in mehkim. Če pomislite, grafični DBMS izvaja druge operacije, da dobi enake rezultate. Vendar sem kot del testov izvedel tudi poskus z zahtevo, kot je:

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

ki odraža prehojeno razdaljo. Vendar je tudi na takih podatkih graf DBMS pokazal rezultate, ki so presegli nekaj sekund ... To je seveda posledica dejstva, da so obstajale poti, kot 0 -> X -> Y ... -> 1, kar je preveril tudi grafični mehanizem.

Tudi za poizvedbo, kot je:

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

Nisem mogel dobiti produktivnega odgovora s časom obdelave, krajšim od sekunde.

Morala zgodbe je, da lepa ideja in paradigmatično modeliranje ne pripeljeta do želenega rezultata, kar je prikazano z veliko večjo učinkovitostjo na primeru ClickHouse. Primer uporabe, predstavljen v tem članku, je jasen anti-vzorec za grafične DBMS, čeprav se zdi primeren za modeliranje v njihovi paradigmi.

Vir: www.habr.com

Dodaj komentar