Eksperiments, kurā tiek pārbaudÄ«ta JanusGraph grafika DBVS pielietojamÄ«ba piemērotu ceļu atraÅ”anas problēmas risināŔanai

Eksperiments, kurā tiek pārbaudÄ«ta JanusGraph grafika DBVS pielietojamÄ«ba piemērotu ceļu atraÅ”anas problēmas risināŔanai

Sveiki visiem. Mēs izstrādājam produktu bezsaistes trafika analīzei. Projekta uzdevums ir saistīts ar apmeklētāju ceļu statistisko analīzi reģionos.

Kā daļu no Ŕī uzdevuma lietotāji var uzdot Ŕāda veida sistēmas vaicājumus:

  • cik apmeklētāju devās no zonas "A" uz zonu "B";
  • cik daudz apmeklētāju devās no zonas "A" uz zonu "B" caur zonu "C" un pēc tam caur zonu "D";
  • cik ilgs laiks bija nepiecieÅ”ams, lai noteikta veida apmeklētājs ceļotu no zonas ā€œAā€ uz zonu ā€œBā€.

un vairākus līdzīgus analītiskos vaicājumus.

Apmeklētāja kustÄ«ba pa apgabaliem ir virzÄ«ta diagramma. Pēc interneta lasÄ«Å”anas es atklāju, ka diagrammu DBVS tiek izmantotas arÄ« analÄ«tiskajām atskaitēm. Man bija vēlme redzēt, kā grafiku DBVS tiks galā ar Ŕādiem vaicājumiem (TL; DR; slikti).

Es izvēlējos izmantot DBVS JanusGraph, kā izcils grafu atvērtā pirmkoda DBVS pārstāvis, kas balstās uz virkni nobrieduÅ”u tehnoloÄ£iju, kurām (manuprāt) vajadzētu nodroÅ”ināt tai pienācÄ«gas darbÄ«bas Ä«paŔības:

  • BerkeleyDB krātuves aizmugure, Apache Cassandra, Scylla;
  • kompleksos indeksus var glabāt Lucene, Elasticsearch, Solr.

JanusGraph autori raksta, ka tas ir piemērots gan OLTP, gan OLAP.

Esmu strādājis ar BerkeleyDB, Apache Cassandra, Scylla un ES, un Å”ie produkti bieži tiek izmantoti mÅ«su sistēmās, tāpēc es biju optimistisks par Ŕī grafika DBVS testÄ“Å”anu. Man Ŕķita dÄ«vaini izvēlēties BerkeleyDB, nevis RocksDB, taču tas, iespējams, ir saistÄ«ts ar darÄ«jumu prasÄ«bām. Jebkurā gadÄ«jumā mērogojama produkta lietoÅ”anai ir ieteicams izmantot Cassandra vai Scylla aizmugursistēmu.

Neo4j neuzskatÄ«ju, jo klasterÄ“Å”anai ir nepiecieÅ”ama komerciāla versija, tas ir, produkts nav atvērtā koda.

Diagrammu DBVS saka: ā€œJa tas izskatās kā grafiks, uzskatiet to par grafiku!ā€ - skaistums!

Vispirms es uzzīmēju grafiku, kas tika izveidots precīzi saskaņā ar grafiku DBVS kanoniem:

Eksperiments, kurā tiek pārbaudÄ«ta JanusGraph grafika DBVS pielietojamÄ«ba piemērotu ceļu atraÅ”anas problēmas risināŔanai

Ir kāda bÅ«tÄ«ba Zone, kas atbild par apgabalu. Ja ZoneStep pieder pie Ŕī Zone, tad viņŔ uz to atsaucas. Pēc bÅ«tÄ«bas Area, ZoneTrack, Person Nepievērsiet uzmanÄ«bu, tie pieder domēnam un netiek uzskatÄ«ti par testa daļu. Kopumā ķēdes meklÄ“Å”anas vaicājums Ŕādai diagrammas struktÅ«rai izskatÄ«tos Ŕādi:

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

Kas krieviski ir apmēram Ŕādi: atrodiet zonu ar ID=0, paņemiet visas virsotnes, no kurām uz to iet mala (ZoneStep), stampējieties, neatgriežoties, lÄ«dz atrodat tās ZoneSteps, no kurām ir mala uz zonu ar ID=19, saskaitiet Ŕādu ķēžu skaitu.

Es nedomāju, ka zinu visas grafikas meklÄ“Å”anas sarežģītÄ«bas, taču Å”is vaicājums tika Ä£enerēts, pamatojoties uz Å”o grāmatu (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Es ielādēju 50 tÅ«kstoÅ”us ierakstu, kuru garums ir no 3 lÄ«dz 20 punktiem, JanusGraph grafiku datu bāzē, izmantojot BerkeleyDB aizmugursistēmu, izveidoju indeksus saskaņā ar vadÄ«ba.

Python lejupielādes skripts:


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)

Mēs izmantojām virtuālo maŔīnu ar 4 kodoliem un 16 GB RAM SSD. JanusGraph tika izvietots, izmantojot Å”o komandu:

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

Å ajā gadÄ«jumā dati un indeksi, kas tiek izmantoti precÄ«zās atbilstÄ«bas meklÄ“Å”anai, tiek glabāti BerkeleyDB. IzpildÄ«jis iepriekÅ” izteikto pieprasÄ«jumu, saņēmu laiku, kas vienāds ar vairākiem desmitiem sekunžu.

Paralēli palaižot četrus iepriekÅ” minētos skriptus, man izdevās pārvērst DBVS par Ä·irbi ar jautru Java stacktraces plÅ«smu (un mums visiem patÄ«k lasÄ«t Java stacktraces) Docker žurnālos.

Pēc dažām pārdomām es nolēmu vienkārÅ”ot diagrammas diagrammu lÄ«dz Ŕādai:

Eksperiments, kurā tiek pārbaudÄ«ta JanusGraph grafika DBVS pielietojamÄ«ba piemērotu ceļu atraÅ”anas problēmas risināŔanai

Izlemjot, ka meklÄ“Å”ana pēc entÄ«tijas atribÅ«tiem bÅ«tu ātrāka nekā meklÄ“Å”ana pēc malām. Rezultātā mans pieprasÄ«jums izvērtās Ŕādi:

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

Kas krieviski ir apmēram Ŕāds: atrodiet ZoneStep ar ID=0, spiediet, neatgriežoties, lÄ«dz atrodat ZoneStep ar ID=19, saskaitiet Ŕādu ķēžu skaitu.

Es arī vienkārŔoju iepriekŔ norādīto ielādes skriptu, lai neradītu nevajadzīgus savienojumus, ierobežojot sevi ar atribūtiem.

Pieprasījuma izpilde vēl prasīja vairākas sekundes, kas mūsu uzdevumam bija pilnīgi nepieņemami, jo tas nemaz nebija piemērots jebkāda veida AdHoc pieprasījumu vajadzībām.

Es mēģināju izvietot JanusGraph, izmantojot Scylla kā ātrāko Cassandra ievieÅ”anu, taču tas arÄ« neizraisÄ«ja nekādas bÅ«tiskas veiktspējas izmaiņas.

Tāpēc, neskatoties uz to, ka "tas izskatās kā grafiks", es nevarēju panākt, lai diagramma DBVS to ātri apstrādātu. Es pilnÄ«bā pieņemu, ka ir kaut kas, ko es nezinu un ka JanusGraph var likt veikt Å”o meklÄ“Å”anu sekundes daļā, tomēr es to nevarēju izdarÄ«t.

Tā kā problēma vēl bija jārisina, sāku domāt par tabulu JOIN un Pivots, kas elegances ziņā optimismu neviesa, bet praksē varētu būt pilnīgi darbojams variants.

MÅ«su projekts jau izmanto Apache ClickHouse, tāpēc es nolēmu pārbaudÄ«t savu pētÄ«jumu par Å”o analÄ«tisko DBVS.

Izvietots ClickHouse, izmantojot vienkārŔu recepti:

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

Es izveidoju datu bāzi un tabulu tajā Ŕādi:

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

Es to aizpildīju ar datiem, izmantojot Ŕādu 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
    )

Tā kā ieliktņi tiek piegādāti partijās, aizpildÄ«Å”ana bija daudz ātrāka nekā JanusGraph.

Izveidoti divi vaicājumi, izmantojot JOIN. Lai pārvietotos no punkta A uz punktu 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

Lai izietu cauri 3 punktiem:

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

PieprasÄ«jumi, protams, izskatās diezgan biedējoÅ”i; reālai lietoÅ”anai jums ir jāizveido programmatÅ«ras Ä£eneratora instalācija. Tomēr viņi strādā un strādā ātri. Gan pirmais, gan otrais pieprasÄ«jums tiek izpildÄ«ts mazāk nekā 0.1 sekundē. Å eit ir piemērs vaicājuma izpildes laikam count (*), kas iet cauri 3 punktiem:

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

PiezÄ«me par IOPS. Aizpildot datus, JanusGraph Ä£enerēja diezgan lielu IOPS skaitu (1000ā€“1300 četriem datu kopas pavedieniem), un IOWAIT bija diezgan augsts. Tajā paŔā laikā ClickHouse radÄ«ja minimālu diska apakÅ”sistēmas slodzi.

Secinājums

Mēs nolēmām izmantot ClickHouse, lai apkalpotu Ŕāda veida pieprasÄ«jumus. Mēs vienmēr varam vēl vairāk optimizēt vaicājumus, izmantojot materializētus skatus un paralēli, iepriekÅ” apstrādājot notikumu straumi, izmantojot Apache Flink, pirms to ielādÄ“Å”anas pakalpojumā ClickHouse.

Veiktspēja ir tik laba, ka mums, iespējams, pat nevajadzēs domāt par tabulu programmÄ“Å”anu. IepriekÅ” mums bija jāveic datu pagriezieni, kas iegÅ«ti no Vertica, augÅ”upielādējot tos Apache Parquet.

Diemžēl vēl viens mēģinājums izmantot diagrammu DBVS bija neveiksmÄ«gs. Es neuzskatÄ«ju, ka JanusGraph ir draudzÄ«ga ekosistēma, kas atvieglotu produkta lietoÅ”anu. Tajā paŔā laikā servera konfigurÄ“Å”anai tiek izmantots tradicionālais Java veids, kas liks cilvēkiem, kuri nav pazÄ«stami ar Java, raudāt asins asaras:

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}

Man izdevās nejauŔi "ielikt" JanusGraph BerkeleyDB versiju.

Dokumentācija indeksu ziņā ir diezgan greiza, jo, lai pārvaldÄ«tu indeksus, Groovy programmā ir jāveic diezgan dÄ«vains Å”amanisms. Piemēram, indeksa izveide ir jāveic, ierakstot kodu Gremlin konsolē (kas, starp citu, nedarbojas no kastes). No oficiālās JanusGraph dokumentācijas:

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

Pēcvārds

Savā ziņā iepriekÅ”minētais eksperiments ir salÄ«dzinājums starp siltu un mÄ«kstu. Ja padomājat par to, diagramma DBVS veic citas darbÄ«bas, lai iegÅ«tu tādus paÅ”us rezultātus. Tomēr testu ietvaros es arÄ« veicu eksperimentu ar Ŕādu pieprasÄ«jumu:

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

kas atspoguļo pastaigas attālumu. Tomēr pat uz Ŕādiem datiem grafiks DBVS uzrādÄ«ja rezultātus, kas pārsniedza dažas sekundes... Tas, protams, ir saistÄ«ts ar to, ka bija tādi ceļi kā 0 -> X -> Y ... -> 1, ko arÄ« pārbaudÄ«ja grafika dzinējs.

Pat tādiem vaicājumiem kā:

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

Es nevarēju saņemt produktīvu atbildi, apstrādes laiks bija mazāks par sekundi.

Stāsta morāle ir tāda, ka skaista ideja un paradigmatiskā modelÄ“Å”ana nenoved pie vēlamā rezultāta, kas tiek demonstrēts ar daudz lielāku efektivitāti, izmantojot ClickHouse piemēru. Å ajā rakstā aprakstÄ«tais lietoÅ”anas gadÄ«jums ir skaidrs pretmodelis grafu DBVS, lai gan Ŕķiet piemērots modelÄ“Å”anai to paradigmā.

Avots: www.habr.com

Pievieno komentāru