Uyğun yolların tapılması probleminin həlli üçün JanusGraph DBMS qrafikinin tətbiqini sınaqdan keçirən təcrübə

Uyğun yolların tapılması probleminin həlli üçün JanusGraph DBMS qrafikinin tətbiqini sınaqdan keçirən təcrübə

Hamıya salam. Biz oflayn trafik təhlili üçün məhsul hazırlayırıq. Layihədə regionlar üzrə ziyarətçi marşrutlarının statistik təhlili ilə bağlı tapşırıq var.

Bu tapşırığın bir hissəsi olaraq istifadəçilər aşağıdakı növ sistem sorğularını verə bilərlər:

  • “A” sahəsindən “B” sahəsinə nə qədər ziyarətçi keçdi;
  • nə qədər ziyarətçi "A" sahəsindən "B" sahəsinə "C" sahəsi ilə, sonra isə "D" sahəsindən keçib;
  • müəyyən bir növ ziyarətçinin “A” sahəsindən “B” sahəsinə səyahət etməsi üçün nə qədər vaxt lazım idi.

və bir sıra oxşar analitik sorğular.

Ziyarətçinin ərazilər arasında hərəkəti istiqamətləndirilmiş qrafikdir. İnterneti oxuduqdan sonra mən kəşf etdim ki, qrafik DBMS-lər analitik hesabatlar üçün də istifadə olunur. Qrafik DBMS-lərin bu cür sorğuların öhdəsindən necə gələcəyini görmək istəyim var idi (TL; DR; zəif).

Mən DBMS-dən istifadə etməyi seçdim JanusGraph, (mənim fikrimcə) onu layiqli əməliyyat xüsusiyyətləri ilə təmin etməli olan yetkin texnologiyalar yığınına əsaslanan açıq mənbəli DBMS qrafikinin görkəmli nümayəndəsi olaraq:

  • BerkeleyDB saxlama dəstəyi, Apache Cassandra, Scylla;
  • mürəkkəb indekslər Lucene, Elasticsearch, Solr-da saxlanıla bilər.

JanusGraph müəllifləri onun həm OLTP, həm də OLAP üçün uyğun olduğunu yazır.

Mən BerkeleyDB, Apache Cassandra, Scylla və ES ilə işləmişəm və bu məhsullar sistemlərimizdə tez-tez istifadə olunur, ona görə də bu qrafik DBMS-ni sınamaqda optimist idim. RocksDB əvəzinə BerkeleyDB-ni seçmək mənə qəribə gəldi, lakin bu, çox güman ki, əməliyyat tələbləri ilə bağlıdır. İstənilən halda, miqyaslana bilən, məhsul istifadəsi üçün Cassandra və ya Scylla-da arxa hissədən istifadə etmək tövsiyə olunur.

Neo4j-i nəzərə almadım, çünki klasterləşmə kommersiya versiyasını tələb edir, yəni məhsul açıq mənbə deyil.

Qrafik DBMS-lər deyir: "Əgər o, qrafikə bənzəyirsə, onu qrafik kimi qəbul edin!" - gözəllik!

Əvvəlcə DBMS qrafikinin qanunlarına uyğun olaraq hazırlanmış bir qrafik çəkdim:

Uyğun yolların tapılması probleminin həlli üçün JanusGraph DBMS qrafikinin tətbiqini sınaqdan keçirən təcrübə

mahiyyəti var Zone, əraziyə cavabdehdir. Əgər ZoneStep buna aiddir Zone, sonra ona istinad edir. Mahiyyəti üzrə Area, ZoneTrack, Person Diqqət etməyin, onlar domenə aiddir və testin bir hissəsi hesab edilmir. Ümumilikdə, belə bir qrafik strukturu üçün zəncirvari axtarış sorğusu belə görünür:

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

Rus dilində belə bir şey var: ID=0 olan bir Zona tapın, kənarın ona getdiyi bütün təpələri götürün (ZoneStep), Zona ilə kənar olan ZoneAddımları tapana qədər geri dönmədən ayaq üstələyin. ID=19, belə zəncirlərin sayını sayın.

Qrafiklərdə axtarışın bütün incəliklərini bildiyimi iddia etmirəm, lakin bu sorğu bu kitab əsasında yaradılıb (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

BerkeleyDB backendindən istifadə edərək JanusGraph qrafik verilənlər bazasına uzunluğu 50 ilə 3 bal arasında dəyişən 20 min treki yüklədim, uyğun olaraq indekslər yaratdım. liderlik.

Python yükləmə skripti:


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-də 4 nüvəli və 16 GB RAM olan VM-dən istifadə etdik. JanusGraph bu əmrdən istifadə edərək yerləşdirilib:

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

Bu halda, dəqiq uyğunluq axtarışları üçün istifadə olunan məlumat və indekslər BerkeleyDB-də saxlanılır. Daha əvvəl verilmiş sorğunu yerinə yetirərək, bir neçə on saniyəyə bərabər vaxt aldım.

Yuxarıdakı 4 skripti paralel olaraq işlətməklə, mən DBMS-ni Docker qeydlərində şən Java stacktraces axını (və biz hamımız Java stacktraces oxumağı sevirik) ilə balqabaq halına gətirə bildim.

Bir az fikirləşdikdən sonra qrafik diaqramı aşağıdakı kimi sadələşdirmək qərarına gəldim:

Uyğun yolların tapılması probleminin həlli üçün JanusGraph DBMS qrafikinin tətbiqini sınaqdan keçirən təcrübə

Müəssisə atributları üzrə axtarışın kənarlar üzrə axtarışdan daha sürətli olacağına qərar vermək. Nəticə etibarı ilə müraciətim belə oldu:

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

Rus dilində belə bir şey var: ID=0 olan ZoneStep-i tapın, ID=19-lu ZoneStep-i tapana qədər geri qayıtmadan ayaq bas, belə zəncirlərin sayını sayın.

Özümü atributlarla məhdudlaşdıraraq, lazımsız əlaqələr yaratmamaq üçün yuxarıda verilmiş yükləmə skriptini də sadələşdirdim.

Sorğunun yerinə yetirilməsi hələ də bir neçə saniyə çəkdi, bu, bizim tapşırığımız üçün tamamilə qəbuledilməz idi, çünki o, istənilən növ AdHoc sorğularının məqsədləri üçün heç də uyğun deyildi.

Ən sürətli Cassandra tətbiqi olaraq Scylla-dan istifadə edərək JanusGraph-ı yerləşdirməyə çalışdım, lakin bu da heç bir əhəmiyyətli performans dəyişikliyinə səbəb olmadı.

Beləliklə, "bu, qrafikə bənzəyir" olmasına baxmayaraq, onu tez bir zamanda emal etmək üçün DBMS qrafikini əldə edə bilmədim. Tamamilə güman edirəm ki, bilmədiyim bir şey var və JanusGraph bu axtarışı saniyənin bir hissəsində həyata keçirmək üçün edilə bilər, lakin bunu edə bilmədim.

Problem hələ də həll edilməli olduğundan, zəriflik baxımından optimizmi ruhlandırmayan, lakin praktikada tamamilə işlək bir seçim ola bilən masaların JOIN və Pivotları haqqında düşünməyə başladım.

Layihəmiz artıq Apache ClickHouse-dan istifadə edir, ona görə də araşdırmalarımı bu analitik DBMS üzərində sınamaq qərarına gəldim.

Sadə bir reseptdən istifadə edərək yerləşdirilmiş ClickHouse:

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

Mən verilənlər bazası və orada cədvəl yaratdım:

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

Aşağıdakı skriptdən istifadə edərək onu məlumatlarla doldurdum:

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
    )

Daxiletmələr partiyalar şəklində gəldiyi üçün doldurma JanusGraph ilə müqayisədə daha sürətli idi.

JOIN istifadə edərək iki sorğu quruldu. A nöqtəsindən B nöqtəsinə keçmək üçün:

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 nöqtədən keçmək üçün:

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

İstəklər, əlbəttə ki, olduqca qorxulu görünür; real istifadə üçün bir proqram generatoru qoşqu yaratmalısınız. Bununla belə, onlar işləyir və tez işləyirlər. Həm birinci, həm də ikinci sorğular 0.1 saniyədən az müddətdə tamamlanır. 3 nöqtədən keçən count(*) üçün sorğunun icra müddətinə bir nümunə:

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

IOPS haqqında qeyd. Məlumatları doldurarkən, JanusGraph kifayət qədər yüksək sayda IOPS (dörd məlumat kütləsi üçün 1000-1300) yaratdı və IOWAIT olduqca yüksək idi. Eyni zamanda, ClickHouse disk alt sistemində minimal yük yaratdı.

Nəticə

Bu tip sorğuya xidmət etmək üçün ClickHouse-dan istifadə etmək qərarına gəldik. Biz həmişə sorğuları ClickHouse-a yükləməzdən əvvəl Apache Flink-dən istifadə edərək hadisə axınını əvvəlcədən emal etməklə maddiləşdirilmiş görünüşlərdən və paralelləşdirmədən istifadə edərək daha da optimallaşdıra bilərik.

Performans o qədər yaxşıdır ki, yəqin ki, cədvəlləri proqramlı şəkildə çevirmək barədə düşünməyə belə ehtiyac qalmayacaq. Əvvəllər biz Vertica-dan Apache Parket-ə yükləmə yolu ilə əldə edilmiş məlumatların pivotlarını etməli idik.

Təəssüf ki, DBMS qrafikindən istifadə etmək üçün başqa bir cəhd uğursuz oldu. Mən JanusGraph-da məhsulun sürətini artırmağı asanlaşdıran dostluq ekosistemine malik olduğunu görmədim. Eyni zamanda, serveri konfiqurasiya etmək üçün ənənəvi Java üsulundan istifadə olunur ki, bu da Java ilə tanış olmayan insanları qan gözyaşı tökməyə məcbur edəcək:

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}

Mən təsadüfən JanusGraph-ın BerkeleyDB versiyasını “qoya bildim”.

Sənədlər indekslər baxımından olduqca əyridir, çünki indeksləri idarə etmək sizdən Groovy-də olduqca qəribə şamanizmi yerinə yetirməyi tələb edir. Məsələn, indeks yaratmaq Gremlin konsolunda kod yazmaqla edilməlidir (bu, yeri gəlmişkən, qutudan kənarda işləmir). Rəsmi JanusGraph sənədlərindən:

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

Sözündən sonra

Müəyyən mənada yuxarıdakı təcrübə isti və yumşaq arasında müqayisədir. Fikir versəniz, qrafik DBMS eyni nəticələri əldə etmək üçün başqa əməliyyatları yerinə yetirir. Bununla birlikdə, testlərin bir hissəsi olaraq, mən də aşağıdakı kimi bir sorğu ilə bir təcrübə keçirdim:

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

bu da gediş məsafəsini əks etdirir. Bununla belə, belə məlumatlarda belə, DBMS qrafiki bir neçə saniyədən çox davam edən nəticələr göstərdi... Bu, təbii ki, belə yolların olması ilə əlaqədardır. 0 -> X -> Y ... -> 1, bunu qrafik mühərriki də yoxladı.

Hətta belə bir sorğu üçün:

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

Bir saniyədən az işləmə müddəti ilə məhsuldar cavab ala bilmədim.

Hekayənin əxlaqı ondan ibarətdir ki, gözəl ideya və paradiqmatik modelləşdirmə istənilən nəticəyə gətirib çıxarmır ki, bu da ClickHouse nümunəsindən istifadə etməklə daha yüksək səmərəliliklə nümayiş etdirilir. Bu məqalədə təqdim olunan istifadə nümunəsi, onların paradiqmasında modelləşdirmə üçün uyğun görünsə də, qrafik DBMS-lər üçün aydın bir anti-naxışdır.

Mənbə: www.habr.com

Добавить комментарий