Eksperiment testiranja primjenjivosti JanusGraph grafa DBMS-a za rješavanje problema pronalaženja odgovarajućih putanja

Eksperiment testiranja primjenjivosti JanusGraph grafa DBMS-a za rješavanje problema pronalaženja odgovarajućih putanja

Zdravo svima. Razvijamo proizvod za analizu offline prometa. Zadatak projekta je statistička analiza ruta posjetilaca po regijama.

Kao dio ovog zadatka, korisnici mogu postavljati sistemske upite sljedećeg tipa:

  • koliko je posjetilaca prešlo iz područja "A" u područje "B";
  • koliko je posjetilaca prošlo iz područja "A" u područje "B" kroz područje "C", a zatim kroz područje "D";
  • koliko je vremena bilo potrebno određenom tipu posjetioca da putuje od područja “A” do područja “B”.

i niz sličnih analitičkih upita.

Kretanje posjetitelja kroz područja je usmjereni graf. Nakon čitanja interneta, otkrio sam da se grafički DBMS-ovi koriste i za analitičke izvještaje. Imao sam želju da vidim kako će se grafički DBMS nositi sa takvim upitima (TL; DR; loše).

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

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

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

Radio sam sa BerkeleyDB, Apache Cassandrom, Scyllom i ES, i ovi proizvodi se često koriste u našim sistemima, tako da sam bio optimističan u pogledu testiranja ovog grafikona DBMS. Bilo mi je čudno izabrati BerkeleyDB u odnosu na RocksDB, ali to je vjerovatno zbog zahtjeva za transakcijama. U svakom slučaju, za skalabilno korištenje proizvoda, predlaže se korištenje backenda na Cassandra ili Scylla.

Nisam razmatrao Neo4j jer je za klasterisanje potrebna komercijalna verzija, odnosno proizvod nije otvorenog koda.

Grafički DBMS-ovi kažu: "Ako izgleda kao graf, tretirajte ga kao graf!" - ljepota!

Prvo sam nacrtao graf, koji je napravljen tačno po kanonima grafskih DBMS-a:

Eksperiment testiranja primjenjivosti JanusGraph grafa DBMS-a za rješavanje problema pronalaženja odgovarajućih putanja

Postoji suština Zone, odgovorna za područje. Ako ZoneStep pripada ovome Zone, onda se on poziva na to. O suštini Area, ZoneTrack, Person Ne obraćajte pažnju, oni pripadaju domeni i ne smatraju se dijelom testa. Ukupno, lančani upit za pretragu za takvu strukturu grafa bi izgledao 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 je otprilike ovako: pronađite zonu sa ID=0, uzmite sve vrhove iz kojih ivica ide do nje (ZoneStep), gazite bez vraćanja nazad dok ne pronađete one ZoneSteps od kojih postoji ivica u zonu sa ID=19, izbroji broj takvih lanaca.

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

Učitao sam 50 hiljada numera dužine od 3 do 20 tačaka u JanusGraph bazu podataka grafova koristeći pozadinu BerkeleyDB, kreirao indekse prema vodstvo.

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 sa 4 jezgra i 16 GB RAM-a na SSD-u. JanusGraph je raspoređen pomoću ove naredbe:

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

U ovom slučaju, podaci i indeksi koji se koriste za traženje tačnih podudaranja pohranjuju se u BerkeleyDB. Nakon što sam izvršio ranije dat zahtjev, dobio sam vrijeme od nekoliko desetina sekundi.

Pokretanjem 4 gornje skripte paralelno, uspio sam pretvoriti DBMS u bundevu sa veselim nizom Java stacktraces (a svi volimo čitati Java stacktraces) u Docker logovima.

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

Eksperiment testiranja primjenjivosti JanusGraph grafa DBMS-a za rješavanje problema pronalaženja odgovarajućih putanja

Odlučivanje da bi pretraživanje po atributima entiteta bilo 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 je otprilike ovako: pronađite ZoneStep sa ID=0, gazite bez vraćanja dok ne nađete ZoneStep sa ID=19, izbrojite broj takvih lanaca.

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

Zahtjev je i dalje trajao nekoliko sekundi, što je za naš zadatak bilo potpuno neprihvatljivo, 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čajnijih promjena u performansama.

Dakle, uprkos činjenici da "izgleda kao graf", nisam mogao da nateram DBMS da ga brzo obradi. U potpunosti pretpostavljam da nešto ne znam i moguće je natjerati JanusGraph da izvrši ovu pretragu u djeliću sekunde, međutim, ja to nisam uspio.

Pošto je problem još trebalo riješiti, počeo sam razmišljati o JOIN-ovima i Pivot-ovima tablica, što nije ulijevalo optimizam u smislu elegancije, ali bi moglo biti potpuno izvodljiva opcija u praksi.

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

Postavljen ClickHouse pomoću jednostavnog recepta:

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 tabelu 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

Popunio sam ga podacima koristeći sljedeću skriptu:

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
    )

Pošto umetci dolaze u serijama, punjenje je bilo mnogo brže nego za JanusGraph.

Napravljena su dva upita koristeći JOIN. Za prelazak od tačke A do tačke 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 tač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, potrebno je kreirati svežanj softverskog generatora. Međutim, rade i rade brzo. I prvi i drugi zahtjev se završavaju za manje od 0.1 sekunde. Evo primjera vremena izvršenja upita za count(*) koji prolazi kroz 3 tač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 visok broj IOPS (1000-1300 za četiri niti populacije podataka) i IOWAIT je bio prilično visok. U isto vrijeme, ClickHouse je generirao minimalno opterećenje na podsistemu diska.

zaključak

Odlučili smo koristiti ClickHouse za servisiranje ove vrste zahtjeva. Uvijek možemo dodatno optimizirati upite korištenjem materijaliziranih pogleda i paralelizacije tako što ćemo unaprijed obraditi tok događaja pomoću Apache Flink-a prije nego što ih učitamo u ClickHouse.

Performanse su toliko dobre da vjerovatno nećemo morati ni razmišljati o programskom okretanju tabela. Ranije smo morali da radimo pivotove podataka preuzetih iz Vertice preko upload-a na Apache Parquet.

Nažalost, drugi pokušaj korištenja grafičkog DBMS-a bio je neuspješan. Nisam smatrao da JanusGraph ima prijateljski ekosistem koji je olakšao upoznavanje sa proizvodom. Istovremeno, za konfiguraciju servera koristi se tradicionalni Java način, koji će natjerati ljude koji nisu upoznati sa Javom isplakati krvne 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 da "stavim" BerkeleyDB verziju JanusGrapha.

Dokumentacija je prilično kriva u smislu indeksa, budući da upravljanje indeksima zahtijeva od vas da izvedete prilično čudan šamanizam u Groovyju. Na primjer, kreiranje indeksa mora biti obavljeno pisanjem koda u konzoli Gremlin (koja, inače, ne radi izvan kutije). 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()

Posle reči

U određenom smislu, gornji eksperiment je poređenje između toplog i mekog. Ako razmislite o tome, grafički DBMS izvodi druge operacije kako bi dobio iste rezultate. Međutim, u sklopu testova, izvršio sam i eksperiment sa zahtjevom poput:

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

što odražava pješačku udaljenost. Međutim, čak i na takvim podacima, graf DBMS je pokazao rezultate koji su prelazili nekoliko sekundi... To je, naravno, zbog činjenice da su postojali putevi poput 0 -> X -> Y ... -> 1, što je grafski mehanizam također provjerio.

Čak i za upit poput:

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

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

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

izvor: www.habr.com

Dodajte komentar