Eksperiment kojim se testira primjenjivost JanusGraph graph DBMS-a za rješavanje problema pronalaženja odgovarajućih staza

Eksperiment kojim se testira primjenjivost JanusGraph graph DBMS-a za rješavanje problema pronalaženja odgovarajućih staza

Bok svima. Razvijamo proizvod za offline analizu prometa. Projekt ima zadatak koji se odnosi na statističku analizu ruta posjetitelja po regijama.

Kao dio ovog zadatka, korisnici mogu postavljati sustavu upite sljedeće vrste:

  • koliko je posjetitelja prešlo iz područja "A" u područje "B";
  • koliko je posjetitelja prošlo iz područja "A" u područje "B" kroz područje "C", a zatim kroz područje "D";
  • koliko je određenoj vrsti posjetitelja trebalo da putuje od područja "A" do područja "B".

i niz sličnih analitičkih upita.

Kretanje posjetitelja kroz područja je usmjereni grafikon. Nakon čitanja interneta otkrio sam da se DBMS-ovi s grafovima također koriste za analitička izvješća. Imao sam želju vidjeti kako će se grafički DBMS-ovi nositi s takvim upitima (TL DR; slabo).

Odlučio sam koristiti DBMS JanusGraph, kao izvanredan predstavnik graph open-source DBMS-a, koji se oslanja na hrpu zrelih tehnologija, koje bi mu (po mom mišljenju) trebale pružiti pristojne operativne karakteristike:

  • BerkeleyDB pozadina za pohranu, Apache Cassandra, Scylla;
  • složeni indeksi mogu se pohraniti u Lucene, Elasticsearch, Solr.

Autori JanusGrapha pišu da je pogodan i za OLTP i za OLAP.

Radio sam s BerkeleyDB, Apache Cassandra, Scylla i ES, i ti se proizvodi često koriste u našim sustavima, tako da sam bio optimističan u vezi s testiranjem ovog grafičkog DBMS-a. Bilo mi je čudno odabrati BerkeleyDB umjesto RocksDB-a, ali to je vjerojatno zbog zahtjeva transakcije. U svakom slučaju, za skalabilnu upotrebu proizvoda, predlaže se korištenje pozadine na Cassandri ili Scylli.

Neo4j nisam razmatrao jer klasteriranje zahtijeva komercijalnu verziju, odnosno proizvod nije otvorenog koda.

Grafovi DBMS-ovi kažu: "Ako izgleda kao grafikon, tretirajte ga kao grafikon!" - ljepota!

Prvo sam nacrtao graf koji je napravljen točno prema kanonima DBMS-a za grafove:

Eksperiment kojim se testira primjenjivost JanusGraph graph DBMS-a za rješavanje problema pronalaženja odgovarajućih staza

Postoji suština Zone, odgovoran za područje. Ako ZoneStep pripada ovome Zone, onda se poziva na to. O suštini Area, ZoneTrack, Person Ne obraćajte pažnju, pripadaju domeni i ne smatraju se dijelom testa. Sveukupno, upit lančanog pretraživanja za takvu strukturu grafikona izgledao bi ovako:

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

Ono što je na ruskom otprilike ovako: pronađite zonu s ID=0, uzmite sve vrhove od kojih rub ide do nje (ZoneStep), gazite bez vraćanja dok ne pronađete one ZoneSteps od kojih postoji rub do Zone s ID=19, izbrojite broj takvih lanaca.

Ne pretvaram se da znam sve zamršenosti pretraživanja grafikona, ali ovaj je upit generiran na temelju ove knjige (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Učitao sam 50 tisuća staza u rasponu od 3 do 20 točaka u bazu podataka grafova JanusGraph koristeći pozadinu BerkeleyDB, kreirao indekse prema rukovodstvo.

Python skripta za preuzimanje:


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)

Koristili smo VM s 4 jezgre i 16 GB RAM-a na SSD-u. JanusGraph je implementiran pomoću ove naredbe:

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

U ovom slučaju, podaci i indeksi koji se koriste za pretraživanje točnog podudaranja pohranjuju se u BerkeleyDB. Nakon što sam izvršio prethodno dani zahtjev, dobio sam vrijeme od nekoliko desetaka sekundi.

Pokretanjem 4 gornje skripte paralelno, uspio sam DBMS pretvoriti u bundevu s veselim nizom praćenja Java stacktracea (a svi volimo čitati Java stacktraceove) u zapisima Dockera.

Nakon malo razmišljanja, odlučio sam pojednostaviti dijagram grafikona na sljedeće:

Eksperiment kojim se testira primjenjivost JanusGraph graph DBMS-a za rješavanje problema pronalaženja odgovarajućih staza

Odlučujući da će pretraživanje po atributima entiteta biti brže od pretraživanja po rubovima. Kao rezultat toga, moj zahtjev se pretvorio u sljedeće:

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

Ono što je na ruskom otprilike ovako: pronađite ZoneStep s ID=0, gazite bez vraćanja dok ne pronađete ZoneStep s ID=19, izbrojite broj takvih lanaca.

Također sam pojednostavio gore navedenu skriptu za učitavanje kako ne bih stvarao nepotrebne veze, ograničavajući se na atribute.

Zahtjev je ipak trebao nekoliko sekundi da se izvrši, što je bilo potpuno neprihvatljivo za naš zadatak, jer uopće nije bilo prikladno za potrebe AdHoc zahtjeva bilo koje vrste.

Pokušao sam implementirati JanusGraph koristeći Scyllu kao najbržu implementaciju Cassandre, ali ni to nije dovelo do značajnih promjena performansi.

Dakle, unatoč činjenici da "izgleda kao grafikon", nisam mogao natjerati DBMS da ga brzo obradi. U potpunosti pretpostavljam da postoji nešto što ne znam i da se JanusGraph može natjerati da izvrši ovu pretragu u djeliću sekunde, međutim, ja to nisam uspio.

Budući da je problem tek trebalo riješiti, počeo sam razmišljati o JOIN-ovima i Pivotima tablica, koji nisu ulijevali optimizam u smislu elegancije, ali bi u praksi mogli biti sasvim izvediva opcija.

Naš projekt već koristi Apache ClickHouse, pa sam odlučio testirati svoje istraživanje na ovom analitičkom DBMS-u.

Postavio ClickHouse koristeći jednostavan recept:

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

Napravio sam bazu podataka i tablicu u njoj ovako:

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

Ispunio sam ga podacima pomoću sljedeće skripte:

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
    )

Budući da umeci dolaze u serijama, punjenje je bilo mnogo brže nego za JanusGraph.

Konstruirana su dva upita pomoću JOIN-a. Za prelazak iz točke A u točku 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

Da biste prošli kroz 3 točke:

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

Zahtjevi, naravno, izgledaju prilično zastrašujuće; za stvarnu upotrebu morate stvoriti programski generatorski snop. Međutim, oni rade i rade brzo. I prvi i drugi zahtjev izvršavaju se za manje od 0.1 sekunde. Ovdje je primjer vremena izvršenja upita za count(*) koji prolazi kroz 3 točke:

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

Napomena o IOPS-u. Prilikom popunjavanja podataka, JanusGraph je generirao prilično velik broj IOPS-a (1000-1300 za četiri niti popunjavanja podataka), a IOWAIT je bio prilično visok. Pritom je ClickHouse generirao minimalno opterećenje diskovnog podsustava.

Zaključak

Odlučili smo koristiti ClickHouse za servisiranje ove vrste zahtjeva. Uvijek možemo dodatno optimizirati upite korištenjem materijaliziranih prikaza i paralelizacije prethodnom obradom toka događaja pomoću Apache Flinka prije njihovog učitavanja u ClickHouse.

Izvedba je toliko dobra da vjerojatno nećemo morati razmišljati o programskom okretanju tablica. Prethodno smo morali raditi pivote podataka dohvaćenih s Vertice putem prijenosa u Apache Parquet.

Nažalost, drugi pokušaj korištenja grafičkog DBMS-a bio je neuspješan. Nisam smatrao da JanusGraph ima prijateljski ekosustav koji je olakšao upoznavanje s proizvodom. U isto vrijeme, za konfiguraciju poslužitelja koristi se tradicionalni Java način, koji će ljude koji nisu upoznati s Javom natjerati na krvave suze:

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}

Uspio sam slučajno "staviti" BerkeleyDB verziju JanusGrapha.

Dokumentacija je prilično zakrivljena u smislu indeksa, budući da upravljanje indeksima zahtijeva da izvodite neke prilično čudne šamanističke radnje u Groovyju. Na primjer, stvaranje indeksa mora se izvršiti pisanjem koda u Gremlin konzoli (koja, usput rečeno, ne radi odmah). Iz službene JanusGraph dokumentacije:

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

pogovor

U određenom smislu, gornji eksperiment je usporedba između toplog i mekog. Ako razmislite o tome, grafički DBMS izvodi druge operacije kako bi dobio iste rezultate. Međutim, kao dio testova, također sam proveo eksperiment sa zahtjevom poput:

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

koji odražava udaljenost hoda. Međutim, čak i na takvim podacima, graf DBMS pokazao je rezultate koji su išli dalje od nekoliko sekundi... To je, naravno, zbog činjenice da su postojali putovi poput 0 -> X -> Y ... -> 1, što je graph engine također provjerio.

Čak i za upit poput:

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

Nisam uspio dobiti produktivan odgovor s vremenom obrade kraćim od sekunde.

Pouka priče je da lijepa ideja i paradigmatsko modeliranje ne dovode do željenog rezultata, što se na primjeru ClickHouse-a pokazuje s puno većom učinkovitošću. Slučaj upotrebe predstavljen u ovom članku jasan je anti-uzorak za grafičke DBMS-ove, iako se čini prikladnim za modeliranje u njihovoj paradigmi.

Izvor: www.habr.com

Dodajte komentar