Փորձ, որը ստուգում է JanusGraph գրաֆիկի DBMS-ի կիրառելիությունը՝ հարմար ուղիներ գտնելու խնդրի լուծման համար

Փորձ, որը ստուգում է JanusGraph գրաֆիկի DBMS-ի կիրառելիությունը՝ հարմար ուղիներ գտնելու խնդրի լուծման համար

Բարեւ բոլորին. Մենք արտադրանք ենք մշակում անցանց երթևեկության վերլուծության համար: Ծրագիրն ունի խնդիր՝ կապված տարածաշրջանների այցելուների երթուղիների վիճակագրական վերլուծության հետ:

Որպես այս առաջադրանքի մաս, օգտվողները կարող են համակարգային հարցումներ տալ հետևյալ տեսակի.

  • քանի այցելու է անցել «Ա» տարածքից «Բ» տարածք;
  • քանի այցելու է անցել «A» տարածքից «B» տարածք «C» և այնուհետև «D» տարածքով.
  • որքան ժամանակ է պահանջվել որոշակի տեսակի այցելուի համար «A» տարածքից «B» տարածք ճանապարհորդելու համար:

և մի շարք նմանատիպ վերլուծական հարցումներ:

Այցելուների շարժումը տարածքներով ուղղորդված գրաֆիկ է: Համացանցը կարդալուց հետո ես հայտնաբերեցի, որ գրաֆիկական DBMS-ները նույնպես օգտագործվում են վերլուծական հաշվետվությունների համար: Ես ցանկություն ունեի տեսնել, թե ինչպես են գրաֆիկական DBMS-ները հաղթահարելու նման հարցումները (TL; DR; Վատ):

Ես որոշեցի օգտագործել DBMS-ը Յանուս ԳրաֆՈրպես գրաֆիկական բաց կոդով DBMS-ի ականավոր ներկայացուցիչ, որը հիմնված է հասուն տեխնոլոգիաների փաթեթի վրա, որը (իմ կարծիքով) պետք է ապահովի նրան պատշաճ գործառնական բնութագրերով.

  • BerkeleyDB պահեստավորման հետնամաս, Apache Cassandra, Scylla;
  • բարդ ինդեքսները կարող են պահվել Lucene, Elasticsearch, Solr-ում:

JanusGraph-ի հեղինակները գրում են, որ այն հարմար է ինչպես OLTP-ի, այնպես էլ OLAP-ի համար։

Ես աշխատել եմ BerkeleyDB-ի, Apache Cassandra-ի, Scylla-ի և ES-ի հետ, և այդ արտադրանքները հաճախ օգտագործվում են մեր համակարգերում, ուստի ես լավատեսորեն էի տրամադրված այս գրաֆիկական DBMS-ի փորձարկման հարցում: Ինձ համար տարօրինակ է ընտրել BerkeleyDB-ն RocksDB-ի փոխարեն, բայց դա, հավանաբար, պայմանավորված է գործարքի պահանջներով: Ամեն դեպքում, մասշտաբային, արտադրանքի օգտագործման համար առաջարկվում է օգտագործել «backend» Cassandra-ի կամ Scylla-ի վրա:

Ես չեմ դիտարկել Neo4j-ը, քանի որ կլաստերավորումը պահանջում է կոմերցիոն տարբերակ, այսինքն՝ արտադրանքը բաց կոդով չէ։

Գրաֆիկական DBMS-ներն ասում են. «Եթե այն նման է գրաֆիկի, վերաբերվեք դրան որպես գրաֆիկի»: - գեղեցկություն!

Նախ, ես գծեցի գրաֆիկ, որը կազմված էր հենց գրաֆիկական DBMS-ների կանոնների համաձայն.

Փորձ, որը ստուգում է JanusGraph գրաֆիկի DBMS-ի կիրառելիությունը՝ հարմար ուղիներ գտնելու խնդրի լուծման համար

Մի էություն կա Zone, տարածքի պատասխանատու. Եթե ZoneStep պատկանում է սրան Zone, ապա անդրադառնում է դրան. Էության վրա Area, ZoneTrack, Person Ուշադրություն մի դարձրեք, դրանք պատկանում են տիրույթին և չեն համարվում թեստի մաս։ Ընդհանուր առմամբ, նման գրաֆիկի կառուցվածքի շղթայական որոնման հարցումը նման կլինի.

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

Ռուսերենում ինչն է նման բան. գտեք Գոտի ID=0-ով, վերցրեք բոլոր գագաթները, որոնցից ծայրը գնում է դեպի այն (ZoneStep), ոտնահարեք առանց հետ գնալու, մինչև գտնեք այն ZoneSteps-ը, որտեղից կա Գոտի եզր: ID=19, հաշվեք նման շղթաների թիվը։

Ես չեմ հավակնում, թե գիտեմ գծապատկերների վրա փնտրելու բոլոր բարդությունները, բայց այս հարցումը ստեղծվել է այս գրքի հիման վրա (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Ես բեռնել եմ 50 հազար հետքեր, որոնք տատանվում են 3-ից 20 միավոր երկարությամբ JanusGraph գրաֆիկական տվյալների բազայում՝ օգտագործելով BerkeleyDB backend-ը, ստեղծել եմ ինդեքսներ՝ համաձայն ղեկավարությունը.

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)

Մենք օգտագործել ենք VM՝ 4 միջուկով և 16 ԳԲ RAM SSD-ով: JanusGraph-ը տեղակայվել է այս հրամանի միջոցով.

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

Այս դեպքում տվյալները և ինդեքսները, որոնք օգտագործվում են ճշգրիտ համընկնումների որոնումների համար, պահվում են BerkeleyDB-ում: Կատարելով ավելի վաղ տրված հարցումը՝ ես ստացա մի քանի տասնյակ վայրկյանի հավասար ժամանակ։

Զուգահեռաբար գործարկելով վերը նշված 4 սցենարները՝ ինձ հաջողվեց DBMS-ը վերածել դդմի՝ Java stacktraces-ի ուրախ հոսքով (և մենք բոլորս սիրում ենք կարդալ Java stacktraces) Docker տեղեկամատյաններում:

Որոշ մտածելուց հետո ես որոշեցի պարզեցնել գրաֆիկի դիագրամը հետևյալով.

Փորձ, որը ստուգում է JanusGraph գրաֆիկի DBMS-ի կիրառելիությունը՝ հարմար ուղիներ գտնելու խնդրի լուծման համար

Որոշել, որ ըստ էության հատկանիշների որոնումն ավելի արագ կլինի, քան եզրերով որոնումը: Արդյունքում իմ խնդրանքը վերածվեց հետևյալի.

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

Ռուսերենում ինչն է նման բան՝ գտեք ZoneStep ID=0-ով, ոտք դրեք առանց հետ գնալու, մինչև գտնեք ZoneStep ID=19-ով, հաշվեք նման շղթաների քանակը։

Ես նաև պարզեցրել եմ վերևում տրված բեռնման սցենարը, որպեսզի ավելորդ կապեր չստեղծեմ՝ սահմանափակվելով ատրիբուտներով։

Հարցումը դեռ մի քանի վայրկյան տևեց, ինչը լիովին անընդունելի էր մեր առաջադրանքի համար, քանի որ այն բոլորովին հարմար չէր որևէ տեսակի AdHoc հարցումների նպատակների համար:

Ես փորձեցի տեղակայել JanusGraph-ը՝ օգտագործելով Scylla-ն որպես Cassandra-ի ամենաարագ իրականացումը, բայց դա նույնպես չի հանգեցրել կատարողականի որևէ էական փոփոխության:

Այսպիսով, չնայած այն հանգամանքին, որ «այն կարծես գրաֆիկ է», ես չկարողացա ստանալ գրաֆիկի DBMS այն արագ մշակելու համար: Ես լիովին ենթադրում եմ, որ կա մի բան, որը ես չգիտեմ, և որ JanusGraph-ին կարելի է ստիպել այս որոնումը կատարել վայրկյանի մի մասում, սակայն ես չկարողացա դա անել:

Քանի որ խնդիրը դեռ պետք է լուծվեր, ես սկսեցի մտածել աղյուսակների JOIN-ների և Pivots-ների մասին, որոնք էլեգանտության առումով լավատեսություն չէին ներշնչում, բայց գործնականում կարող էին լիովին կիրառելի տարբերակ լինել։

Մեր նախագիծն արդեն օգտագործում է Apache ClickHouse-ը, ուստի ես որոշեցի փորձարկել իմ հետազոտությունը այս վերլուծական DBMS-ի վրա:

Տեղադրեց ClickHouse-ը պարզ բաղադրատոմսի միջոցով.

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

Ես ստեղծեցի տվյալների բազա և դրա մեջ աղյուսակ հետևյալ կերպ.

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

Ես այն լրացրեցի տվյալներով՝ օգտագործելով հետևյալ սցենարը.

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
    )

Քանի որ ներդիրները գալիս են խմբաքանակներով, լիցքավորումը շատ ավելի արագ էր, քան JanusGraph-ի համար:

Ստեղծել է երկու հարցում՝ օգտագործելով JOIN: A կետից 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

3 կետով անցնելու համար.

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

Հարցումները, իհարկե, բավականին սարսափելի տեսք ունեն, իրական օգտագործման համար անհրաժեշտ է ստեղծել ծրագրային ապահովման գեներատորի զրահ: Այնուամենայնիվ, նրանք աշխատում են և արագ են աշխատում: Ե՛վ առաջին, և՛ երկրորդ հարցումները կատարվում են 0.1 վայրկյանից պակաս ժամանակում: Ահա 3 կետով անցնող count(*) հարցման կատարման ժամանակի օրինակ.

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

Նշում IOPS-ի մասին. Տվյալները համալրելիս JanusGraph-ը ստեղծեց բավականին մեծ թվով IOPS (1000-1300 չորս տվյալների պոպուլյացիայի թելերի համար) և IOWAIT-ը բավականին բարձր էր: Միևնույն ժամանակ, ClickHouse-ը նվազագույն բեռ է առաջացրել սկավառակի ենթահամակարգի վրա:

Ամփոփում

Մենք որոշեցինք օգտագործել ClickHouse-ը այս տեսակի հարցումները սպասարկելու համար: Մենք միշտ կարող ենք լրացուցիչ օպտիմիզացնել հարցումները՝ օգտագործելով նյութականացված դիտումները և զուգահեռացումը՝ նախապես մշակելով իրադարձությունների հոսքը Apache Flink-ի միջոցով՝ նախքան դրանք ClickHouse-ում բեռնելը:

Կատարումը այնքան լավն է, որ մենք, հավանաբար, նույնիսկ ստիպված չենք լինի մտածել աղյուսակների ծրագրային պտտման մասին: Նախկինում մենք պետք է կատարեինք Vertica-ից վերցված տվյալների առանցքը Apache Parquet-ում վերբեռնման միջոցով:

Ցավոք, գրաֆիկական DBMS-ի օգտագործման հերթական փորձն անհաջող էր: Ես չգտայ, որ JanusGraph-ն ունի ընկերական էկոհամակարգ, որը հեշտացնում է արտադրանքի հետ արագության հասնելը: Միևնույն ժամանակ, սերվերը կարգավորելու համար օգտագործվում է Java-ի ավանդական եղանակը, որը կստիպի 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 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}

Ինձ հաջողվեց պատահաբար «դնել» JanusGraph-ի BerkeleyDB տարբերակը։

Փաստաթղթերը բավականին խեղաթյուրված են ինդեքսների առումով, քանի որ ինդեքսների կառավարումը պահանջում է, որ դուք կատարեք որոշակի տարօրինակ շամանիզմ Groovy-ում: Օրինակ, ինդեքսի ստեղծումը պետք է արվի Գրեմլինի կոնսոլում կոդ գրելով (ինչն, ի դեպ, արկղից դուրս չի աշխատում): 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()

Հետո

Ինչ-որ իմաստով վերը նշված փորձը համեմատություն է ջերմի և փափուկի միջև: Եթե ​​մտածեք դրա մասին, ապա գրաֆիկական DBMS-ը կատարում է այլ գործողություններ՝ նույն արդյունքները ստանալու համար: Այնուամենայնիվ, որպես թեստերի մաս, ես նաև փորձ կատարեցի հետևյալ խնդրանքով.

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

որն արտացոլում է քայլելու հեռավորությունը: Այնուամենայնիվ, նույնիսկ նման տվյալների վրա գրաֆիկական DBMS-ը ցույց տվեց արդյունքներ, որոնք գերազանցում էին մի քանի վայրկյանը... Սա, իհարկե, պայմանավորված է նրանով, որ կային նման ուղիներ. 0 -> X -> Y ... -> 1, որը ստուգել է նաև գրաֆիկի շարժիչը։

Նույնիսկ այնպիսի հարցման համար, ինչպիսին է.

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

Ես չկարողացա ստանալ արդյունավետ պատասխան մեկ վայրկյանից պակաս մշակման ժամանակով:

Պատմության բարոյականությունն այն է, որ գեղեցիկ գաղափարը և պարադիգմատիկ մոդելավորումը չեն հանգեցնում ցանկալի արդյունքի, ինչը ցույց է տրվում շատ ավելի բարձր արդյունավետությամբ՝ օգտագործելով ClickHouse-ի օրինակը: Այս հոդվածում ներկայացված օգտագործման դեպքը հստակ հակաօրինաչափություն է գրաֆիկական DBMS-ների համար, չնայած թվում է, որ այն հարմար է դրանց պարադիգմում մոդելավորման համար:

Source: www.habr.com

Добавить комментарий