JanusGraph graafikandmeid töötleva süsteemi rakendatavuse eksperiment sobivate teede otsimise ülesande täitmiseks

JanusGraph graafikandmeid töötleva süsteemi rakendatavuse eksperiment sobivate teede otsimise ülesande täitmiseks

Tere kõigile. Arendame toodet offline-liiklusanalüüsiks. Projekti raames on ülesanne, mis on seotud külastajate liikumisteede statistilise analüüsiga.

Selle ülesande alla kuulub, et kasutajad saavad süsteemile esitada järgmisi päringute tüüpe:

  • kui palju külastajaid liikus piirkonnast "A" piirkonda "B";
  • kui palju külastajaid liikus piirkonnast "A" piirkonda "B" läbi piirkonna "C", seejärel läbi piirkonna "D";
  • kui kaua võttis teatud tüüpi külastaja liikumine piirkonnast "A" piirkonda "B".

ja veel mitmeid sarnaseid analüütilisi päringuid.

Külastaja liikumine piirkondade vahel kujutab endast suunatud graafi. Uurides internetti, avastasin, et graafikandmeid töötavad süsteemid on samuti kasutusel analüüsiraportite koostamiseks. Olin huvitatud, kuidas graafikandmeid töötlevad süsteemid selliste päringutega toimetavad (TL;DR; halb).

Valisin kasutamiseks andmebaasi JanusGraph, kui silmapaistva esindaja graafikute open-source andmebaasi, mis tugineb küpsete tehnoloogiate virnale, mis (minu arvates) peaks pakkuma talle korralikke tegevusnäitajaid:

  • BerkeleyDB, Apache Cassandra, Scylla salvestamise tagumine osa;
  • keerulisi indekseid saab hoida Lucenes, Elasticsearchis, Solris.

JanusGraphi autorid kirjutavad, et see sobib nii OLTP kui ka OLAP jaoks.

Olen töötanud BerkeleyDB, Apache Cassandra, Scylla ja ES-ga, lisaks on need tooted sageli meie süsteemides kasutusel, seega vaatasin selle graafikute andmebaasi testimist optimistlikult. BerkeleyDB valik tundus mulle veider, mitte RocksDB, kuid tõenäoliselt on see seotud tehingu nõuetega. Igatahes, skaleeritava, toote kasutamise jaoks soovitatakse kasutada Cassandra või Scylla tagumist osa.

Neo4j ei olnud minu kaalutluses, kuna klasterdamiseks on vajalik äriversioon, see tähendab, et toode ei ole avatud.

Graafikute andmebaasid ütlevad: "Kui midagi näeb välja nagu graaf — töötlege seda kui graafi!" — ilu!

Esmalt joonistasin graafi, mis on täpselt tehtud graafikute andmebaasi kanonite järgi:

JanusGraph graafikandmeid töötleva süsteemi rakendatavuse eksperiment sobivate teede otsimise ülesande täitmiseks

On olemas üksus Zone, mis vastutab ala eest. Kui ZoneStep kuulub sellele Zone, siis viitab ta sellele. Subjekti kohta Area, ZoneTrack, Isik ära pöörake tähelepanu, need kuuluvad domeenile ja testimise kontekstis ei arvestata. Seega näeks selline graafiline struktuur välja nagu:

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

Mida see eesti keeles tähendab: leia Zone ID-ga 0, võta kõik tipud, millelt sinna viib haru (ZoneStep), mine edasi ilma tagasi pöördumata kuni leiad sellised ZoneStep’id, millelt viib haru Zone’ni ID-ga 19, loe selliste kettide arv.

Ma ei pretendeeri, et tean kõiki graafide otsingu nüansse, kuid see päring genereeriti selle raamatu põhjal (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Laadisin JanusGraphi graafitüüpi andmebaasi 50 000 rada pikkusega 3 kuni 20 punkti, kasutades BerkeleyDB tagapinda, seadistasin indeksid vastavalt juhendile.

Python'i laadimisskript:


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)

Kasutati 4 tuumaga VM-i ja 16 GB RAM SSD-l. JanusGraph käivitati järgmise käsu abil:

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

Selles olukorras hoitakse andmeid ja indeksid, mida kasutatakse täpse vaste otsimiseks, BerkeleyDB-s. Käivitades eelnevalt toodud päringu, sain aja, mis oli mõne kümne sekundi vältel.

Käivitades 4 ülaltoodud skripti paralleelselt, õnnestus mul andmebaas muuta kõrvitsaks, millega kaasnes rõõmus Java stekti jälgede voog (ja me kõik armastame lugeda Java stekti jälgi) Docker logides.

Mõtlema hakates otsustasin grafi skeemi lihtsustada järgmisele:

JanusGraph graafikandmeid töötleva süsteemi rakendatavuse eksperiment sobivate teede otsimise ülesande täitmiseks

Otsustasin, et objekti atribuutidest otsimine on kiirem kui servade kaudu otsimine. Lõpuks muutus mu päring järgmiseks:

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

Mis vene keeles tähendab umbkaudu: leia ZoneStep ID-ga 0, liigu tagasi pöördumata kuni leiad ZoneStep ID-ga 19, loe selliste ahelate arv.

Ülaltoodud laadimisskripti lihtsustasin samuti, et mitte luua liigseid seoseid, piirdudes ainult atribuutidega.

Päring töötas siiski mitu sekundit, mis oli meie ülesande jaoks täiesti vastuvõetamatu, kuna AdHoc päringute eesmärkide jaoks ei sobinud see absoluutselt.

Katsusin käivitada JanusGraph'i, kasutades Scyllat, mis on Cassandra kiireim teostus, kuid see ei toonud ka mingisuguseid olulisemaid jõudluse muutusi.

Seega, vaatamata sellele, et "see näeb välja nagu graf", ei suutnud ma grafitüüpi andmebaasi seda kiiresti tööle panna. Eeldan, et ma ei tea midagi olulist ja oleks võimalik sundida JanusGraph'i seda otsingut täitma sekundite murdosa jooksul, kuid mina seda ei suutnud.

Kuna ülesanne pidi ikkagi lahendatud saama, hakkasin mõtlema JOIN-idele ja Pivot-tabelitele, mis ei pakkunud palju lootust elegantsuse osas, kuid võis olla täiesti toimiv lahendus praktikas.

Meie projektis kasutatakse juba Apache ClickHouse'i, seega otsustasin oma uuringud selle analüütilise andmebaasi juhtimiseks testida.

Käivitasin ClickHouse'i lihtsa retsepti alusel:

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

Loomisin seal andmebaasi ja tabeli vormis:

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

Täitsin selle andmetega järgmise skripti abil:

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
    )

Kuna sisestamised toimuvad grupiviisiliselt, toimus täitmine palju kiiremini kui JanusGraphi puhul.

Tõin kaks päringut JOIN abil. Punktist A punkti B liikumiseks:

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

Kolme punkti kaudu liikumiseks:

VALI s3.person,
       s1z,
       s1w,
       s2z,
       s2w,
       s3.zone,
       s3.when
FROM
  (VALI s1.person AS person,
          s1.zone AS s1z,
          s1.when AS s1w,
          s2.zone AS s2z,
          s2.when AS s2w
   FROM
     (VALI *
      FROM steps
      WHERE (area = 0)
        AND (zone = 0)) AS s1 KAS INNER JOIN
     (VALI *
      FROM steps AS s2
      WHERE (area = 0)
        AND (zone = 3)) AS s2 USING person
   WHERE s1.when <= s2.when) p KAS INNER JOIN
  (VALI *
   FROM steps
   WHERE (area = 0)
     AND (zone = 19)) AS s3 USING person
WHERE p.s2w <= s3.when

Küsimused näevad tõepoolest üsna hirmutavad välja; reaalsete rakenduste jaoks peab olema programmiline sidumine-generaator. Siiski, need töötavad ja teevad seda kiiresti. Nii esimene kui ka teine küsimus täidetakse vähem kui 0,1 sekundi jooksul. Siin on näide päringu täitmise ajast count(*) läbimise jaoks 3 punkti kaudu:

VALI count(*)
FROM 
(
    VALI 
        s1.person AS person, 
        s1.zone AS s1z, 
        s1.when AS s1w, 
        s2.zone AS s2z, 
        s2.when AS s2w
    FROM 
    (
        VALI *
        FROM steps
        WHERE (area = 0) AND (zone = 0)
    ) AS s1
    KAS INNER JOIN 
    (
        VALI *
        FROM steps AS s2
        WHERE (area = 0) AND (zone = 3)
    ) AS s2 USING (person)
    WHERE s1.when <= s2.when
) AS p
KAS INNER JOIN 
(
    VALI *
    FROM steps
    WHERE (area = 0) AND (zone = 19)
) AS s3 USING (person)
WHERE p.s2w <= s3.when

┌─count()─┐
│   11592 │
└─────────┘

1 rida komplektis. Aeg: 0.068 sek. Töödeldud 250,03 tuhat rida, 8,00 MB (3,69 miljonit rida/s, 117,98 MB/s.)

Märkus IOPS kohta. Andmete täitmisel genereeris JanusGraph üsna kõrge IOPS (1000-1300 nelja andmete täitmise voolu jaoks) ning IOWAIT oli samuti üsna kõrge. Samal ajal genereeris ClickHouse minimaalset koormust ketasüsteemile.

Kokkuvõte

Otsustasime kasutada ClickHouse'i selliste päringute teenindamiseks. Saame alati veelgi optimeerida päringuid, kasutades materialiseeritud vaateid ja parallelismi, töötades ürituste voogu ette Apache Flinkiga enne nende laadimist ClickHouse'i.

Jõudlus on nii hea, et me ei pea tõenäoliselt isegi mõtlema tabeli pööramisele programmide abil. Varem pidime andmeid, mis saadi Verticast, pöörama Apache Parquet'i väljundiga.

Kahjuks ei krooninud veel üks katse kasutada graafipõhist andmebaasi edu. Ma ei leidnud, et JanusGraph omaks sõbralikku ökosüsteemi, mis võimaldaks toote kiirelt selgeks saada. Lisaks kasutab serveri konfigureerimine traditsioonilist Java-lähenemist, mis paneb Java-taustaga inimesi nutma:

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 заменяет Gryo и 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 и Graphson, последние версии
  - { 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] }}
  # Старые версии сериализации для обратной совместимости:
  - { 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}

Mul juhtus kogemata "panema" BerkeleyDB versiooni JanusGraph.

Dokumentatsioon on indekseerimise osas üsna ebaselge, kuna indeksite haldamiseks tuleb Groovy's teha üsna kummalisi toiminguid. Näiteks peab indeksi loomine toimuma Gremlin konsolis koodi kirjutamise teel (mis, muide, ei tööta kohe välja pakendis). JanusGraph ametlikust dokumentatsioonist:

graph.tx().rollback() \/\/ Ära loo uusi indeksit, kui tehing on aktiivne
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()

\// Oota, kuni indeks on saadaval
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
\// Indekseeri olemasolevad andmed uuesti
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()

Järelsõna

Teatud mõttes on eelpooltoodud eksperiment sarnane sooja ja pehme võrdlemisega. Kui süveneda, siis graafiline andmebaas sooritab samu tulemusi saavutamiseks erinevaid operatsioone. Siiski, katsete raames tegin ka eksperimenti, kus päring oli selline:

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

mis, mis reflekteerib sammu kättesaadavust. Kuid isegi nende andmete puhul näitas graafiku andmebaas tulemust, mis ületas mõne sekundi piire… See on kindlasti seotud selliste teedega, 0 -> X -> Y ... -> 1, mida ka graafik mootor kontrollis.

Isegi sellise päringu puhul:

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

ei suutnud ma saada tulemuslikku vastust töötlemise ajaga vähem kui sekund.

Moraal on see, et ilus idee ja paradigmaatiline modelleerimine ei viitsi soovitud tulemuseni, mida palju efektiivsemalt demonstreeritakse ClickHouse'i näitel. Käesolevas artiklis toodud kasutusjuht on selge antipattern graafika andmebaasidele, kuigi see näeb välja sobiv nende paradigma modelleerimiseks.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster