JanusGraph DBMS grafikoaren aplikagarritasuna probatzen duen esperimentu bat bide egokiak aurkitzeko arazoa konpontzeko.

JanusGraph DBMS grafikoaren aplikagarritasuna probatzen duen esperimentu bat bide egokiak aurkitzeko arazoa konpontzeko.

Kaixo guztioi. Lineaz kanpoko trafikoa aztertzeko produktu bat garatzen ari gara. Proiektuak eskualdeetako bisitarien ibilbideen analisi estatistikoarekin lotutako zeregina du.

Zeregin honen barruan, erabiltzaileek sistema mota honetako kontsultak egin ditzakete:

  • zenbat bisitari igaro diren "A" eremutik "B" eremura;
  • zenbat bisitari igaro diren "A" eremutik "B" eremura "C" eremutik eta gero "D" eremutik;
  • zenbat denbora behar izan duen bisitari mota jakin batek "A" eremutik "B" eremura joateko.

eta antzeko hainbat kontsulta analitiko.

Bisitariaren mugimendua eremuetan zehar zuzendutako grafiko bat da. Internet irakurri ondoren, DBMS grafikoak txosten analitikoetarako ere erabiltzen direla aurkitu nuen. DBMS grafikoek halako kontsultei nola aurre egingo zien ikusteko gogoa nuen (TL: DR; gaizki).

DBMS erabiltzea aukeratu nuen JanusGraph, kode irekiko DBMS grafikoaren ordezkari nabarmen gisa, teknologia helduen pila batean oinarritzen dena, eta horrek (nire ustez) ezaugarri operatibo duinak eman beharko lioke:

  • BerkeleyDB biltegiratze backend, Apache Cassandra, Scylla;
  • indize konplexuak Lucene, Elasticsearch, Solr-en gorde daitezke.

JanusGraph-en egileek OLTP zein OLAPerako egokia dela idazten dute.

BerkeleyDB, Apache Cassandra, Scylla eta ES-ekin lan egin dut, eta produktu hauek maiz erabiltzen dira gure sistemetan, beraz, baikor nengoen DBMS grafiko hau probatzeko. Bitxia iruditu zait BerkeleyDB aukeratzea RocksDB baino gehiago, baina hori ziurrenik transakzio-eskakizunengatik da. Nolanahi ere, produktu eskalagarrian erabiltzeko, Cassandra edo Scylla-n backend bat erabiltzea gomendatzen da.

Ez nuen Neo4j kontuan hartu clustering-ak bertsio komertziala behar duelako, hau da, produktua ez da kode irekikoa.

Grafiko DBMSek esaten dute: "Grafiko baten itxura badu, trata ezazu grafiko bat bezala!" - edertasuna!

Lehenik eta behin, grafiko bat marraztu nuen, DBMS grafikoen kanonen arabera egin zena:

JanusGraph DBMS grafikoaren aplikagarritasuna probatzen duen esperimentu bat bide egokiak aurkitzeko arazoa konpontzeko.

Esentzia bat dago Zone, arloko arduraduna. Bada ZoneStep honi dagokio Zone, orduan aipatzen du. Funtsean Area, ZoneTrack, Person Ez ezazu kasurik egin, domeinukoak dira eta ez dira probaren partetzat hartzen. Guztira, honelako grafiko-egitura baterako kate bilaketa-kontsulta itxura izango litzateke:

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

Errusieraz honelako zerbait da: aurkitu ID=0 duen Zona bat, hartu ertz bat bertara doan erpin guztiak (ZoneStep), zapaldu atzera egin gabe, Zonarako ertz bat duten ZoneSteps horiek aurkitu arte. ID=19, zenbatu horrelako kate kopurua.

Ez dut grafikoetan bilaketaren konplexutasun guztiak ezagutzen ditudanik, baina kontsulta hau liburu honetan oinarrituta sortu da (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

50 eta 3 puntu arteko luzera duten 20 mila pista kargatu nituen JanusGraph datu-base grafiko batean BerkeleyDB backend-a erabiliz, indizeen arabera sortu. lidergoa.

Python deskargatzeko gidoia:


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)

SSD batean 4 nukleo eta 16 GB RAM dituen VM bat erabili dugu. JanusGraph komando hau erabiliz zabaldu zen:

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

Kasu honetan, bat-etortze zehatzak bilatzeko erabiltzen diren datuak eta indizeak BerkeleyDB-n gordetzen dira. Lehen emandako eskaera gauzatu ondoren, hainbat hamarna segundoren denbora jaso nuen.

Goiko 4 scriptak paraleloan exekutatuta, DBMS kalabaza bihurtzea lortu nuen Java stacktraces jario alai batekin (eta denoi gustatzen zaigu Java stacktraces irakurtzea) Docker erregistroetan.

Pentsatu ondoren, grafikoaren diagrama honela sinplifikatzea erabaki nuen:

JanusGraph DBMS grafikoaren aplikagarritasuna probatzen duen esperimentu bat bide egokiak aurkitzeko arazoa konpontzeko.

Entitate-atributuen arabera bilatzea ertzen bidez egitea baino azkarragoa izango litzatekeela erabakitzea. Ondorioz, nire eskaera honako hau bihurtu zen:

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

Errusieraz honelako zerbait da: aurkitu ZoneStep ID=0rekin, zapaldu atzera egin gabe ZoneStep ID=19rekin aurkitu arte, zenbatu horrelako kate kopurua.

Goian emandako kargatzeko gidoia ere sinplifikatu nuen alferrikako konexioak ez sortzeko, atributuetara mugatuz.

Eskaera oraindik hainbat segundo behar izan zen betetzeko, eta hori guztiz onartezina zen gure zereginerako, ez baitzen batere egokia AdHoc edozein motatako eskaerak egiteko.

JanusGraph inplementatzen saiatu nintzen Scylla erabiliz Cassandra inplementazio azkarren gisa, baina honek ere ez zuen errendimendu aldaketa garrantzitsurik ekarri.

Beraz, "grafiko bat dirudi" izan arren, ezin izan nuen DBMS grafikoa azkar prozesatu. Ezagutzen ez dudan zerbait dagoela eta JanusGraph bilaketa hori segundo zati batean egin daitekeela suposatzen dut, hala ere, ezin izan nuen egin.

Arazoa oraindik konpondu behar zenez, JOIN eta Pivot taulak pentsatzen hasi nintzen, dotoreziari dagokionez baikortasunik sortzen ez zutenak, baina praktikan guztiz bideragarria izan zitekeen aukera.

Gure proiektuak Apache ClickHouse erabiltzen du dagoeneko, beraz, nire ikerketa DBMS analitiko honetan probatzea erabaki nuen.

ClickHouse zabaldu da errezeta sinple bat erabiliz:

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

Datu-base bat eta taula bat sortu nuen bertan honela:

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

Datuekin bete nuen script hau erabiliz:

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
    )

Txertaketak loteka etortzen direnez, betetzea JanusGraph-en baino askoz azkarragoa izan zen.

Bi kontsulta eraiki JOIN erabiliz. A puntutik B puntura joateko:

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 puntu pasatzeko:

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

Eskaerek, noski, nahiko beldurgarriak dirudite; benetako erabilerarako, software-sorgailuaren arnesa sortu behar duzu. Hala ere, lan egiten dute eta azkar lan egiten dute. Lehenengo eta bigarren eskaerak 0.1 segundo baino gutxiagoan betetzen dira. Hona hemen count(*) 3 puntutik igarotzen den kontsultaren exekuzio denboraren adibide bat:

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

IOPSari buruzko ohar bat. Datuak populatzean, JanusGraph-ek IOPS kopuru nahiko altua sortu zuen (1000-1300 datu-populazio-harietarako) eta IOWAIT nahiko altua izan zen. Aldi berean, ClickHousek karga minimoa sortu zuen disko azpisisteman.

Ondorioa

Mota honetako eskaerak egiteko ClickHouse erabiltzea erabaki genuen. Beti optimizatu ditzakegu kontsultak ikuspegi materializatuak eta paralelizazioa erabiliz, gertaeren korrontea Apache Flink erabiliz aldez aurretik prozesatuz ClickHousen kargatu aurretik.

Errendimendua hain da ona, ezen seguruenik ez dugu taulak programatikoki pibotatzea pentsatu beharko. Aurretik, Verticatik ateratako datuen piboteak egin behar izan genituen Apache Parquet-era kargatuz.

Zoritxarrez, DBMS grafiko bat erabiltzeko beste saiakera batek ez zuen arrakastarik izan. Ez nuen aurkitu JanusGraph-ek produktuaren berri izatea errazten duen ekosistema atsegina duenik. Aldi berean, zerbitzaria konfiguratzeko, Java modu tradizionala erabiltzen da, eta horrek Java ezagutzen ez duen jendea odol malkoak negar egingo du:

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}

Ustekabean JanusGraph-en BerkeleyDB bertsioa "jartzea" lortu nuen.

Dokumentazioa nahiko okerra da indizeei dagokienez, indizeak kudeatzeak Groovy-n xamanismo arraro bat egitea eskatzen baitu. Adibidez, indize bat sortzea Gremlin kontsolan kodea idatziz egin behar da (horrek, bide batez, ez du kaxatik kanpo funtzionatzen). JanusGraph dokumentazio ofizialetik:

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

Zentzu batean, goiko esperimentua bero eta bigunen arteko konparazioa da. Pentsatzen baduzu, DBMS grafiko batek beste eragiketa batzuk egiten ditu emaitza berdinak lortzeko. Hala ere, proben barruan, esperimentu bat ere egin nuen eskaera batekin:

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

oinezko distantzia islatzen duena. Hala ere, halako datuetan ere, DBMS grafikoak segundo batzuetatik haratago joan ziren emaitzak erakusten zituen... Hori, noski, bezalako bideak egon zirelako da. 0 -> X -> Y ... -> 1, grafiko-motorrak ere egiaztatu zuena.

Nahiz eta honelako kontsulta baterako:

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

Ezin izan dut erantzun produktiborik lortu segundo bat baino gutxiagoko prozesatzeko denborarekin.

Istorioaren morala da ideia eder batek eta modelaketa paradigmatikoak ez dutela nahi den emaitza ekartzen, eta hori askoz eraginkortasun handiagoarekin frogatzen da ClickHouseren adibidea erabiliz. Artikulu honetan aurkezten den erabilera kasua DBMS grafikoen aurkako eredu argia da, nahiz eta haien paradigman modelatzeko egokia dirudien.

Iturria: www.habr.com

Gehitu iruzkin berria