Uygun yolları bulma sorununu çözmek için JanusGraph grafik DBMS'nin uygulanabilirliğini test eden bir deney

Uygun yolları bulma sorununu çözmek için JanusGraph grafik DBMS'nin uygulanabilirliğini test eden bir deney

Herkese selam. Çevrimdışı trafik analizi için bir ürün geliştiriyoruz. Projenin, bölgeler arasındaki ziyaretçi rotalarının istatistiksel analiziyle ilgili bir görevi var.

Bu görevin bir parçası olarak kullanıcılar aşağıdaki türdeki sistem sorgularını sorabilirler:

  • "A" alanından "B" alanına kaç ziyaretçinin geçtiği;
  • "A" alanından "C" alanı ve ardından "D" alanı üzerinden "B" alanına kaç ziyaretçinin geçtiği;
  • Belirli bir ziyaretçi tipinin “A” alanından “B” alanına seyahat etmesinin ne kadar sürdüğü.

ve bir dizi benzer analitik sorgu.

Ziyaretçinin alanlar arasındaki hareketi yönlendirilmiş bir grafiktir. İnterneti okuduktan sonra grafik DBMS'lerin analitik raporlar için de kullanıldığını keşfettim. Grafik DBMS'lerin bu tür sorgularla nasıl başa çıkacağını görmek istedim (TL; DR; kötü).

DBMS'yi kullanmayı seçtim JanusGrafik, bir dizi olgun teknolojiye dayanan ve (bence) ona iyi operasyonel özellikler sağlaması gereken grafik açık kaynaklı DBMS'nin olağanüstü bir temsilcisi olarak:

  • BerkeleyDB depolama arka ucu, Apache Cassandra, Scylla;
  • karmaşık indeksler Lucene, Elasticsearch, Solr'da saklanabilir.

JanusGraph'ın yazarları bunun hem OLTP hem de OLAP için uygun olduğunu yazıyor.

BerkeleyDB, Apache Cassandra, Scylla ve ES ile çalıştım ve bu ürünler sistemlerimizde sıklıkla kullanılıyor, dolayısıyla bu grafik DBMS'yi test etme konusunda iyimserdim. RocksDB yerine BerkeleyDB'yi seçmeyi tuhaf buldum, ancak bu muhtemelen işlem gereksinimlerinden kaynaklanıyor. Her durumda, ölçeklenebilir ürün kullanımı için Cassandra veya Scylla'da bir arka uç kullanılması önerilir.

Neo4j'yi kümeleme ticari bir sürüm gerektirdiğinden, yani ürünün açık olmamasından dolayı dikkate almadım.

Grafik DBMS'ler şunu söylüyor: "Eğer bir grafik gibi görünüyorsa, ona bir grafik gibi davranın!" - güzellik!

İlk önce, tam olarak grafik DBMS kurallarına göre yapılmış bir grafik çizdim:

Uygun yolları bulma sorununu çözmek için JanusGraph grafik DBMS'nin uygulanabilirliğini test eden bir deney

Bir öz var Zone, bölgeden sorumludur. Eğer ZoneStep buna ait Zone, sonra ona atıfta bulunur. Öz üzerine Area, ZoneTrack, Person Dikkat etmeyin, alana aittirler ve testin bir parçası olarak değerlendirilmezler. Toplamda böyle bir grafik yapısına yönelik zincirleme arama sorgusu şöyle görünecektir:

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

Rusça'da buna benzer bir şey: ID=0 olan bir Bölge bulun, bir kenarın ona gittiği tüm köşeleri alın (ZoneStep), Bölgeye bir kenarın olduğu ZoneStep'leri bulana kadar geri gitmeden durun. ID=19, bu tür zincirlerin sayısını sayın.

Grafiklerde arama yapmanın tüm inceliklerini bildiğimi iddia etmiyorum, ancak bu sorgu bu kitaba dayanarak oluşturuldu (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

BerkeleyDB arka ucunu kullanarak uzunlukları 50 ila 3 nokta arasında değişen 20 bin parçayı JanusGraph grafik veritabanına yükledim, indeksleri buna göre oluşturdum liderlik.

Python indirme komut dosyası:


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 üzerinde 4 çekirdekli ve 16 GB RAM'e sahip bir VM kullandık. JanusGraph şu komut kullanılarak konuşlandırıldı:

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

Bu durumda tam eşleşme aramaları için kullanılan veriler ve indeksler BerkeleyDB'de saklanır. Daha önce verilen isteği yerine getirdikten sonra onlarca saniyeye eşit bir süre aldım.

Yukarıdaki 4 betiği paralel olarak çalıştırarak, DBMS'yi Docker günlüklerinde neşeli bir Java yığın izleme akışı (ve hepimiz Java yığın izlemelerini okumayı seviyoruz) içeren bir balkabağına dönüştürmeyi başardım.

Biraz düşündükten sonra grafik diyagramını aşağıdaki şekilde basitleştirmeye karar verdim:

Uygun yolları bulma sorununu çözmek için JanusGraph grafik DBMS'nin uygulanabilirliğini test eden bir deney

Varlık niteliklerine göre arama yapmanın, kenarlara göre arama yapmaktan daha hızlı olacağına karar vermek. Sonuç olarak talebim şuna dönüştü:

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

Rusça'da şöyle bir şey var: ID=0 ile ZoneStep'i bulun, ID=19 ile ZoneStep'i bulana kadar geri dönmeden durun, bu tür zincirlerin sayısını sayın.

Gereksiz bağlantılar oluşturmamak için yukarıda verilen yükleme betiğini de basitleştirerek kendimi niteliklerle sınırladım.

Talebin tamamlanması yine de birkaç saniye sürdü; bu da bizim görevimiz için kesinlikle kabul edilemezdi, çünkü herhangi bir AdHoc talebin amacına hiç uygun değildi.

En hızlı Cassandra uygulaması olarak Scylla'yı kullanarak JanusGraph'ı dağıtmayı denedim, ancak bu da herhangi bir önemli performans değişikliğine yol açmadı.

Yani "grafik gibi görünmesine" rağmen grafik DBMS'nin bunu hızlı bir şekilde işlemesini sağlayamadım. Bilmediğim bir şey olduğunu ve JanusGraph'ın bu aramayı saniyeler içinde gerçekleştirebileceğini tamamen varsayıyorum, ancak bunu yapamadım.

Sorunun hala çözülmesi gerektiğinden, şıklık açısından iyimserlik uyandırmayan ancak pratikte tamamen işe yarayabilir bir seçenek olabilecek JOIN'ler ve Pivot tabloları hakkında düşünmeye başladım.

Projemiz zaten Apache ClickHouse kullanıyor, bu yüzden araştırmamı bu analitik DBMS üzerinde test etmeye karar verdim.

Basit bir tarif kullanarak ClickHouse'u dağıttık:

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

Bir veritabanı ve içinde şöyle bir tablo oluşturdum:

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ğıdaki betiği kullanarak verilerle 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
    )

Uçlar gruplar halinde geldiğinden dolum JanusGraph'a göre çok daha hızlıydı.

JOIN kullanarak iki sorgu oluşturuldu. A noktasından B noktasına gitmek için:

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 noktadan geçmek için:

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

İstekler elbette oldukça korkutucu görünüyor; gerçek kullanım için bir yazılım oluşturucu donanımı oluşturmanız gerekir. Ancak çalışıyorlar ve hızlı çalışıyorlar. Hem birinci hem de ikinci istek 0.1 saniyeden daha kısa sürede tamamlanır. Count(*) için 3 noktadan geçen sorgu yürütme süresinin bir örneği:

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 hakkında bir not. Verileri doldururken, JanusGraph oldukça yüksek sayıda IOPS (dört veri popülasyonu iş parçacığı için 1000-1300) oluşturdu ve IOWAIT oldukça yüksekti. Aynı zamanda ClickHouse, disk alt sisteminde minimum düzeyde yük oluşturdu.

Sonuç

Bu tür taleplere hizmet vermek için ClickHouse'u kullanmaya karar verdik. Olay akışını ClickHouse'a yüklemeden önce Apache Flink kullanarak ön işleme tabi tutarak, somutlaştırılmış görünümler ve paralelleştirme kullanarak sorguları her zaman daha da optimize edebiliriz.

Performans o kadar iyi ki muhtemelen pivot tabloları programlı olarak döndürmeyi düşünmemize bile gerek kalmayacak. Daha önce Vertica'dan alınan verilerin pivotlarını Apache Parquet'e yükleme yoluyla yapmamız gerekiyordu.

Ne yazık ki, grafik DBMS'yi kullanmaya yönelik başka bir girişim başarısız oldu. JanusGraph'ın ürüne ayak uydurmayı kolaylaştıran dost canlısı bir ekosisteme sahip olduğunu görmedim. Aynı zamanda, sunucuyu yapılandırmak için geleneksel Java yöntemi kullanılır; bu, Java'ya aşina olmayan kişilerin kan ağlatmasına neden olur:

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}

JanusGraph'ın BerkeleyDB versiyonunu yanlışlıkla "koymayı" başardım.

Dokümantasyon indeksler açısından oldukça çarpıktır, çünkü indeksleri yönetmek Groovy'de oldukça tuhaf bir şamanizm gerçekleştirmenizi gerektirir. Örneğin, bir dizin oluşturmanın Gremlin konsoluna kod yazarak yapılması gerekir (bu arada, bu kutudan çıktığı gibi çalışmıyor). Resmi JanusGraph belgelerinden:

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

Послесловие

Yukarıdaki deney bir bakıma sıcak ve yumuşak arasında bir karşılaştırmadır. Düşünürseniz, bir grafik DBMS'si aynı sonuçları elde etmek için başka işlemler de gerçekleştirir. Ancak testlerin bir parçası olarak aşağıdaki gibi bir istekle de bir deney yaptım:

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

yürüme mesafesini yansıtır. Ancak bu tür veriler üzerinde bile DBMS grafiği birkaç saniyeyi aşan sonuçlar gösterdi... Bunun elbette nedeni şu şekilde yolların olmasıydı: 0 -> X -> Y ... -> 1Grafik motoru da bunu kontrol etti.

Şunun gibi bir sorgu için bile:

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

Bir saniyeden daha kısa bir işlem süresiyle verimli bir yanıt alamadım.

Hikayenin ana fikri, güzel bir fikrin ve paradigmatik modellemenin istenen sonuca yol açmamasıdır; bu, ClickHouse örneği kullanılarak çok daha yüksek bir verimlilikle gösterilmiştir. Bu makalede sunulan kullanım durumu, grafik DBMS'ler için açık bir anti-modeldir, ancak kendi paradigmalarında modelleme için uygun görünmektedir.

Kaynak: habr.com

Yorum ekle