Eksperimen menguji kebolehgunaan DBMS graf JanusGraph untuk menyelesaikan masalah mencari laluan yang sesuai

Eksperimen menguji kebolehgunaan DBMS graf JanusGraph untuk menyelesaikan masalah mencari laluan yang sesuai

Hai semua. Kami sedang membangunkan produk untuk analisis trafik luar talian. Projek ini mempunyai tugas yang berkaitan dengan analisis statistik laluan pelawat merentasi wilayah.

Sebagai sebahagian daripada tugas ini, pengguna boleh bertanya pertanyaan sistem jenis berikut:

  • berapa ramai pelawat yang lalu dari kawasan "A" ke kawasan "B";
  • berapa ramai pelawat yang melalui kawasan "A" ke kawasan "B" melalui kawasan "C" dan kemudian melalui kawasan "D";
  • berapa lama masa yang diambil untuk jenis pelawat tertentu untuk pergi dari kawasan "A" ke kawasan "B".

dan beberapa pertanyaan analitikal yang serupa.

Pergerakan pelawat merentasi kawasan adalah graf terarah. Selepas membaca Internet, saya mendapati bahawa DBMS graf juga digunakan untuk laporan analisis. Saya mempunyai keinginan untuk melihat bagaimana DBMS graf akan menangani pertanyaan sedemikian (TL; DR; teruk).

Saya memilih untuk menggunakan DBMS JanusGraph, sebagai wakil cemerlang bagi DBMS sumber terbuka graf, yang bergantung pada timbunan teknologi matang, yang (pada pendapat saya) harus memberikannya ciri-ciri operasi yang baik:

  • Bahagian belakang storan BerkeleyDB, Apache Cassandra, Scylla;
  • indeks kompleks boleh disimpan di Lucene, Elasticsearch, Solr.

Pengarang JanusGraph menulis bahawa ia sesuai untuk OLTP dan OLAP.

Saya telah bekerja dengan BerkeleyDB, Apache Cassandra, Scylla dan ES, dan produk ini sering digunakan dalam sistem kami, jadi saya optimistik untuk menguji DBMS graf ini. Saya mendapati pelik untuk memilih BerkeleyDB berbanding RocksDB, tetapi itu mungkin disebabkan oleh keperluan transaksi. Walau apa pun, untuk penggunaan produk boleh skala, dicadangkan untuk menggunakan hujung belakang pada Cassandra atau Scylla.

Saya tidak menganggap Neo4j kerana pengelompokan memerlukan versi komersial, iaitu produk tidak terbuka.

DBMS Graf berkata: "Jika ia kelihatan seperti graf, layan ia seperti graf!" - kecantikan!

Mula-mula, saya melukis graf, yang dibuat tepat mengikut kanun graf DBMS:

Eksperimen menguji kebolehgunaan DBMS graf JanusGraph untuk menyelesaikan masalah mencari laluan yang sesuai

Ada intipati Zone, bertanggungjawab ke atas kawasan tersebut. Jika ZoneStep kepunyaan ini Zone, kemudian dia merujuk kepadanya. Pada intipati Area, ZoneTrack, Person Jangan ambil perhatian, mereka tergolong dalam domain dan tidak dianggap sebagai sebahagian daripada ujian. Secara keseluruhan, pertanyaan carian rantaian untuk struktur graf sedemikian akan kelihatan seperti:

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

Apa dalam bahasa Rusia adalah seperti ini: cari Zon dengan ID=0, ambil semua bucu dari mana tepi pergi kepadanya (ZoneStep), hentak tanpa berpatah balik sehingga anda menemui ZoneSteps yang terdapat tepi ke Zon dengan ID=19, kira nombor rantai tersebut.

Saya tidak berpura-pura mengetahui semua kerumitan carian pada graf, tetapi pertanyaan ini dihasilkan berdasarkan buku ini (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Saya memuatkan 50 ribu trek dengan panjang antara 3 hingga 20 mata ke dalam pangkalan data graf JanusGraph menggunakan bahagian belakang BerkeleyDB, mencipta indeks mengikut kepimpinan.

Skrip muat turun 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 teras dan 16 GB RAM pada SSD. JanusGraph telah digunakan menggunakan arahan ini:

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

Dalam kes ini, data dan indeks yang digunakan untuk carian padanan tepat disimpan dalam BerkeleyDB. Setelah melaksanakan permintaan yang diberikan sebelum ini, saya menerima masa yang sama dengan beberapa puluh saat.

Dengan menjalankan 4 skrip di atas secara selari, saya berjaya mengubah DBMS menjadi labu dengan aliran ceria jejak tindanan Java (dan kita semua suka membaca jejak tindanan Java) dalam log Docker.

Selepas beberapa pemikiran, saya memutuskan untuk memudahkan rajah graf kepada yang berikut:

Eksperimen menguji kebolehgunaan DBMS graf JanusGraph untuk menyelesaikan masalah mencari laluan yang sesuai

Memutuskan bahawa mencari mengikut atribut entiti akan menjadi lebih pantas daripada mencari mengikut tepi. Akibatnya, permintaan saya bertukar menjadi seperti berikut:

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

Apa dalam bahasa Rusia adalah seperti ini: cari ZoneStep dengan ID=0, hentak tanpa kembali sehingga anda menemui ZoneStep dengan ID=19, kira bilangan rantai tersebut.

Saya juga memudahkan skrip pemuatan yang diberikan di atas agar tidak membuat sambungan yang tidak perlu, mengehadkan diri saya kepada atribut.

Permintaan itu masih mengambil masa beberapa saat untuk diselesaikan, yang tidak boleh diterima sepenuhnya untuk tugas kami, kerana ia sama sekali tidak sesuai untuk tujuan permintaan AdHoc dalam apa jua bentuk.

Saya cuba menggunakan JanusGraph menggunakan Scylla sebagai pelaksanaan Cassandra terpantas, tetapi ini juga tidak membawa kepada sebarang perubahan prestasi yang ketara.

Jadi walaupun pada hakikatnya "ia kelihatan seperti graf", saya tidak dapat mendapatkan graf DBMS untuk memprosesnya dengan cepat. Saya mengandaikan sepenuhnya bahawa terdapat sesuatu yang saya tidak tahu dan JanusGraph boleh dibuat untuk melakukan carian ini dalam sepersekian saat, namun, saya tidak dapat melakukannya.

Memandangkan masalah itu masih perlu diselesaikan, saya mula memikirkan tentang JOIN dan Pivot jadual, yang tidak menimbulkan keyakinan dari segi keanggunan, tetapi boleh menjadi pilihan yang boleh digunakan sepenuhnya dalam amalan.

Projek kami sudah menggunakan Apache ClickHouse, jadi saya memutuskan untuk menguji penyelidikan saya tentang DBMS analitikal ini.

Digunakan ClickHouse menggunakan resipi mudah:

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 mencipta pangkalan data dan jadual 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
    )

Memandangkan sisipan datang secara berkelompok, pengisian adalah lebih cepat daripada JanusGraph.

Membina dua pertanyaan menggunakan JOIN. Untuk bergerak 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 melalui 3 mata:

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

Permintaan, sudah tentu, kelihatan agak menakutkan; untuk kegunaan sebenar, anda perlu mencipta abah-abah penjana perisian. Walau bagaimanapun, mereka bekerja dan mereka bekerja dengan cepat. Kedua-dua permintaan pertama dan kedua diselesaikan dalam masa kurang daripada 0.1 saat. Berikut ialah contoh masa pelaksanaan pertanyaan untuk count(*) yang melalui 3 mata:

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

Nota tentang IOPS. Apabila mengisi data, JanusGraph menjana bilangan IOPS yang agak tinggi (1000-1300 untuk empat utas populasi data) dan IOWAIT agak tinggi. Pada masa yang sama, ClickHouse menjana beban minimum pada subsistem cakera.

Kesimpulan

Kami memutuskan untuk menggunakan ClickHouse untuk melayani permintaan jenis ini. Kami sentiasa boleh mengoptimumkan pertanyaan dengan lebih lanjut menggunakan paparan terwujud dan paralelisasi dengan pra-memproses strim acara menggunakan Apache Flink sebelum memuatkannya ke ClickHouse.

Prestasinya sangat baik sehingga kita mungkin tidak perlu memikirkan tentang memutar jadual secara pemrograman. Sebelum ini, kami perlu melakukan pangsi data yang diambil daripada Vertica melalui muat naik ke Apache Parket.

Malangnya, percubaan lain untuk menggunakan DBMS graf tidak berjaya. Saya tidak mendapati JanusGraph mempunyai ekosistem yang mesra yang menjadikannya mudah untuk mengetahui dengan pantas dengan produk. Pada masa yang sama, untuk mengkonfigurasi pelayan, cara Java tradisional digunakan, yang akan membuat orang yang tidak biasa dengan Java menangis air mata 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 berjaya "meletakkan" versi BerkeleyDB JanusGraph.

Dokumentasinya agak bengkok dari segi indeks, kerana menguruskan indeks memerlukan anda melakukan beberapa bomoh yang agak pelik dalam Groovy. Sebagai contoh, mencipta indeks mesti dilakukan dengan menulis kod dalam konsol Gremlin (yang, dengan cara itu, tidak berfungsi di luar kotak). Daripada dokumentasi rasmi 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()

afterword

Dari satu segi, percubaan di atas adalah perbandingan antara hangat dan lembut. Jika anda memikirkannya, graf DBMS menjalankan operasi lain untuk mendapatkan hasil yang sama. Walau bagaimanapun, sebagai sebahagian daripada ujian, saya juga menjalankan percubaan 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. Walau bagaimanapun, walaupun pada data sedemikian, graf DBMS menunjukkan hasil yang melebihi beberapa saat... Ini, sudah tentu, disebabkan oleh fakta bahawa terdapat laluan seperti 0 -> X -> Y ... -> 1, yang juga disemak oleh enjin graf.

Walaupun untuk pertanyaan seperti:

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

Saya tidak dapat mendapat respons yang produktif dengan masa pemprosesan kurang daripada satu saat.

Moral cerita ini ialah idea yang indah dan pemodelan paradigmatik tidak membawa kepada hasil yang diinginkan, yang ditunjukkan dengan kecekapan yang lebih tinggi menggunakan contoh ClickHouse. Kes penggunaan yang dibentangkan dalam artikel ini ialah anti-corak yang jelas untuk DBMS graf, walaupun nampaknya sesuai untuk pemodelan dalam paradigma mereka.

Sumber: www.habr.com

Tambah komen