Eksperimenti i verifikimit të aplikueshmërisë së DB-ve grafike JanusGraph për zgjidhjen e problemit të kërkimit të rrugëve të përshtatshme

Eksperimenti i verifikimit të aplikueshmërisë së DB-ve grafike JanusGraph për zgjidhjen e problemit të kërkimit të rrugëve të përshtatshme

Përshëndetje të gjithëve. Ne po zhvillojmë një produkt për analizën e trafik të jashtëm. Në projekt ka një detyrë që lidhet me analizën statistikore të rrugëve të lëvizjes së vizitorëve nëpër zona.

Në kuadër të kësaj detyre, përdoruesit mund të bëjnë kërkesa të tilla si:

  • sa vizitorĂ« kaluan nga zona "A" nĂ« zonĂ«n "B";
  • sa vizitorĂ« kaluan nga zona "A" nĂ« zonĂ«n "B" pĂ«rmes zonĂ«s "C", pastaj pĂ«rmes zonĂ«s "D";
  • sa kohĂ« ka marrĂ« kalimi i njĂ« vizitori tĂ« caktuar nga zona "A" nĂ« zonĂ«n "B".

dhe një sërë kërkesash analitike të ngjashme.

Lëvizja e vizitorëve nëpër zona paraqet një grafik të orientuar. Pas leximit të disa burimeve në internet, zbulova se DB-të grafike përdoren edhe për raportet analitike. Më erdhi dëshira të shoh si do të përballen me këto kërkesa DB-të grafike (TL;DR; keq).

Kam zgjedhur për përdorim DB-në JanusGraph, si një përfaqësues të shkëlqyer të bazave të të dhënave grafike open-source, e cila mbështetet në një teknologji të pjekur, që (mendimi im) duhet t'i sigurojë asaj performancë të pranueshme operacionale:

  • backend-i i magazinĂ«s BerkeleyDB, Apache Cassandra, Scylla;
  • indekse tĂ« komplikuara mund tĂ« ruhen nĂ« Lucene, Elasticsearch, Solr.

Autoret e JanusGraph thonë se ajo është e përshtatshme për OLTP dhe OLAP.

Unë kam punuar me BerkeleyDB, Apache Cassandra, Scylla dhe ES, për më tepër, këto produkte shpesh përdoren në sistemet tona, kështu që shikoja me optimizëm testimin e kësaj baze të të dhënash grafike. Më dukej e çuditshme zgjedhja e BerkeleyDB-së në vend të RocksDB-së, por ndoshta kjo është për shkak të kërkesave për transaksionet. Në çdo rast, për përdorim të shkallëzueshëm dhe produktiv, rekomandohet të përdoret backend-i në Cassandra ose Scylla.

Neo4j nuk e shqyrtova, pasi për klasterizimin kërkohet versioni komercial, pra produkti nuk është i hapur.

Baza tĂ« dhĂ«nash grafike thonĂ«: "NĂ«se diçka duket si njĂ« grafik — trajtojeni si njĂ« grafik!" — bukuri!

Së pari, kam vizatuar një grafik që është bërë pikërisht sipas kanoneve të bazave të të dhënave grafike:

Eksperimenti i verifikimit të aplikueshmërisë së DB-ve grafike JanusGraph për zgjidhjen e problemit të kërkimit të rrugëve të përshtatshme

Ka një entitet Zone, që është përgjegjës për një zonë. Nëse ZoneStep i përket kësaj Zone, atëherë ai e referon atë. Në entitet Zona, ZoneTrack, Person mos i kushtoni vëmendje, ato i përkasin domainit dhe në kuadër të testit nuk shqyrtohen. Pra, për një strukturë të tillë grafike, kërkesa për të gjetur zinxhirë do të dukej si:

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

ÇfarĂ« nĂ« rusisht duket si: gjej Zone me ID=0, merr tĂ« gjitha majat tĂ« cilat kanĂ« njĂ« lidhje (ZoneStep) pĂ«r tĂ«, eci pa u kthyer deri sa tĂ« gjej ato ZoneStep, nga tĂ« cilat ka njĂ« lidhje nĂ« Zone me ID=19, numĂ«ro numrin e atyre zinxhirĂ«ve.

Nuk pretendoj se njoh të gjitha nuancat e kërkimit në graf, por kjo kërkesë u gjenerua mbi bazën e kësaj libri (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Kam ngarkuar 50 mijë njerëz me një gjatësi nga 3 deri në 20 pika në bazën e të dhënave grafike JanusGraph, e cila përdor backend-in BerkeleyDB, krijova indekse sipas udhëzimeve.

Skripti për ngarkimin në Python:


nga nga import random
nga nga time import time

nga nga init import g, graph

nëse __name__ == '__main__':

    pika = []
    maksimale_zone = 19
    zcache = dict()
    për i në gamën (0, maksimale_zone + 1):
        zcache[i] = g.addV('Zone').property('id', i).next()

    startZ = zcache[0]
    endZ = zcache[maksimale_zone]

    për i në gamën (0, 10000):

        nëse jo i % 100:
            print(i)

        start = g.addV('ZoneStep').property('time', int(time())).next()
        g.V(start).addE('belongs').to(startZ).iterate()

        ndërsa e vërtetë:
            pt = g.addV('ZoneStep').property('time', int(time())).next()
            end_chain = random()
            nëse end_chain < 0.3:
                g.V(pt).addE('belongs').to(endZ).iterate()
                g.V(start).addE('goes').to(pt).iterate()
                break
            tjetër:
                zone_id = int(random() * maksimale_zone)
                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)

I është përdorur VM me 4 bërthama dhe 16 GB RAM në SSD. JanusGraph u vendos me këtë komandë:

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

Në këtë rast, të dhënat dhe indekset që përdoren për kërkimin e përputhjes së saktë ruhen në BerkeleyDB. Pas ekzekutimit të kërkesës së dhënë më parë, kam marrë një kohë të barabartë me disa dhjetëra sekonda.

Duke ekzekutuar 4 skriptat e mësipërm në paralel, arrita ta shndërroj DB në një të tillë me një fluks të këndshëm të stectraive Java (dhe ne të gjithë e duam të lexojmë stectraive Java) në logjet e Docker.

Pas mendova, vendosa ta e thjeshtoj diagramën e grafit në këtë formë:

Eksperimenti i verifikimit të aplikueshmërisë së DB-ve grafike JanusGraph për zgjidhjen e problemit të kërkimit të rrugëve të përshtatshme

Duke menduar se kërkimi sipas atributeve të entitetit do të ishte më i shpejtë se kërkimi sipas skajeve. Si rezultat, kërkesa ime u shndërrua në këtë:

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

Kjo në rusisht do të thoshte: gjej ZoneStep me ID=0, vajti pa u kthyer derisa të gjej ZoneStep me ID=19, llogariti numrin e lidhjeve të tilla.

Skemën e shkarkimit të dhënë më sipër, e thjeshtova gjithashtu, për të mos krijuar lidhje të tepruara, duke u kufizuar në atribute.

Kërkesa gjithsesi e ekzekutuar disa sekonda, që ishte plotësisht e papranueshme për detyrën tonë, pasi për qëllime AdHoc kërkesa e llojeve të rastësishme nuk ishte aspak e përshtatshme.

Kam provuar të zgjas JanusGraph duke përdorur Scylla, si realizimin më të shpejtë të Cassandra, por kjo gjithashtu nuk çoi në ndonjë ndryshim të rëndësishëm në performancë.

Pra, megjithëse "dukesh si graf", nuk mund të bëj që baza e të dhënave grafike të përpunojë këtë shpejt. E kam të qartë që ndoshta nuk di diçka dhe mund të bëj që JanusGraph të realizojë këtë kërkesë për disa sekonda, por nuk më ka dalë.

Pasi që ishte e nevojshme të zgjidhet problemi, fillova të mendoj për JOIN dhe Pivot të tabelave, që nuk jepte optimizëm në aspektin e elegancës, por mund të ishte një mundësi e pranueshme në praktikë.

Në projektin tonë tashmë po përdoret Apache ClickHouse, kështu që vendosa të verifikoj përfundimet e mia në këtë DB analitik.

E lançova ClickHouse në një recetë të thjeshtë:

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

Krijova një DB dhe një tabelë si kjo:

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

E mbusha atë me të dhëna duke përdorur këtë skript:

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
    )

Duke insertet janë bërë në grupe, mbushja ishte shumë më e shpejtë se për JanusGraph.

Ndërtoi dy kërkesa nëpërmjet JOIN. Për kalimin nga pika A në pikën 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

Për kalimin përmes 3 pikave:

Zgjidhni s3.person,
       s1z,
       s1w,
       s2z,
       s2w,
       s3.zone,
       s3.when
FROM
  (Zgjidhni s1.person AS person,
          s1.zone AS s1z,
          s1.when AS s1w,
          s2.zone AS s2z,
          s2.when AS s2w
   FROM
     (Zgjidhni *
      NGA steps
      KU (area = 0)
        DHE (zone = 0)) SI s1 ÇDO INNER JOIN
     (Zgjidhni *
      NGA steps SI s2
      KU (area = 0)
        DHE (zone = 3)) SI s2 PËRMBAJTJE person
   KU s1.when <= s2.when) p ÇDO INNER JOIN
  (Zgjidhni *
   NGA steps
   KU (area = 0)
     DHE (zone = 19)) SI s3 PËRMBAJTJE person
KU p.s2w <= s3.when

Kërkesat, sigurisht, duken mjaft frikësuese, për përdorim real kërkohet një lidhje programore-gjenerator. Megjithatë, ato funksionojnë dhe funksionojnë shpejt. Të dy kërkesat, të parën dhe të dytën, i përfundon për më pak se 0.1 sekondë. Ja një shembull i kohës së ekzekutimit të kërkesës për count(*) kalim nëpër 3 pika:

Zgjidhni count(*)
FROM 
(
    Zgjidhni 
        s1.person AS person, 
        s1.zone AS s1z, 
        s1.when AS s1w, 
        s2.zone AS s2z, 
        s2.when AS s2w
    FROM 
    (
        Zgjidhni *
        NGA steps
        KU (area = 0) DHE (zone = 0)
    ) SI s1
    ÇDO INNER JOIN 
    (
        Zgjidhni *
        NGA steps SI s2
        KU (area = 0) DHE (zone = 3)
    ) SI s2 PËRMBAJTJE (person)
    KU s1.when <= s2.when
) SI p
ÇDO INNER JOIN 
(
    Zgjidhni *
    NGA steps
    KU (area = 0) DHE (zone = 19)
) SI s3 PËRMBAJTJE (person)
KU p.s2w <= s3.when

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

1 rreshta në grup. Koha e kaluar: 0.068 sek. Procesuar 250.03 mijë rreshta, 8.00 MB (3.69 milion rreshta/s., 117.98 MB/s.)

Vërejtje në lidhje me IOPS. Gjatë mbushjes së të dhënave, JanusGraph gjeneroi një numër të lartë të IOPS (1000-1300 për katër këngë mbushjeje të dhënash), dhe IOWAIT ishte mjaft i lartë. Në të njëjtën kohë, ClickHouse gjeneroi një ngarkesë minimale në nënshkrimin e diskut.

Përfundimi

Vendosëm të përdorim ClickHouse për të shërbyer kërkesat e këtij lloji. Ne gjithmonë mund të optimizojmë edhe më shumë kërkesat duke përdorur pamjet e materializuara dhe paralelizimin, duke kryer përpunimin paraprak të rrjedhës së ngjarjeve me Apache Flink para se t'i ngarkojmë në ClickHouse.

Performanca është kaq e mirë, saqë ndoshta nuk do të na nevojitet të mendojmë madje për pivot-et e tabelave me mjete programore. Më parë, na duhej të bënim pivot të të dhënave të nxjerra nga Vertica përmes eksportit në Apache Parquet.

FatkeqĂ«sisht, pĂ«rpjekja e radhĂ«s pĂ«r tĂ« pĂ«rdorur njĂ« DB graf ishte e pasuksesshme. Nuk e gjeta JanusGraph si njĂ« ekosistem miqĂ«sor, qĂ« lejon njĂ« adaptim tĂ« shpejtĂ« me produktin. NdĂ«rkohĂ«, konfigurimi i serverit ndiqet nga mĂ«nyra tradicionale Java, qĂ« do t’i bĂ«jĂ« tĂ« qajnĂ« me lot tĂ« kuq njerĂ«zit qĂ« nuk janĂ« tĂ« njohur me Java:

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 është këtu për të zëvendësuar Gryo dhe 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 dhe Graphson, versionet më të reja
  - { 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] }}
  # Versione më të vjetra serializimi për kompatibilitetin mbrapa:
  - { 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}

Më rezultoi rastësi "të vendosja" versionin BerkeleyDB të JanusGraph.

Dokumentacioni është mjaft i çrregullt sa i përket indekseve, pasi administrimi i indekseve kërkon të bëhen disa magji të çuditshme në Groovy. Për shembull, krijimi i një indeksi duhet të bëhet duke shkruar kod në konsolën Gremlin (e cila, për të qenë e saktë, nuk punon direkt). Nga dokumentacioni zyrtar i JanusGraph:

graph.tx().rollback() //Kurrë mos krijoni indekse të reja ndërsa një transaksion është aktiv
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()

//Prisni që indeksi të bëhet i disponueshëm
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
//Rindizajni të dhënat ekzistuese
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()

Pasthënie

Në një kuptim, eksperimenti i sipërm është një krahasim i ngrohtë me të butë. Nëse e mendoni, një DB graf-zhvillim kryen operacione të tjera për të arritur të njëjtat rezultate. Megjithatë, gjatë testeve unë bëra edhe një eksperiment me një kërkesë të këtij lloji:

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

i cili pasqyron aksesin në hapësira. Megjithatë, edhe me të dhëna të tilla, baza e të dhënave grafike tregoi rezultate që tejkalonin disa sekonda... Kjo, natyrisht, lidhet me faktin se kishte rrugë të tilla 0 -> X -> Y ... -> 1, të cilat motori grafik gjithashtu i kontrollonte.

Madje për një kërkesë të tillë:

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

nuk më arriti të merrja një përgjigje produktive me kohë përpunimi më pak se një sekondë.

Moraliteti i fabulës është se një ide e bukur dhe modelizimi paradigmatik nuk çojnë në rezultatin e dëshiruar, i cili demonstrohet me një efikasitet shumë më të lartë në shembullin e ClickHouse. Shembulli i sjellë në këtë artikull është një antipater të qartë për bazat e të dhënave grafike, megjithëse duket se është i përshtatshëm për modelim në paradigmën e tyre.

Burimi: habr.com

Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« đŸ”„ Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« | ProHoster