Eksperimentas, kuriame tikrinamas JanusGraph grafiko DBVS pritaikomumas sprendžiant tinkamų kelių paieškos problemą

Eksperimentas, kuriame tikrinamas JanusGraph grafiko DBVS pritaikomumas sprendžiant tinkamų kelių paieškos problemą

Sveiki visi. Kuriame produktą neprisijungus srauto analizei. Projektas turi užduotį, susijusią su lankytojų maršrutų tarp regionų statistine analize.

Vykdydami šią užduotį vartotojai gali užduoti tokio tipo sistemos užklausas:

  • kiek lankytojų perėjo iš „A“ zonos į „B“ zoną;
  • kiek lankytojų perėjo iš „A“ į „B“ sritį per zoną „C“, o paskui per „D“ zoną;
  • kiek laiko prireikė tam tikro tipo lankytojui nukeliauti iš zonos „A“ į zoną „B“.

ir daug panašių analitinių užklausų.

Lankytojo judėjimas per sritis yra nukreiptas grafikas. Paskaitęs internetą atradau, kad grafinės DBVS naudojamos ir analitinėms ataskaitoms. Aš norėjau pamatyti, kaip grafų DBVS susidoros su tokiomis užklausomis (TL; DR; prastai).

Pasirinkau naudoti DBVS JanusGrafas, kaip išskirtinis grafinio atvirojo kodo DBVS atstovas, kuris remiasi daugybe brandžių technologijų, kurios (mano nuomone) turėtų suteikti jai tinkamas veikimo charakteristikas:

  • „BerkeleyDB“ saugykla, „Apache Cassandra“, „Scylla“;
  • sudėtingus indeksus galima saugoti Lucene, Elasticsearch, Solr.

JanusGraph autoriai rašo, kad jis tinka ir OLTP, ir OLAP.

Dirbau su BerkeleyDB, Apache Cassandra, Scylla ir ES, o šie produktai dažnai naudojami mūsų sistemose, todėl buvau nusiteikęs optimistiškai išbandyti šio grafiko DBVS. Man pasirodė keista pasirinkti BerkeleyDB, o ne RocksDB, bet tikriausiai taip yra dėl operacijos reikalavimų. Bet kokiu atveju, naudojant keičiamo dydžio produktą, Cassandra arba Scylla rekomenduojama naudoti backend.

Neo4j nesvarsčiau, nes grupavimui reikalinga komercinė versija, tai yra, produktas nėra atvirojo kodo.

Diagramos DBVS sako: „Jei jis atrodo kaip grafikas, traktuokite jį kaip grafiką! - grožis!

Pirmiausia nubraižiau grafiką, kuris buvo sudarytas tiksliai pagal grafų DBVS kanonus:

Eksperimentas, kuriame tikrinamas JanusGraph grafiko DBVS pritaikomumas sprendžiant tinkamų kelių paieškos problemą

Yra esmė Zone, atsakingas už sritį. Jeigu ZoneStep priklauso tai Zone, tada jis nurodo tai. Iš esmės Area, ZoneTrack, Person Nekreipkite dėmesio, jie priklauso domenui ir nėra laikomi testo dalimi. Iš viso tokios grafiko struktūros grandinės paieškos užklausa atrodytų taip:

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

Kas rusiškai yra maždaug taip: suraskite zoną su ID=0, paimkite visas viršūnes, iš kurių į ją eina briauna (ZoneStep), trypkite negrįždami, kol rasite tuos ZoneSteps, iš kurių yra kraštas į zoną su ID=19, suskaičiuokite tokių grandinių skaičių.

Neapsimetinėju, kad žinau visas paieškos pagal grafikus subtilybes, bet ši užklausa buvo sugeneruota remiantis šia knyga (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Įkėliau 50 tūkstančių takelių, kurių ilgis svyruoja nuo 3 iki 20 taškų, į JanusGraph grafikų duomenų bazę naudodamas BerkeleyDB užpakalinę programą, sukūriau indeksus pagal vadovavimas.

Python atsisiuntimo scenarijus:


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 diske naudojome VM su 4 branduoliais ir 16 GB RAM. JanusGraph buvo įdiegtas naudojant šią komandą:

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

Tokiu atveju duomenys ir indeksai, naudojami tikslios atitikties paieškoms, yra saugomi BerkeleyDB. Įvykdęs anksčiau pateiktą prašymą, gavau kelių dešimčių sekundžių laiką.

Lygiagrečiai paleisdamas 4 aukščiau nurodytus scenarijus, man pavyko paversti DBVS į moliūgą su linksmu Java stacktraces srautu (ir mes visi mėgstame skaityti Java stacktraces) Docker žurnaluose.

Šiek tiek pagalvojęs nusprendžiau supaprastinti grafiko diagramą taip:

Eksperimentas, kuriame tikrinamas JanusGraph grafiko DBVS pritaikomumas sprendžiant tinkamų kelių paieškos problemą

Nusprendus, kad paieška pagal objekto atributus būtų greitesnė nei paieška pagal kraštus. Dėl to mano prašymas virto taip:

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

Kas rusiškai yra maždaug taip: surask ZoneStep su ID=0, trypk negrįždamas, kol rasi ZoneStep su ID=19, suskaičiuok tokių grandinių skaičių.

Taip pat supaprastinau aukščiau pateiktą įkėlimo scenarijų, kad nesukurčiau nereikalingų ryšių, apsiribodamas atributais.

Užklausa vis tiek užtruko kelias sekundes, o tai buvo visiškai nepriimtina mūsų užduočiai, nes ji visiškai netiko bet kokio pobūdžio AdHoc užklausoms.

Bandžiau įdiegti „JanusGraph“, naudodamas „Scylla“ kaip greičiausią „Cassandra“ diegimą, tačiau tai taip pat nepakeitė jokių reikšmingų našumo pokyčių.

Taigi, nepaisant to, kad „tai atrodo kaip grafikas“, aš negalėjau priversti grafiko DBVS greitai jo apdoroti. Visiškai manau, kad kažko nežinau ir kad JanusGraph gali būti priversti atlikti šią paiešką per sekundės dalį, tačiau man nepavyko to padaryti.

Kadangi problemą dar reikėjo išspręsti, pradėjau galvoti apie lentelių JOIN ir Pivots, kurie elegancijos atžvilgiu optimizmo nesukėlė, tačiau praktiškai galėtų būti visiškai tinkamas variantas.

Mūsų projektas jau naudoja „Apache ClickHouse“, todėl nusprendžiau išbandyti savo tyrimą apie šią analitinę DBVS.

„ClickHouse“ įdiegta naudojant paprastą receptą:

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

Sukūriau tokią duomenų bazę ir lentelę joje:

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

Aš užpildžiau jį duomenimis naudodamas šį scenarijų:

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
    )

Kadangi įdėklai pateikiami partijomis, užpildymas buvo daug greitesnis nei JanusGraph.

Sukūrė dvi užklausas naudojant JOIN. Norėdami pereiti iš taško A į tašką 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

Norėdami pereiti 3 taškus:

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

Žinoma, užklausos atrodo gana bauginančios; realiam naudojimui reikia sukurti programinės įrangos generatoriaus laidą. Tačiau jie veikia ir dirba greitai. Tiek pirmoji, tiek antroji užklausa įvykdoma greičiau nei per 0.1 sekundės. Čia yra užklausos vykdymo laiko, kai skaičius (*), eina per 3 taškus, pavyzdys:

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

Pastaba apie IOPS. Pildydamas duomenis, JanusGraph sugeneravo gana didelį IOPS skaičių (1000–1300 keturioms duomenų populiacijos gijomis), o IOWAIT buvo gana didelis. Tuo pačiu metu ClickHouse sukūrė minimalią disko posistemio apkrovą.

išvada

Tokio tipo užklausoms aptarnauti nusprendėme naudoti ClickHouse. Visada galime toliau optimizuoti užklausas naudodami materializuotus rodinius ir lygiagretavimą, iš anksto apdorodami įvykių srautą naudodami „Apache Flink“, prieš įkeldami jas į „ClickHouse“.

Našumas toks geras, kad turbūt net nereikės galvoti apie sukamąsias lenteles programiškai. Anksčiau turėjome atlikti duomenų, gautų iš „Vertica“, pasukimą įkeliant juos į „Apache Parquet“.

Deja, dar vienas bandymas naudoti grafinę DBVS buvo nesėkmingas. Nemanau, kad JanusGraph turi draugišką ekosistemą, kuri leistų lengvai įsibėgėti su produktu. Tuo pačiu metu serverio konfigūravimui naudojamas tradicinis „Java“ būdas, kuris privers žmones, kurie nėra susipažinę su Java, verkti kraujo ašaromis:

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}

Man pavyko netyčia „įdėti“ JanusGraph BerkeleyDB versiją.

Dokumentacija yra gana kreiva indeksų atžvilgiu, nes norint valdyti indeksus, Groovy programoje reikia atlikti gana keistą šamanizmą. Pavyzdžiui, indekso kūrimas turi būti atliktas įrašant kodą Gremlin konsolėje (kuris, beje, neveikia iš karto). Iš oficialios JanusGraph dokumentacijos:

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

Tam tikra prasme aukščiau pateiktas eksperimentas yra šilto ir minkšto palyginimas. Jei galvojate apie tai, grafikas DBVS atlieka kitas operacijas, kad gautų tuos pačius rezultatus. Tačiau atlikdamas testus taip pat atlikau eksperimentą su tokia užklausa:

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

kuris atspindi ėjimo atstumą. Tačiau net ir ant tokių duomenų diagrama DBVS rodė rezultatus, kurie viršijo kelias sekundes... Taip, žinoma, dėl to, kad buvo keli kaip 0 -> X -> Y ... -> 1, kurį patikrino ir grafiko variklis.

Net ir tokiai užklausai kaip:

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

Man nepavyko gauti produktyvaus atsakymo, kai apdorojimo laikas buvo trumpesnis nei sekundė.

Istorijos moralas yra tas, kad graži idėja ir paradigminis modeliavimas nepriveda prie norimo rezultato, o tai daug didesniu efektyvumu demonstruojama naudojant ClickHouse pavyzdį. Šiame straipsnyje pateiktas naudojimo atvejis yra aiškus grafinių DBVS, nors ir atrodo tinkamas modeliuoti jų paradigmoje, modelis.

Šaltinis: www.habr.com

Добавить комментарий