Tilraun sem prófar nothæfi JanusGraph graf DBMS til að leysa vandamálið við að finna hentugar leiðir

Tilraun sem prófar nothæfi JanusGraph graf DBMS til að leysa vandamálið við að finna hentugar leiðir

Hæ allir. Við erum að þróa vöru fyrir ónettengda umferðargreiningu. Verkefnið hefur verkefni sem tengist tölfræðilegri greiningu á ferðaleiðum á milli landshluta.

Sem hluti af þessu verkefni geta notendur spurt kerfisfyrirspurna af eftirfarandi gerð:

  • hversu margir gestir fóru frá svæði "A" yfir á svæði "B";
  • hversu margir gestir fóru frá svæði "A" yfir á svæði "B" í gegnum svæði "C" og síðan í gegnum svæði "D";
  • hversu langan tíma það tók fyrir ákveðna tegund gesta að ferðast frá svæði „A“ til svæðis „B“.

og fjölda svipaðra greiningarfyrirspurna.

Hreyfing gestsins yfir svæði er beint línurit. Eftir að hafa lesið internetið uppgötvaði ég að graf DBMS eru einnig notuð fyrir greiningarskýrslur. Ég hafði löngun til að sjá hvernig graf DBMS myndi takast á við slíkar fyrirspurnir (TL; DR; illa).

Ég valdi að nota DBMS JanusGraph, sem framúrskarandi fulltrúi grafísks opins DBMS, sem byggir á stafla af þroskaðri tækni, sem (að mínu mati) ætti að veita því viðeigandi rekstrareiginleika:

  • BerkeleyDB geymslubakendi, Apache Cassandra, Scylla;
  • Hægt er að geyma flóknar vísitölur í Lucene, Elasticsearch, Solr.

Höfundar JanusGraph skrifa að það henti bæði fyrir OLTP og OLAP.

Ég hef unnið með BerkeleyDB, Apache Cassandra, Scylla og ES, og þessar vörur eru oft notaðar í kerfum okkar, svo ég var bjartsýnn á að prófa þetta graf DBMS. Mér fannst skrítið að velja BerkeleyDB fram yfir RocksDB, en það er líklega vegna viðskiptakrafnanna. Í öllum tilvikum, fyrir stigstærð vörunotkun, er mælt með því að nota bakenda á Cassandra eða Scylla.

Ég taldi ekki Neo4j vegna þess að þyrping krefst viðskiptaútgáfu, það er að varan er ekki opinn uppspretta.

Graf DBMSs segja: "Ef það lítur út eins og línurit, meðhöndlaðu það eins og línurit!" - fegurð!

Í fyrsta lagi teiknaði ég línurit, sem var gert nákvæmlega í samræmi við grunnreglur graf DBMSs:

Tilraun sem prófar nothæfi JanusGraph graf DBMS til að leysa vandamálið við að finna hentugar leiðir

Það er kjarni Zone, sem ber ábyrgð á svæðinu. Ef ZoneStep tilheyrir þessu Zone, þá vísar hann til þess. Um kjarnann Area, ZoneTrack, Person Ekki taka eftir, þau tilheyra léninu og eru ekki talin hluti af prófinu. Alls myndi keðjuleitarfyrirspurn fyrir slíka grafbyggingu líta svona út:

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

Hvað á rússnesku er eitthvað á þessa leið: finndu Zone með ID=0, taktu alla hornpunkta sem brún fer á (ZoneStep), stappaðu án þess að fara til baka þar til þú finnur ZoneSteps sem það er brún að Zone með ID=19, teldu fjölda slíkra keðja.

Ég þykist ekki vita allar ranghala leitina á línuritum, en þessi fyrirspurn var búin til út frá þessari bók (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Ég hlóð 50 þúsund lögum á bilinu 3 til 20 punkta að lengd inn í JanusGraph grafgagnagrunn með því að nota BerkeleyDB bakenda, bjó til vísitölur skv. forystu.

Python niðurhalshandrit:


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)

Við notuðum VM með 4 kjarna og 16 GB vinnsluminni á SSD. JanusGraph var sett á vettvang með þessari skipun:

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

Í þessu tilviki eru gögnin og vísitölurnar sem eru notaðar fyrir nákvæma samsvörun geymd í BerkeleyDB. Eftir að hafa framkvæmt beiðnina sem gefin var áðan fékk ég tíma sem jafngildir nokkrum tugum sekúndna.

Með því að keyra 4 ofangreind forskrift samhliða, tókst mér að breyta DBMS í grasker með glaðlegum straumi af Java stacktraces (og við elskum öll að lesa Java stacktraces) í Docker logs.

Eftir smá umhugsun ákvað ég að einfalda línuritið í eftirfarandi:

Tilraun sem prófar nothæfi JanusGraph graf DBMS til að leysa vandamálið við að finna hentugar leiðir

Að ákveða að leit eftir eiginleikum einingarinnar væri hraðari en að leita eftir brúnum. Fyrir vikið breyttist beiðni mín í eftirfarandi:

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

Hvað á rússnesku er eitthvað á þessa leið: finndu ZoneStep með ID=0, stappaðu án þess að fara til baka þar til þú finnur ZoneStep með ID=19, teldu fjölda slíkra keðja.

Ég einfaldaði einnig hleðsluforritið sem gefið er upp hér að ofan til að búa ekki til óþarfa tengingar, takmarka mig við eiginleika.

Beiðnin tók samt nokkrar sekúndur að klára, sem var algjörlega óviðunandi fyrir verkefni okkar, þar sem hún hentaði alls ekki fyrir tilgangi AdHoc beiðna af neinu tagi.

Ég reyndi að dreifa JanusGraph með því að nota Scylla sem hraðvirkustu Cassandra útfærsluna, en þetta leiddi heldur ekki til marktækra breytinga á frammistöðu.

Svo þrátt fyrir þá staðreynd að "það lítur út eins og línurit", gat ég ekki fengið grafið DBMS til að vinna það hratt. Ég geri alveg ráð fyrir að það sé eitthvað sem ég veit ekki og að JanusGraph sé hægt að framkvæma þessa leit á sekúndubroti, hins vegar gat ég ekki gert það.

Þar sem enn þurfti að leysa vandamálið fór ég að hugsa um JOINs og Pivots of töflur, sem vöktu ekki bjartsýni hvað varðar glæsileika, en gæti verið fullkomlega framkvæmanlegur kostur í reynd.

Verkefnið okkar notar nú þegar Apache ClickHouse, svo ég ákvað að prófa rannsóknir mínar á þessu greinandi DBMS.

Notaði ClickHouse með einfaldri uppskrift:

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

Ég bjó til gagnagrunn og töflu í honum svona:

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

Ég fyllti það með gögnum með því að nota eftirfarandi handrit:

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
    )

Þar sem innlegg koma í lotum var fyllingin mun hraðari en fyrir JanusGraph.

Smíðaði tvær fyrirspurnir með JOIN. Til að fara frá punkti A yfir í punkt 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

Til að fara í gegnum 3 punkta:

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

Beiðnirnar líta auðvitað frekar ógnvekjandi út; til raunverulegrar notkunar þarftu að búa til hugbúnaðarrafall. Hins vegar virka þeir og þeir vinna hratt. Bæði fyrstu og annarri beiðni er lokið á innan við 0.1 sekúndu. Hér er dæmi um framkvæmdartíma fyrirspurnar fyrir talningu (*) sem fer í gegnum 3 punkta:

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

Athugasemd um IOPS. Þegar gögn voru fyllt út myndaði JanusGraph nokkuð háan fjölda IOPS (1000-1300 fyrir fjóra gagnafjöldaþræði) og IOWAIT var frekar hátt. Á sama tíma myndaði ClickHouse lágmarksálag á undirkerfi disksins.

Ályktun

Við ákváðum að nota ClickHouse til að sinna þessari tegund beiðni. Við getum alltaf fínstillt fyrirspurnir enn frekar með því að nota efnislegar skoðanir og samhliða samsetningu með því að forvinna viðburðarstrauminn með Apache Flink áður en þær eru hlaðnar inn í ClickHouse.

Frammistaðan er svo góð að við þurfum sennilega ekki einu sinni að hugsa um að snúa töflum forritunarlega. Áður þurftum við að gera pivots á gögnum sem voru sótt frá Vertica með því að hlaða upp á Apache Parket.

Því miður tókst önnur tilraun til að nota graf DBMS misheppnuð. Mér fannst JanusGraph ekki vera með vinalegt vistkerfi sem gerði það auðvelt að komast upp með vöruna. Á sama tíma, til að stilla netþjóninn, er hefðbundin Java leið notuð, sem mun fá fólk sem ekki þekkir Java til að gráta blóðtár:

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}

Mér tókst óvart að „setja“ BerkeleyDB útgáfuna af JanusGraph.

Skjölin eru frekar skakkt hvað varðar vísitölur, þar sem að stjórna vísitölum krefst þess að þú framkvæmir frekar undarlegan shamanisma í Groovy. Til dæmis þarf að búa til vísitölu með því að skrifa kóða í Gremlin stjórnborðið (sem, við the vegur, virkar ekki beint úr kassanum). Frá opinberu JanusGraph skjölunum:

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

Eftirsögn

Í vissum skilningi er tilraunin hér að ofan samanburður á heitu og mjúku. Ef þú hugsar um það framkvæmir graf DBMS aðrar aðgerðir til að fá sömu niðurstöður. Hins vegar, sem hluti af prófunum, gerði ég einnig tilraun með beiðni eins og:

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

sem endurspeglar göngufæri. Hins vegar, jafnvel á slíkum gögnum, sýndi grafið DBMS niðurstöður sem fóru yfir nokkrar sekúndur... Þetta er auðvitað vegna þess að það voru leiðir eins og 0 -> X -> Y ... -> 1, sem grafavélin athugaði einnig.

Jafnvel fyrir fyrirspurn eins og:

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

Ég gat ekki fengið afkastamikið svar með vinnslutíma sem var innan við sekúndu.

Siðferði sögunnar er að falleg hugmynd og hugmyndafræðileg líkan leiða ekki til tilætluðs árangurs, sem er sýnt með mun meiri skilvirkni með því að nota ClickHouse dæmi. Notkunartilvikið sem kynnt er í þessari grein er skýrt andmynstur fyrir graf DBMSs, þó að það virðist henta fyrir líkanagerð í þeirra hugmyndafræði.

Heimild: www.habr.com

Bæta við athugasemd