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: tÀhelepanu, Àrge pöörake sellele tÀhelepanu, need kuuluvad domeenile ja testi raames ei ole need arvesse vÔetud. KokkuvÔtteks, sellise graafilise struktuuri jaoks otsingupÀring kettaid nÀeks vÀlja jÀrgmine:

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