Eksperimen yang menguji penerapan DBMS grafik JanusGraph untuk memecahkan masalah menemukan jalur yang sesuai

Eksperimen yang menguji penerapan DBMS grafik JanusGraph untuk memecahkan masalah menemukan jalur yang sesuai

Halo semua. Kami sedang mengembangkan produk untuk analisis lalu lintas offline. Proyek ini mempunyai tugas terkait analisis statistik rute pengunjung lintas wilayah.

Sebagai bagian dari tugas ini, pengguna dapat menanyakan pertanyaan sistem jenis berikut:

  • berapa pengunjung yang melintas dari area "A" ke area "B";
  • berapa banyak pengunjung yang lewat dari area "A" ke area "B" melalui area "C" dan kemudian melalui area "D";
  • berapa lama waktu yang dibutuhkan oleh jenis pengunjung tertentu untuk melakukan perjalanan dari area β€œA” ke area β€œB”.

dan sejumlah pertanyaan analitis serupa.

Pergerakan pengunjung melintasi area merupakan grafik berarah. Setelah membaca Internet, saya menemukan bahwa DBMS grafik juga digunakan untuk laporan analitis. Saya mempunyai keinginan untuk melihat bagaimana DBMS grafik akan mengatasi pertanyaan seperti itu (TL; DR; buruk).

Saya memilih untuk menggunakan DBMS JanusGraph, sebagai perwakilan luar biasa dari DBMS sumber terbuka grafik, yang mengandalkan serangkaian teknologi matang, yang (menurut saya) harus memberikan karakteristik operasional yang layak:

  • Backend penyimpanan BerkeleyDB, Apache Cassandra, Scylla;
  • indeks kompleks dapat disimpan di Lucene, Elasticsearch, Solr.

Penulis JanusGraph menulis bahwa ini cocok untuk OLTP dan OLAP.

Saya telah bekerja dengan BerkeleyDB, Apache Cassandra, Scylla dan ES, dan produk ini sering digunakan di sistem kami, jadi saya optimis untuk menguji DBMS grafik ini. Saya merasa aneh memilih BerkeleyDB daripada RocksDB, tapi itu mungkin karena persyaratan transaksi. Bagaimanapun, untuk penggunaan produk yang skalabel, disarankan untuk menggunakan backend di Cassandra atau Scylla.

Saya tidak mempertimbangkan Neo4j karena clustering memerlukan versi komersial, yaitu produknya bukan open source.

DBMS Grafik mengatakan: β€œJika terlihat seperti grafik, perlakukan seperti grafik!” - kecantikan!

Pertama, saya menggambar grafik, yang dibuat persis sesuai dengan aturan DBMS grafik:

Eksperimen yang menguji penerapan DBMS grafik JanusGraph untuk memecahkan masalah menemukan jalur yang sesuai

Ada intinya Zone, bertanggung jawab atas area tersebut. Jika ZoneStep milik ini Zone, lalu dia merujuknya. Intinya Area, ZoneTrack, Person Jangan perhatikan, mereka termasuk dalam domain dan tidak dianggap sebagai bagian dari pengujian. Secara total, kueri penelusuran berantai untuk struktur grafik seperti itu akan terlihat seperti:

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

Yang dalam bahasa Rusia kira-kira seperti ini: temukan Zona dengan ID=0, ambil semua simpul dari mana sebuah tepi menuju ke sana (ZoneStep), injak tanpa kembali sampai Anda menemukan ZoneSteps yang darinya terdapat tepi ke Zona dengan ID=19, hitung jumlah rantai tersebut.

Saya tidak berpura-pura mengetahui semua seluk-beluk pencarian grafik, tetapi kueri ini dibuat berdasarkan buku ini (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Saya memuat 50 ribu trek dengan panjang mulai dari 3 hingga 20 poin ke dalam database grafik JanusGraph menggunakan backend BerkeleyDB, membuat indeks sesuai dengan kepemimpinan.

Skrip pengunduhan python:


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)

Kami menggunakan VM dengan 4 core dan RAM 16 GB pada SSD. JanusGraph dikerahkan menggunakan perintah ini:

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

Dalam hal ini, data dan indeks yang digunakan untuk pencarian pencocokan tepat disimpan di BerkeleyDB. Setelah menjalankan permintaan yang diberikan sebelumnya, saya menerima waktu yang setara dengan beberapa puluh detik.

Dengan menjalankan 4 skrip di atas secara paralel, saya berhasil mengubah DBMS menjadi labu dengan aliran Java stacktraces yang ceria (dan kita semua suka membaca Java stacktraces) di log Docker.

Setelah berpikir beberapa lama, saya memutuskan untuk menyederhanakan diagram grafik menjadi berikut:

Eksperimen yang menguji penerapan DBMS grafik JanusGraph untuk memecahkan masalah menemukan jalur yang sesuai

Memutuskan bahwa pencarian berdasarkan atribut entitas akan lebih cepat daripada pencarian berdasarkan tepian. Hasilnya, permintaan saya berubah menjadi sebagai berikut:

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

Yang dalam bahasa Rusia kira-kira seperti ini: temukan ZoneStep dengan ID=0, injak tanpa kembali sampai Anda menemukan ZoneStep dengan ID=19, hitung jumlah rantai tersebut.

Saya juga menyederhanakan skrip pemuatan yang diberikan di atas agar tidak membuat koneksi yang tidak perlu, membatasi diri pada atribut.

Permintaan tersebut masih membutuhkan waktu beberapa detik untuk diselesaikan, yang sama sekali tidak dapat diterima untuk tugas kami, karena permintaan tersebut sama sekali tidak sesuai untuk tujuan permintaan AdHoc dalam bentuk apa pun.

Saya mencoba menerapkan JanusGraph menggunakan Scylla sebagai implementasi Cassandra tercepat, tetapi ini juga tidak menghasilkan perubahan kinerja yang signifikan.

Jadi meskipun "terlihat seperti grafik", saya tidak bisa membuat DBMS grafik memprosesnya dengan cepat. Saya berasumsi sepenuhnya bahwa ada sesuatu yang tidak saya ketahui dan JanusGraph dapat dibuat untuk melakukan pencarian ini dalam sepersekian detik, namun saya tidak dapat melakukannya.

Karena masalahnya masih perlu diselesaikan, saya mulai memikirkan tentang GABUNG dan Pivot tabel, yang tidak menginspirasi optimisme dalam hal keanggunan, namun bisa menjadi pilihan yang sepenuhnya bisa diterapkan dalam praktiknya.

Proyek kami sudah menggunakan Apache ClickHouse, jadi saya memutuskan untuk menguji penelitian saya pada DBMS analitis ini.

ClickHouse dikerahkan menggunakan resep sederhana:

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

Saya membuat database dan tabel di dalamnya seperti ini:

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

Saya mengisinya dengan data menggunakan skrip berikut:

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
    )

Karena sisipan datang dalam jumlah banyak, pengisian menjadi jauh lebih cepat dibandingkan JanusGraph.

Membuat dua kueri menggunakan GABUNG. Untuk berpindah dari titik A ke titik 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

Untuk melewati 3 poin:

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

Permintaannya tentu saja terlihat cukup menakutkan, untuk penggunaan nyata, Anda perlu membuat perangkat lunak generator harness. Namun, mereka bekerja dan bekerja dengan cepat. Permintaan pertama dan kedua diselesaikan dalam waktu kurang dari 0.1 detik. Berikut adalah contoh waktu eksekusi query untuk count(*) yang melewati 3 titik:

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

Catatan tentang IOPS. Saat melakukan populasi data, JanusGraph menghasilkan jumlah IOPS yang cukup tinggi (1000-1300 untuk empat thread populasi data) dan IOWAIT yang cukup tinggi. Pada saat yang sama, ClickHouse menghasilkan beban minimal pada subsistem disk.

Kesimpulan

Kami memutuskan untuk menggunakan ClickHouse untuk melayani permintaan jenis ini. Kami selalu dapat lebih mengoptimalkan kueri menggunakan tampilan material dan paralelisasi dengan melakukan pra-pemrosesan aliran peristiwa menggunakan Apache Flink sebelum memuatnya ke ClickHouse.

Performanya sangat bagus sehingga kami mungkin tidak perlu memikirkan tentang memutar tabel secara terprogram. Sebelumnya, kami harus melakukan pivot data yang diambil dari Vertica melalui upload ke Apache Parket.

Sayangnya, upaya lain untuk menggunakan DBMS grafik tidak berhasil. Menurut saya JanusGraph tidak memiliki ekosistem ramah yang memudahkan untuk mendapatkan informasi terbaru tentang produk. Pada saat yang sama, untuk mengkonfigurasi server, digunakan cara tradisional Java, yang akan membuat orang yang tidak terbiasa dengan Java menangis darah:

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}

Saya berhasil secara tidak sengaja "meletakkan" JanusGraph versi BerkeleyDB.

Dokumentasinya cukup buruk dalam hal indeks, karena mengelola indeks mengharuskan Anda melakukan perdukunan yang agak aneh di Groovy. Misalnya, membuat indeks harus dilakukan dengan menulis kode di konsol Gremlin (yang, omong-omong, tidak langsung berfungsi). Dari dokumentasi resmi JanusGraph :

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

penutup

Bisa dibilang, eksperimen di atas adalah perbandingan antara hangat dan lembut. Jika Anda memikirkannya, DBMS grafik melakukan operasi lain untuk mendapatkan hasil yang sama. Namun, sebagai bagian dari pengujian, saya juga melakukan eksperimen dengan permintaan seperti:

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

yang mencerminkan jarak berjalan kaki. Namun, bahkan pada data seperti itu, grafik DBMS menunjukkan hasil yang melampaui beberapa detik... Hal ini, tentu saja, disebabkan oleh fakta bahwa terdapat jalur seperti 0 -> X -> Y ... -> 1, yang juga diperiksa oleh mesin grafik.

Bahkan untuk pertanyaan seperti:

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

Saya tidak bisa mendapatkan respons produktif dengan waktu pemrosesan kurang dari satu detik.

Pesan moral dari cerita ini adalah bahwa ide yang indah dan pemodelan paradigmatik tidak memberikan hasil yang diinginkan, yang ditunjukkan dengan efisiensi yang jauh lebih tinggi menggunakan contoh ClickHouse. Kasus penggunaan yang disajikan dalam artikel ini merupakan anti-pola yang jelas untuk DBMS grafik, meskipun tampaknya cocok untuk pemodelan dalam paradigmanya.

Sumber: www.habr.com

Tambah komentar