Eksperiment graafibaseeritud andmebaasi JanusGraph rakendatavuse kontrollimiseks sobivate teede leidmisel

Eksperiment graafibaseeritud andmebaasi JanusGraph rakendatavuse kontrollimiseks sobivate teede leidmisel

Tere kõigile. Me arendame toodet offline-liikluse analüüsimiseks. Projekti raames on ülesanne, mis on seotud külastajate liikumisteede statistilise analüüsiga.

Selle ülesande raames saavad kasutajad esitada süsteemile järgmise tüüpi päringuid:

  • kui palju külastajaid liikus alalt "A" alale "B";
  • kui palju külastajaid liikus alalt "A" alale "B" üle ala "C" ja seejärel ala "D";
  • kui kaua kulus teatud tüüpi külastaja liikumiseks alalt "A" alale "B."

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

Külastaja liikumine alade vahel esindab suunatud graafi. Internetis lugedes avastasin, et graafikandjad on kasutusel ka analüütilistes aruannetes. Mul tekkis huvi vaadata, kuidas selliste päringutega toime tulevad graafikandjad (TL;DR; halvasti).

Valisin kasutamiseks andmebaasi JanusGraph, kui silmapaistva esindaja graafilistest avatud lähtekoodiga andmebaasidest, mis toetuvad küpsete tehnoloogiate kogumile, mis (minu arvates) peaks tagama korralikud operatiivsed omadused:

  • salvestustase BerkeleyDB, Apache Cassandra, Scylla;
  • komplekse indekseerimist saab salvestada Lucenes, Elasticsearchis, Solris.

JanusGraph'i 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, seetõttu vaatasin graafikandjat testimisele optimistlikult. Mulle tundus kummaline valida BerkeleyDB, mitte RocksDB, kuid tõenäoliselt on see seotud nõudmistega tehingute osas. Igatahes, skaleeritava, tootmis kasutuse jaoks soovitatakse kasutada Cassandrat või Scyllat taustana.

Neo4j ei olnud ma vaadanud, kuna klastri jaoks on vajalik kommertsversioon, seega pole toode avatud.

Graafikandjad ütlevad: "Kui midagi näeb välja nagu graaf, töödelge seda nagu graafi!" — ilus!

Esmalt joonistasin graafi, mis on tõeliselt graafikandjate kanonite järgi loodud:

Eksperiment graafibaseeritud andmebaasi JanusGraph rakendatavuse kontrollimiseks sobivate teede leidmisel

On olemas entiteet Zone, mis vastutab ala eest. Kui ZoneStep kuulub sellele, siis viitab see sellele. Entiteedid ZoneArea ZoneTrack, Person, ei pane tähele, need kuuluvad domeeni ja ei ole testi raames käsitletud. Seega, sellise graafilise struktuuri puhul näeks otsinguahelate päring välja nagu: внимание не обращайте, они принадлежат домену и в рамках теста не рассматриваются. Итого, к такой графовой структуре запрос поиска цепочек выглядел бы как:

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

See eesti keeles: leia Zone id-ga 0, võta kõik tipud, millel on serv (ZoneStep), kulge tagasi pöördumata, kuni leiad sellised ZoneStep'id, millel on serv Zone'iga id-ga 19, loe selliste ahelate arv.

Ma ei pretendeeri graafide otsimise kõigi nüansside teadmisele, kuid see päring genereeriti selle raamatu põhjal (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Olen laadinud 50 000 lugu, mille pikkus on 3 kuni 20 punkti, graafandmebaasi JanusGraph, mis kasutab BerkeleyDB tagapinda, ja loonud indekseid vastavalt juhendile.

Laadimiscript Pythonis:


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 VM-i, millel on 4 tuuma ja 16 GB RAM SSD-l. JanusGraph pandi üles järgmise käsu abil:

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

Sellisel juhul hoitakse andmeid ja indekseid, mida kasutatakse täpse vaste otsimiseks, BerkeleyDB-s. Tehes eelnevalt toodu päringu, sain tulemuseks mitukümmend sekundit.

Käivitasin 4 eespool mainitud skripti paralleelselt, suutsin muuta andmebaasi kõrvitsaks koos rõõmsa Java stekki jäljendiga (ja me kõik armastame lugeda Java stekki jälgi) Dockeri logides.

Mõeldes sellele, otsustasin lihtsustada graafi skeemi järgmiseks:

Eksperiment graafibaseeritud andmebaasi JanusGraph rakendatavuse kontrollimiseks sobivate teede leidmisel

Otsustades, et objekti atribuutide järgi otsimine on kiirem kui servade järgi. Lõpuks mu päring muutus järgmiseks:

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

See eesti keeles: leia ZoneStep id-ga 0, kulge tagasi pöördumata, kuni leiate ZoneStep id-ga 19, loe selliste ahelate arv.

Eespool toodud laadimiscripti olen samuti lihtsustanud, et mitte luua tarbetuid seoseid, piirdudes ainult atributidega.

Küsimus kestis siiski mitu sekundit, mis meie ülesande jaoks oli täiesti vastuvõetamatu, kuna ad-hoc küsimustega ei sobinud see üldse.

Proovisin JanusGraphi käivitada Scylla abil, mis on Cassandra kõige kiirem rakendus, kuid see ei andnud mingit olulist tulemuslikkuse tõusu.

Seega, hoolimata sellest, et "see näeb välja nagu graaf", ei suutnud ma graafipõhist andmebaasi selle kiiresti töötama panna. Oletan, et ma ei tea midagi ja JanusGraphi saab selle otsinguga töötama panna sekundite jooksul, kuid mul see ei õnnestunud.

Kuna ülesanne pidi olema lahendatud, hakkasin mõtlema JOIN-idele ja Pivot-tabelitele, mis ei pakkunud palju optimismi stiili osas, kuid võisid praktikas olla üsna toimivad.

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

Käivitamine ClickHouse'i lihtsa retsepti järgi:

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

Loomiskäigus andmebaas ja tabel järgmise struktuuriga:

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 skriptiga:

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 sisestused toimuvad partiidena, oli täitmine palju kiirem kui JanusGraphiga.

Konstrueerisin kaks päringut JOIN-ide abil. A-st B-sse 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.inimene,
       s1z,
       s1w,
       s2z,
       s2w,
       s3.tsooni,
       s3.kui
FROM
  (VALI s1.inimene KUI nimed,
          s1.tsoon AS s1z,
          s1.kui AS s1w,
          s2.tsoon AS s2z,
          s2.kui AS s2w
   FROM
     (VALI *
      FROM sammud
      WHERE (ala = 0)
        AND (tsoon = 0)) NIMEDEGA s1 KUID INNER JOIN
     (VALI *
      FROM sammud NIMEDEGA s2
      WHERE (ala = 0)
        AND (tsoon = 3)) NIMEDEGA s2 KASUTADES inimene
   KUS s1.kui <= s2.kui) p KUID INNER JOIN
  (VALI *
   FROM sammud
   WHERE (ala = 0)
     AND (tsoon = 19)) NIMEDEGA s3 KASUTADES inimene
KUS p.s2w <= s3.kui

Küsitlused näivad muidugi üsna hirmutavad, kuid tegelikuks kasutuseks on vajalik tuua programmiline raamistiku generaator. Siiski, need töötavad ja töötavad kiiresti. Nii esimene kui ka teine päring täidetakse vähem kui 0,1 sekundiga. Siin on näide päringu täitmise ajast count(*) läbimise jaoks 3 punkti kaudu:

VALI count(*)
FROM 
(
    VALI 
        s1.inimene AS inimene, 
        s1.tsoon AS s1z, 
        s1.kui AS s1w, 
        s2.tsoon AS s2z, 
        s2.kui AS s2w
    FROM 
    (
        VALI *
        FROM sammud
        WHERE (ala = 0) JA (tsoon = 0)
    ) NIMEDEGA s1
    KUID INNER JOIN 
    (
        VALI *
        FROM sammud NIMEDEGA s2
        WHERE (ala = 0) JA (tsoon = 3)
    ) NIMEDEGA s2 KASUTADES (inimene)
    KUS s1.kui <= s2.kui
) NIMEDEGA p
KUID INNER JOIN 
(
    VALI *
    FROM sammud
    WHERE (ala = 0) JA (tsoon = 19)
) NIMEDEGA s3 KASUTADES (inimene)
KUS p.s2w <= s3.kui

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

1 rida komplektis. Kulunud: 0,068 s. 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 suhteliselt kõrge IOPS (1000-1300 nelja andmete täitmise voolu jaoks), samas kui IOWAIT oli üsna kõrge. Samal ajal genereeris ClickHouse minimaalset koormust kettasüsteemile.

Kokkuvõte

Otsustasime kasutada ClickHouse'i selliste päringute teenindamiseks. Saame alati veelgi optimeerida päringuid, kasutades materialiseeritud vaateid ja paralleliseerimist, tehes eelnevat töötlemist sündmuste voogude jaoks Apache Flinki abil enne nende laadimist ClickHouse'i.

Jõudlus on nii hea, et me tõenäoliselt ei pea isegi mõtlema pivot-table'ide programmilise läbiviimise peale. Varem pidime me tegema andmete pivot'e, mis pärinevad Verticast läbi eksportimise Apache Parquet'i.

Kahjuks ei olnud veel üks katse grafitabelite DB kasutamisel edukas. Ma ei leidnud, et JanusGraph'i ekosüsteem oleks sõbralik, mis võimaldaks toote kiiret omandamist. Samuti kasutab serveri konfiguratsioon traditsioonilist Java-viisi, mis paneks inimesi, kes ei tunne Java-d, veretuks.

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 on asend Gryo ja 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 ja Graphson, uusimad versioonid
  - { 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] }}
  # Vana serialiseerimise versioonid tagurpidi ühilduvuse jaoks:
  - { 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}

Olin juhuslikult "panema" BerkeleyDB versiooni JanusGraph.

Dokumentatsioon on indeksite osas üsna kohmakas, kuna indeksite haldamiseks on vaja teha üsna kummalisi rituaale Groovy-s. Näiteks indeksi loomine peab toimuma Gremlini konsoolis koodi kirjutamise kaudu (mille tööle saamine ei toimi muide kohe). JanusGraphi ametlikest dokumentidest:

graph.tx().rollback()  // Ära loo uusi indekseid, 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()
// Uuenda olemasolevaid andmeid
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()

Eessõna

Teatud mõttes on eelpooltoodud eksperiment võrreldav sooja ja pehme võrdlemisega. Kui süveneda, siis graafiku andmebaas teostab teisi operatsioone, et saavutada samu tulemusi. Siiski tegin testide raames ka eksperiment järgmise taolise päringuga:

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

, mis peegeldab astmelist ligipääsetavust. Siiski näitas graafiku andmebaas isegi selliste andmete puhul tulemust, mis ületas mitut sekundit ... See on muidugi seotud sellega, et olid teed, millel oli 0 -> X -> Y ... -> 1, mida graafiku mootor samuti kontrollis.

Isegi järgmise taolise päringu puhul:

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

ei õnnestunud mul saada tõhusat vastust töötlusajaga alla sekundi.

Moraal on see, et ilus ide ja paradigmastik modelleerimine ei viida soovitud tulemusele, mida märksa kõrgema efektiivsusega demonstreeritakse ClickHouse'i näitel. Artiklis toodud näidisjuhtum on selge antipattern graafiku andmebaaside jaoks, kuigi see näeb välja sobiv nende paradigma modelleerimiseks.

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster