Arbrawf yn profi cymhwysedd graff JanusGraph DBMS ar gyfer datrys y broblem o ddod o hyd i lwybrau addas

Arbrawf yn profi cymhwysedd graff JanusGraph DBMS ar gyfer datrys y broblem o ddod o hyd i lwybrau addas

Helo i gyd. Rydym yn datblygu cynnyrch ar gyfer dadansoddi traffig all-lein. Mae gan y prosiect dasg sy'n ymwneud Γ’ dadansoddiad ystadegol o lwybrau ymwelwyr ar draws rhanbarthau.

Fel rhan o'r dasg hon, gall defnyddwyr ofyn ymholiadau system o'r math canlynol:

  • faint o ymwelwyr a aeth o ardal "A" i ardal "B";
  • faint o ymwelwyr a aeth o ardal "A" i ardal "B" trwy ardal "C" ac yna trwy ardal "D";
  • faint o amser gymerodd hi i fath arbennig o ymwelydd deithio o ardal β€œA” i ardal β€œB”.

a nifer o ymholiadau dadansoddol tebyg.

Mae symudiad yr ymwelydd ar draws ardaloedd yn graff cyfeiriedig. Ar Γ΄l darllen y Rhyngrwyd, darganfyddais fod DBMSs graff hefyd yn cael eu defnyddio ar gyfer adroddiadau dadansoddol. Roeddwn yn awyddus i weld sut y byddai DBMSs graff yn ymdopi ag ymholiadau o'r fath (TL; DR; yn wael).

Dewisais ddefnyddio'r DBMS Graff Ionawr, fel cynrychiolydd rhagorol o graff ffynhonnell agored DBMS, sy'n dibynnu ar bentwr o dechnolegau aeddfed, a ddylai (yn fy marn i) ddarparu nodweddion gweithredol gweddus iddo:

  • Backend storio BerkeleyDB, Apache Cassandra, Scylla;
  • gellir storio mynegeion cymhleth yn Lucene, Elasticsearch, Solr.

Mae awduron JanusGraph yn ysgrifennu ei fod yn addas ar gyfer OLTP ac OLAP.

Rydw i wedi gweithio gyda BerkeleyDB, Apache Cassandra, Scylla ac ES, ac mae'r cynhyrchion hyn yn cael eu defnyddio'n aml yn ein systemau, felly roeddwn i'n obeithiol am brofi'r graff hwn DBMS. Roeddwn yn ei chael hi'n rhyfedd dewis BerkeleyDB dros RocksDB, ond mae'n debyg bod hynny oherwydd y gofynion trafodion. Mewn unrhyw achos, ar gyfer defnydd graddadwy, cynnyrch, argymhellir defnyddio backend ar Cassandra neu Scylla.

Ni wnes i ystyried Neo4j oherwydd bod clystyru yn gofyn am fersiwn fasnachol, hynny yw, nid yw'r cynnyrch yn ffynhonnell agored.

Mae Graffiau DBMS yn dweud: β€œOs yw’n edrych fel graff, dylech ei drin fel graff!” - harddwch!

Yn gyntaf, lluniais graff, a wnaethpwyd yn union yn Γ΄l canonau graff DBMSs:

Arbrawf yn profi cymhwysedd graff JanusGraph DBMS ar gyfer datrys y broblem o ddod o hyd i lwybrau addas

Mae hanfod Zone, yn gyfrifol am yr ardal. Os ZoneStep yn perthyn i hyn Zone, yna cyfeiria ato. Ar hanfod Area, ZoneTrack, Person Peidiwch Γ’ thalu sylw, maen nhw'n perthyn i'r parth ac nid ydyn nhw'n cael eu hystyried fel rhan o'r prawf. Yn gyfan gwbl, byddai ymholiad chwilio cadwyn ar gyfer strwythur graff o'r fath yn edrych fel:

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

Beth yn Rwsieg yw rhywbeth fel hyn: dewch o hyd i Barth gydag ID=0, cymerwch yr holl fertigau y mae ymyl yn mynd ato (ZoneStep), stompiwch heb fynd yn Γ΄l nes i chi ddod o hyd i'r ZoneSteps hynny y mae ymyl i'r Parth Γ’ nhw ID=19, cyfrwch nifer y cadwyni o'r fath.

Nid wyf yn smalio fy mod yn gwybod holl gymhlethdodau chwilio ar graffiau, ond cynhyrchwyd yr ymholiad hwn yn seiliedig ar y llyfr hwn (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Llwythais 50 mil o draciau yn amrywio o 3 i 20 pwynt o hyd i gronfa ddata graff JanusGraph gan ddefnyddio backend BerkeleyDB, creu mynegeion yn Γ΄l arweinyddiaeth.

Sgript lawrlwytho Python:


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)

Fe wnaethon ni ddefnyddio VM gyda 4 cores a 16 GB RAM ar SSD. Cafodd JanusGraph ei ddefnyddio gan ddefnyddio'r gorchymyn hwn:

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

Yn yr achos hwn, mae'r data a'r mynegeion a ddefnyddir ar gyfer chwiliadau cyfatebol union yn cael eu storio yn BerkeleyDB. Wedi gweithredu'r cais a roddwyd yn gynharach, cefais amser cyfartal i sawl degau o eiliadau.

Trwy redeg y 4 sgript uchod yn gyfochrog, llwyddais i droi'r DBMS yn bwmpen gyda ffrwd siriol o stactraces Java (ac rydym i gyd wrth ein bodd yn darllen stactraces Java) yn y logiau Docker.

Ar Γ΄l ychydig o feddwl, penderfynais symleiddio'r diagram graff i'r canlynol:

Arbrawf yn profi cymhwysedd graff JanusGraph DBMS ar gyfer datrys y broblem o ddod o hyd i lwybrau addas

Penderfynu y byddai chwilio yn Γ΄l priodoleddau endid yn gyflymach na chwilio yn Γ΄l ymylon. O ganlyniad, trodd fy nghais i'r canlynol:

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

Beth yn Rwsieg yw rhywbeth fel hyn: dewch o hyd i ZoneStep gydag ID=0, stompiwch heb fynd yn Γ΄l nes i chi ddod o hyd i ZoneStep gydag ID=19, cyfrwch nifer y cadwyni o'r fath.

Fe wnes i hefyd symleiddio'r sgript llwytho a roddir uchod er mwyn peidio Γ’ chreu cysylltiadau diangen, gan gyfyngu fy hun i briodoleddau.

Roedd y cais yn dal i gymryd sawl eiliad i'w gwblhau, a oedd yn gwbl annerbyniol ar gyfer ein tasg, gan nad oedd yn addas o gwbl at ddibenion ceisiadau AdHoc o unrhyw fath.

Ceisiais ddefnyddio JanusGraph gan ddefnyddio Scylla fel y gweithrediad Cassandra cyflymaf, ond ni arweiniodd hyn ychwaith at unrhyw newidiadau perfformiad sylweddol.

Felly er gwaethaf y ffaith bod "mae'n edrych fel graff", ni allwn gael y graff DBMS i'w brosesu'n gyflym. Rwy'n cymryd yn llwyr bod rhywbeth nad wyf yn ei wybod ac y gellir gwneud JanusGraph i berfformio'r chwiliad hwn mewn ffracsiwn o eiliad, fodd bynnag, nid oeddwn yn gallu ei wneud.

Gan fod angen datrys y broblem o hyd, dechreuais feddwl am JOINs a Pivots of tables, nad oedd yn ysgogi optimistiaeth o ran ceinder, ond a allai fod yn opsiwn cwbl ymarferol yn ymarferol.

Mae ein prosiect eisoes yn defnyddio Apache ClickHouse, felly penderfynais brofi fy ymchwil ar y DBMS dadansoddol hwn.

Wedi defnyddio ClickHouse gan ddefnyddio rysΓ‘it syml:

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

Creais gronfa ddata a thabl ynddi fel hyn:

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

Fe'i llenwais Γ’ data gan ddefnyddio'r sgript ganlynol:

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
    )

Gan fod mewnosodiadau yn dod mewn sypiau, roedd y llenwi'n llawer cyflymach nag ar gyfer JanusGraph.

Lluniwyd dau ymholiad gan ddefnyddio JOIN. I symud o bwynt A i bwynt 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

I fynd trwy 3 phwynt:

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

Mae'r ceisiadau, wrth gwrs, yn edrych yn eithaf brawychus; ar gyfer defnydd go iawn, mae angen i chi greu harnais generadur meddalwedd. Fodd bynnag, maent yn gweithio ac yn gweithio'n gyflym. Mae'r cais cyntaf a'r ail gais yn cael eu cwblhau mewn llai na 0.1 eiliad. Dyma enghraifft o amser gweithredu ymholiad ar gyfer cyfrif(*) yn mynd trwy 3 phwynt:

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

Nodyn am IOPS. Wrth boblogi data, cynhyrchodd JanusGraph nifer eithaf uchel o IOPS (1000-1300 ar gyfer pedwar llinyn poblogaeth data) ac roedd IOWAIT yn eithaf uchel. Ar yr un pryd, cynhyrchodd ClickHouse y llwyth lleiaf posibl ar yr is-system ddisg.

Casgliad

Fe wnaethom benderfynu defnyddio ClickHouse i wasanaethu'r math hwn o gais. Gallwn bob amser optimeiddio ymholiadau ymhellach gan ddefnyddio golygfeydd wedi'u gwireddu a chyfochredd trwy rag-brosesu'r ffrwd digwyddiad gan ddefnyddio Apache Flink cyn eu llwytho i mewn i ClickHouse.

Mae'r perfformiad mor dda mae'n debyg na fydd yn rhaid i ni hyd yn oed feddwl am dablau pivoting yn rhaglennol. Yn flaenorol, roedd yn rhaid i ni wneud colyn o ddata a adalwyd o Vertica trwy uwchlwytho i Apache Parquet.

Yn anffodus, bu ymgais arall i ddefnyddio graff DBMS yn aflwyddiannus. Doeddwn i ddim yn gweld bod gan JanusGraph ecosystem gyfeillgar a oedd yn ei gwneud hi'n hawdd i ddod yn gyfarwydd Γ’'r cynnyrch. Ar yr un pryd, i ffurfweddu'r gweinydd, defnyddir y ffordd Java draddodiadol, a fydd yn gwneud i bobl nad ydynt yn gyfarwydd Γ’ Java wylo dagrau gwaed:

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}

Llwyddais i "roi" fersiwn BerkeleyDB o JanusGraph yn ddamweiniol.

Mae'r ddogfennaeth yn eithaf cam o ran mynegeion, gan fod rheoli mynegeion yn gofyn ichi berfformio rhywfaint o siamaniaeth rhyfedd yn Groovy. Er enghraifft, rhaid creu mynegai trwy ysgrifennu cod yn y consol Gremlin (sydd, gyda llaw, ddim yn gweithio allan o'r bocs). O ddogfennaeth swyddogol JanusGraph:

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

Afterword

Mewn un ystyr, mae'r arbrawf uchod yn gymhariaeth rhwng cynnes a meddal. Os ydych chi'n meddwl amdano, mae graff DBMS yn perfformio gweithrediadau eraill i gael yr un canlyniadau. Fodd bynnag, fel rhan o'r profion, cynhaliais arbrawf hefyd gyda chais fel:

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

sy'n adlewyrchu pellter cerdded. Fodd bynnag, hyd yn oed ar ddata o'r fath, roedd y graff DBMS yn dangos canlyniadau a aeth y tu hwnt i ychydig eiliadau... Mae hyn, wrth gwrs, oherwydd y ffaith bod llwybrau fel 0 -> X -> Y ... -> 1, a wiriodd yr injan graff hefyd.

Hyd yn oed ar gyfer ymholiad fel:

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

Nid oeddwn yn gallu cael ymateb cynhyrchiol gydag amser prosesu o lai nag eiliad.

Moesol y stori yw nad yw syniad hardd a modelu paradigmatig yn arwain at y canlyniad a ddymunir, a ddangosir gydag effeithlonrwydd llawer uwch gan ddefnyddio enghraifft ClickHouse. Mae'r achos defnydd a gyflwynir yn yr erthygl hon yn gwrth-batrwm clir ar gyfer DBMSs graff, er ei fod yn ymddangos yn addas ar gyfer modelu yn eu patrwm.

Ffynhonnell: hab.com

Ychwanegu sylw