Usa ka eksperimento nga nagsulay sa paggamit sa JanusGraph graph DBMS alang sa pagsulbad sa problema sa pagpangita sa angay nga mga agianan

Usa ka eksperimento nga nagsulay sa paggamit sa JanusGraph graph DBMS alang sa pagsulbad sa problema sa pagpangita sa angay nga mga agianan

Kumusta tanan. Naghimo kami usa ka produkto alang sa pag-analisar sa trapiko sa offline. Ang proyekto adunay usa ka buluhaton nga may kalabutan sa istatistikal nga pagtuki sa mga ruta sa mga bisita sa mga rehiyon.

Isip bahin niini nga buluhaton, ang mga tiggamit makapangutana sa mga pangutana sa sistema sa mosunod nga matang:

  • pila ka mga bisita ang milabay gikan sa lugar nga "A" hangtod sa lugar nga "B";
  • pila ka mga bisita ang milabay gikan sa lugar nga "A" hangtod sa lugar "B" hangtod sa lugar nga "C" ug dayon sa lugar nga "D";
  • unsa kadugay ang pagbiyahe sa usa ka matang sa bisita gikan sa lugar nga "A" ngadto sa lugar nga "B".

ug ubay-ubay nga susamang analytical nga mga pangutana.

Ang lihok sa bisita tabok sa mga lugar kay usa ka direkta nga graph. Human sa pagbasa sa Internet, akong nadiskobrehan nga ang graph DBMSs gigamit usab alang sa analytical nga mga taho. Ako adunay tinguha sa pagtan-aw kon sa unsang paagi graph DBMSs makasagubang sa maong mga pangutana (TL; DR; dili maayo).

Gipili nako nga gamiton ang DBMS JanusGraph, isip usa ka talagsaon nga representante sa graph nga open-source nga DBMS, nga nagsalig sa usa ka stack sa hamtong nga mga teknolohiya, nga (sa akong opinyon) kinahanglan nga maghatag niini og desente nga mga kinaiya sa operasyon:

  • BerkeleyDB storage backend, Apache Cassandra, Scylla;
  • Ang mga komplikadong indeks mahimong tipigan sa Lucene, Elasticsearch, Solr.

Ang mga tagsulat sa JanusGraph nagsulat nga kini angay alang sa OLTP ug OLAP.

Nakatrabaho ko sa BerkeleyDB, Apache Cassandra, Scylla ug ES, ug kini nga mga produkto kanunay nga gigamit sa among mga sistema, busa malaumon ako bahin sa pagsulay sa kini nga graph nga DBMS. Nakita nako nga katingad-an ang pagpili sa BerkeleyDB kaysa sa RocksDB, apan kana tingali tungod sa mga kinahanglanon sa transaksyon. Sa bisan unsang kaso, alang sa scalable, paggamit sa produkto, gisugyot nga gamiton ang backend sa Cassandra o Scylla.

Wala nako gikonsiderar ang Neo4j tungod kay ang clustering nanginahanglan usa ka komersyal nga bersyon, nga mao, ang produkto dili bukas nga gigikanan.

Ang Graph DBMSs nag-ingon: "Kung kini tan-awon sama sa usa ka graph, tagda kini sama sa usa ka graph!" - katahom!

Una, nagdrowing ko og graph, nga gihimo sumala sa mga canon sa graph DBMSs:

Usa ka eksperimento nga nagsulay sa paggamit sa JanusGraph graph DBMS alang sa pagsulbad sa problema sa pagpangita sa angay nga mga agianan

Adunay usa ka diwa Zone, responsable sa lugar. Kung ZoneStep nahisakop niini Zone, unya iyang gipasabot kini. Sa esensya Area, ZoneTrack, Person Ayaw pagtagad, sila nahisakop sa domain ug wala isipa nga bahin sa pagsulay. Sa kinatibuk-an, ang usa ka pangutana sa pagpangita sa kadena alang sa ingon nga istruktura sa graph sama sa:

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

Unsa sa Russian ang usa ka butang nga sama niini: pangitaa ang usa ka Zone nga adunay ID = 0, kuhaa ang tanan nga mga vertices diin ang usa ka ngilit moadto niini (ZoneStep), pag-stomp nga dili na mobalik hangtod makita nimo ang mga ZoneSteps diin adunay sulud sa Zone nga adunay ID=19, ihap ang gidaghanon sa maong mga kadena.

Wala ako magpakaaron-ingnon nga nahibal-an ang tanan nga mga kakuti sa pagpangita sa mga graph, apan kini nga pangutana gihimo base sa kini nga libro (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Nag-load ko og 50 ka libo nga mga track gikan sa 3 ngadto sa 20 puntos ang gitas-on ngadto sa JanusGraph graph database gamit ang BerkeleyDB backend, naghimo og mga indeks sumala sa pagpangulo.

script sa pag-download sa 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)

Gigamit namon ang usa ka VM nga adunay 4 nga mga cores ug 16 GB RAM sa usa ka SSD. Gi-deploy ang JanusGraph gamit kini nga sugo:

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

Sa kini nga kaso, ang mga datos ug mga indeks nga gigamit alang sa eksaktong pagpares nga pagpangita gitipigan sa BerkeleyDB. Human matuman ang hangyo nga gihatag sa sayo pa, nakadawat ako usa ka oras nga katumbas sa pila ka napulo ka segundo.

Pinaagi sa pagpadagan sa 4 sa ibabaw nga mga script nga managsama, nakahimo ako sa paghimo sa DBMS nga usa ka kalabasa nga adunay usa ka malipayong sapa sa Java stacktraces (ug kitang tanan ganahan nga mobasa sa Java stacktraces) sa Docker logs.

Human sa pipila ka paghunahuna, nakahukom ko nga pasimplehon ang graph diagram ngadto sa mosunod:

Usa ka eksperimento nga nagsulay sa paggamit sa JanusGraph graph DBMS alang sa pagsulbad sa problema sa pagpangita sa angay nga mga agianan

Ang pagdesisyon nga ang pagpangita pinaagi sa mga attribute sa entity mas paspas kaysa pagpangita pinaagi sa mga kilid. Ingon nga resulta, ang akong hangyo nahimong mosunod:

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

Unsa sa Russian ang usa ka butang nga sama niini: pangitaa ang ZoneStep nga adunay ID = 0, stomp nga dili na mobalik hangtod makita nimo ang ZoneStep nga adunay ID = 19, ihap ang gidaghanon sa ingon nga mga kadena.

Gipasimple usab nako ang loading script nga gihatag sa ibabaw aron dili makamugna og dili kinahanglan nga mga koneksyon, nga gilimitahan ang akong kaugalingon sa mga hiyas.

Ang hangyo nagkinahanglan gihapon og pipila ka mga segundo aron makompleto, nga hingpit nga dili madawat alang sa among buluhaton, tungod kay kini dili gayud angay alang sa mga katuyoan sa AdHoc nga mga hangyo sa bisan unsa nga matang.

Gisulayan nako ang pag-deploy sa JanusGraph gamit ang Scylla ingon ang labing paspas nga pagpatuman sa Cassandra, apan wala usab kini hinungdan sa bisan unsang hinungdanon nga pagbag-o sa pasundayag.

Busa bisan pa sa kamatuoran nga "kini morag usa ka graph", dili nako makuha ang graph nga DBMS aron maproseso kini dayon. Ako hingpit nga nagtuo nga adunay usa ka butang nga wala nako nahibal-an ug nga ang JanusGraph mahimo nga himuon kini nga pagpangita sa usa ka tipik sa usa ka segundo, bisan pa, wala nako mahimo.

Tungod kay ang problema kinahanglan pa nga masulbad, nagsugod ako sa paghunahuna bahin sa JOINs ug Pivots sa mga lamesa, nga wala magdasig sa pagkamalaumon sa mga termino sa kaanindot, apan mahimo nga usa ka hingpit nga magamit nga kapilian sa praktis.

Gigamit na sa among proyekto ang Apache ClickHouse, mao nga nakahukom ko nga sulayan ang akong panukiduki bahin niining analytical DBMS.

Gi-deploy ang ClickHouse gamit ang usa ka yano nga resipe:

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

Naghimo ako usa ka database ug usa ka lamesa nga sama niini:

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

Gipuno nako kini sa datos gamit ang mosunod nga script:

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
    )

Tungod kay ang mga pagsal-ot moabut sa mga batch, ang pagpuno mas paspas kaysa sa JanusGraph.

Naghimo ug duha ka pangutana gamit ang JOIN. Sa paglihok gikan sa punto A ngadto sa punto 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

Aron makaagi sa 3 puntos:

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

Ang mga hangyo, siyempre, tan-awon nga makahadlok; alang sa tinuud nga paggamit, kinahanglan nimo nga maghimo usa ka software generator harness. Bisan pa, sila nagtrabaho ug sila dali nga nagtrabaho. Ang una ug ikaduha nga mga hangyo nahuman sa wala’y 0.1 segundos. Ania ang usa ka pananglitan sa oras sa pagpatuman sa pangutana alang sa count(*) nga moagi sa 3 puntos:

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

Usa ka nota bahin sa IOPS. Kung nag-populate sa datos, ang JanusGraph nakamugna og medyo taas nga gidaghanon sa IOPS (1000-1300 alang sa upat ka mga thread sa populasyon sa datos) ug ang IOWAIT taas kaayo. Sa samang higayon, ang ClickHouse nakamugna og gamay nga load sa disk subsystem.

konklusyon

Kami nakahukom sa paggamit sa ClickHouse sa pag-alagad niini nga matang sa hangyo. Kanunay namong ma-optimize ang mga pangutana gamit ang materialized view ug parallelization pinaagi sa pre-processing sa event stream gamit ang Apache Flink sa dili pa kini i-load sa ClickHouse.

Ang pasundayag maayo kaayo nga tingali dili na kita maghunahuna bahin sa pag-pivot sa mga lamesa sa programmatically. Kaniadto, kinahanglan namon nga buhaton ang mga pivots sa datos nga nakuha gikan sa Vertica pinaagi sa pag-upload sa Apache Parquet.

Ikasubo, ang laing pagsulay sa paggamit sa usa ka graph nga DBMS wala molampos. Wala nako nakit-an ang JanusGraph nga adunay usa ka mahigalaon nga ekosistema nga nagpasayon ​​​​sa pagkuha sa paspas sa produkto. Sa parehas nga oras, aron ma-configure ang server, gigamit ang tradisyonal nga paagi sa Java, nga maghimo sa mga tawo nga dili pamilyar sa Java nga maghilak sa mga luha sa dugo:

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}

Ako nakahimo sa aksidenteng "ibutang" ang BerkeleyDB nga bersyon sa JanusGraph.

Ang dokumentasyon medyo hiwi sa mga termino sa mga indeks, tungod kay ang pagdumala sa mga indeks nanginahanglan kanimo nga maghimo usa ka medyo katingad-an nga shamanismo sa Groovy. Pananglitan, ang paghimo sa usa ka indeks kinahanglan buhaton pinaagi sa pagsulat sa code sa console sa Gremlin (nga, sa tinuud, dili molihok sa gawas sa kahon). Gikan sa opisyal nga dokumentasyon sa 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()

Pagkahuman

Sa usa ka diwa, ang eksperimento sa ibabaw usa ka pagtandi tali sa init ug humok. Kung imong hunahunaon kini, ang usa ka graph nga DBMS nagpahigayon sa ubang mga operasyon aron makuha ang parehas nga mga resulta. Bisan pa, isip bahin sa mga pagsulay, nagpahigayon usab ako usa ka eksperimento sa usa ka hangyo sama sa:

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

nga nagpakita sa gilay-on sa paglakaw. Bisan pa, bisan sa ingon nga datos, ang graph nga DBMS nagpakita sa mga resulta nga lapas sa pipila ka segundo ... Kini, siyempre, tungod sa kamatuoran nga adunay mga agianan sama sa 0 -> X -> Y ... -> 1, nga gisusi usab sa graph engine.

Bisan alang sa usa ka pangutana sama sa:

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

Wala ako makakuha usa ka produktibo nga tubag nga adunay oras sa pagproseso nga wala’y usa ka segundo.

Ang moral sa istorya mao nga ang usa ka matahum nga ideya ug paradigmatic modeling dili mosangpot sa gitinguha nga resulta, nga gipakita uban sa mas taas nga efficiency sa paggamit sa panig-ingnan sa ClickHouse. Ang kaso sa paggamit nga gipresentar niini nga artikulo usa ka tin-aw nga anti-pattern para sa graph DBMSs, bisan kung kini angay alang sa pagmodelo sa ilang paradigm.

Source: www.habr.com

Idugang sa usa ka comment