Kokeilu, jossa testataan JanusGraph-graafin DBMS:n soveltuvuutta sopivien polkujen löytämisen ongelman ratkaisemiseen

Kokeilu, jossa testataan JanusGraph-graafin DBMS:n soveltuvuutta sopivien polkujen löytämisen ongelman ratkaisemiseen

Hei kaikki. Kehitämme tuotetta offline-liikenteen analysointiin. Hankkeen tehtävänä on alueiden välisten kävijäreittien tilastollinen analyysi.

Osana tätä tehtävää käyttäjät voivat kysyä seuraavan tyyppisiä järjestelmäkyselyitä:

  • kuinka monta kävijää kulki alueelta "A" alueelle "B";
  • kuinka monta kävijää kulki alueelta "A" alueelle "B" alueen "C" ja sitten alueen "D" kautta;
  • kuinka kauan tietyntyyppisen vierailijan matka A-alueelta B-alueelle kesti.

ja useita vastaavia analyyttisiä kyselyitä.

Vierailijan liike alueiden välillä on suunnattu graafi. Luettuani Internetiä huomasin, että graafisia DBMS-järjestelmiä käytetään myös analyyttisiin raportteihin. Halusin nähdä, kuinka graafiset DBMS:t selviäisivät tällaisista kyselyistä (TL; DR; huonosti).

Päätin käyttää DBMS:ää JanusGrafiikka, erinomaisena edustajana graafisen avoimen lähdekoodin DBMS:stä, joka perustuu joukkoon kypsää teknologiaa, jonka (mielestäni) pitäisi tarjota sille kunnolliset toimintaominaisuudet:

  • BerkeleyDB-tallennustaustajärjestelmä, Apache Cassandra, Scylla;
  • monimutkaisia ​​indeksejä voidaan tallentaa Luceneen, Elasticsearchiin, Solriin.

JanusGraphin kirjoittajat kirjoittavat, että se sopii sekä OLTP:lle että OLAP:lle.

Olen työskennellyt BerkeleyDB:n, Apache Cassandran, Scyllan ja ES:n kanssa, ja näitä tuotteita käytetään usein järjestelmissämme, joten olin optimistinen tämän graafisen DBMS:n testaamisen suhteen. Minusta oli outoa valita BerkeleyDB RocksDB:n sijaan, mutta se johtuu todennäköisesti tapahtumavaatimuksista. Joka tapauksessa skaalautuvan tuotteen käyttöä varten on suositeltavaa käyttää taustaohjelmaa Cassandrassa tai Scyllassa.

En ottanut huomioon Neo4j:tä, koska klusterointi vaatii kaupallisen version, eli tuote ei ole avoin.

Graafi-DBMS:t sanovat: "Jos se näyttää kaaviolta, käsittele sitä kaaviona!" - kauneus!

Ensin piirsin graafin, joka tehtiin täsmälleen graafisen DBMS:n kanonien mukaan:

Kokeilu, jossa testataan JanusGraph-graafin DBMS:n soveltuvuutta sopivien polkujen löytämisen ongelman ratkaisemiseen

On olemassa olemus Zone, joka vastaa alueesta. Jos ZoneStep kuuluu tähän Zone, sitten hän viittaa siihen. Pohjimmiltaan Area, ZoneTrack, Person Älä kiinnitä huomiota, ne kuuluvat verkkotunnukseen, eikä niitä pidetä osana testiä. Kaiken kaikkiaan ketjuhakukysely tällaiselle kaaviorakenteelle näyttäisi tältä:

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

Mikä venäjäksi on jotain tällaista: etsi vyöhyke, jonka ID=0, ota kaikki kärjet, joista reuna menee siihen (ZoneStep), tallaa palaamatta takaisin, kunnes löydät ne ZoneSteps-alueet, joista on reuna vyöhykkeelle ID=19, laske tällaisten ketjujen määrä.

En väitä tietäväni kaikkia kaavioiden haun hienouksia, mutta tämä kysely luotiin tämän kirjan perusteella (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Latasin 50 tuhatta 3-20 pisteen pituista raitaa JanusGraph-graafitietokantaan BerkeleyDB-taustaohjelmalla, loin indeksit johtajuutta.

Python-latausskripti:


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)

Käytimme virtuaalikonetta, jossa oli 4 ydintä ja 16 Gt RAM-muistia SSD-levyllä. JanusGraph otettiin käyttöön tällä komennolla:

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

Tässä tapauksessa tarkan vastaavuuden hauissa käytettävät tiedot ja indeksit tallennetaan BerkeleyDB:hen. Suoritettuani aiemmin antaman pyynnön, sain ajan, joka vastaa useita kymmeniä sekunteja.

Suorittamalla neljä yllä olevaa skriptiä rinnakkain, onnistuin muuttamaan DBMS:n kurpitsaksi iloisella Java-pinojälkien virran (ja me kaikki rakastamme Java-pinojälkien lukemista) Dockerin lokeissa.

Hetken pohdinnan jälkeen päätin yksinkertaistaa kaaviokuvan seuraavasti:

Kokeilu, jossa testataan JanusGraph-graafin DBMS:n soveltuvuutta sopivien polkujen löytämisen ongelman ratkaisemiseen

Päättäminen, että etsiminen entiteettiattribuuttien perusteella olisi nopeampaa kuin reunojen etsiminen. Tämän seurauksena pyyntöni muuttui seuraavaksi:

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

Mikä venäjäksi on jotain tällaista: etsi ZoneStep ID=0, taputtele palaamatta, kunnes löydät ZoneStepin ID=19, laske tällaisten ketjujen määrä.

Myös yllä annettua latausskriptiä yksinkertaistin, jotta en luo tarpeettomia yhteyksiä ja rajoittuin attribuutteihin.

Pyynnön suorittaminen kesti silti useita sekunteja, mikä oli täysin mahdotonta hyväksyä tehtävämme kannalta, koska se ei soveltunut lainkaan AdHoc-pyyntöjen tarkoituksiin.

Yritin ottaa JanusGraphia käyttöön käyttämällä Scyllaa nopeimpana Cassandra-toteutuksena, mutta tämäkään ei johtanut merkittäviin suoritusmuutoksiin.

Joten huolimatta siitä, että "se näyttää kaaviolta", en saanut kaavion DBMS:ää käsittelemään sitä nopeasti. Oletan täysin, että on jotain, jota en tiedä ja että JanusGraph voidaan saada suorittamaan tämä haku sekunnin murto-osassa, mutta en kuitenkaan pystynyt tekemään sitä.

Koska ongelma piti vielä ratkaista, aloin pohtia taulukoiden JOIN- ja Pivotteja, jotka eivät herättäneet optimismia eleganssin suhteen, mutta voisivat olla käytännössä täysin toimiva vaihtoehto.

Projektissamme käytetään jo Apache ClickHousea, joten päätin testata tutkimustani tällä analyyttisellä tietokantajärjestelmällä.

ClickHouse otettiin käyttöön yksinkertaisella reseptillä:

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

Tein tietokannan ja siihen taulukon seuraavasti:

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

Täytin sen tiedoilla seuraavalla skriptillä:

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
    )

Koska lisäosat tulevat erissä, täyttö oli paljon nopeampaa kuin JanusGraphilla.

Rakensi kaksi kyselyä käyttämällä JOIN. Siirtyminen pisteestä A pisteeseen 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

Voit käydä läpi 3 pistettä:

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

Pyynnöt näyttävät tietysti melko pelottavilta; todellista käyttöä varten sinun on luotava ohjelmistogeneraattorivaljaat. Ne kuitenkin toimivat ja toimivat nopeasti. Sekä ensimmäinen että toinen pyyntö suoritetaan alle 0.1 sekunnissa. Tässä on esimerkki kyselyn suoritusajasta count(*):lle, joka kulkee 3 pisteen läpi:

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

Huomautus IOPS:stä. Tietoa täytettäessä JanusGraph loi melko suuren määrän IOPS:iä (1000-1300 neljälle tietopopulaatiosäikeelle) ja IOWAIT oli melko korkea. Samaan aikaan ClickHouse loi minimaalisen kuormituksen levyalijärjestelmään.

Johtopäätös

Päätimme käyttää ClickHousea tämäntyyppisten pyyntöjen hoitamiseen. Voimme aina optimoida kyselyitä edelleen materialisoituneiden näkymien ja rinnakkaistamisen avulla esikäsittelemällä tapahtumavirran Apache Flinkillä ennen niiden lataamista ClickHouseen.

Suorituskyky on niin hyvä, että meidän ei todennäköisesti tarvitse edes ajatella taulukoiden ohjelmointia. Aiemmin meidän piti tehdä Pivotit Verticasta haetuille tiedoille lataamalla ne Apache Parquetiin.

Valitettavasti toinen yritys käyttää kaavion DBMS:ää epäonnistui. En huomannut, että JanusGraphilla olisi ystävällinen ekosysteemi, joka tekisi helpoksi päästä vauhtiin tuotteen kanssa. Samanaikaisesti palvelimen konfiguroinnissa käytetään perinteistä Java-tapaa, joka saa ihmiset, jotka eivät tunne Javaa, itkemään veren kyyneleitä:

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}

Onnistuin vahingossa "laittamaan" BerkeleyDB-version JanusGraphista.

Dokumentaatio on indeksien suhteen varsin vino, sillä indeksien hallinta vaatii Groovyssa melko outoa shamanismia. Esimerkiksi hakemiston luominen on tehtävä kirjoittamalla koodi Gremlin-konsoliin (joka muuten ei toimi heti valmiina). Virallisista JanusGraph-dokumenteista:

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

loppusanat

Yllä oleva kokeilu on tavallaan vertailu lämpimän ja pehmeän välillä. Jos ajattelet sitä, kuvaajan DBMS suorittaa muita toimintoja saadakseen samat tulokset. Osana testejä tein kuitenkin myös kokeen seuraavalla pyynnöstä:

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

joka heijastaa kävelyetäisyyttä. Kuitenkin myös sellaisilla tiedoilla kaavio DBMS näytti tuloksia, jotka ylittivät muutaman sekunnin... Tämä tietysti johtuu siitä, että siellä oli polkuja, kuten 0 -> X -> Y ... -> 1, jonka kaaviomoottori myös tarkisti.

Jopa sellaiseen kyselyyn kuin:

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

En saanut tuottavaa vastausta alle sekunnin käsittelyajalla.

Tarinan moraali on, että kaunis idea ja paradigmaattinen mallinnus eivät johda haluttuun lopputulokseen, mikä näkyy paljon tehokkaammin ClickHousen esimerkillä. Tässä artikkelissa esitetty käyttötapaus on selkeä graafisen DBMS:n vastainen malli, vaikka se näyttää sopivalta mallintamiseen niiden paradigmassa.

Lähde: will.com

Lisää kommentti