تجربة اختبار إمكانية تطبيق نظام JanusGraph DBMS لحل مشكلة إيجاد المسارات المناسبة

تجربة اختبار إمكانية تطبيق نظام JanusGraph DBMS لحل مشكلة إيجاد المسارات المناسبة

أهلاً بكم. نحن نعمل على تطوير منتج لتحليل حركة المرور دون اتصال بالإنترنت. المشروع له مهمة تتعلق بالتحليل الإحصائي لمسارات الزوار عبر المناطق.

كجزء من هذه المهمة، يمكن للمستخدمين طرح استعلامات النظام من النوع التالي:

  • كم عدد الزوار الذين مروا من المنطقة "أ" إلى المنطقة "ب"؟
  • كم عدد الزوار الذين مروا من المنطقة "أ" إلى المنطقة "ب" مروراً بالمنطقة "ج" ومن ثم عبر المنطقة "د"؟
  • كم من الوقت يستغرق سفر نوع معين من الزوار من المنطقة "أ" إلى المنطقة "ب".

وعدد من الاستفسارات التحليلية المماثلة.

حركة الزائر عبر المناطق عبارة عن رسم بياني موجه. بعد قراءة الإنترنت، اكتشفت أن أنظمة إدارة قواعد البيانات البيانية تُستخدم أيضًا في التقارير التحليلية. كانت لدي رغبة في معرفة كيف ستتعامل أنظمة إدارة قواعد البيانات البيانية مع مثل هذه الاستعلامات (TL، DR. ضعيف).

اخترت استخدام نظام إدارة قواعد البيانات (DBMS). جانوسجراف، كممثل بارز لنظام إدارة قواعد البيانات مفتوح المصدر للرسم البياني، والذي يعتمد على مجموعة من التقنيات الناضجة، والتي (في رأيي) يجب أن تزوده بخصائص تشغيلية لائقة:

  • الواجهة الخلفية للتخزين BerkeleyDB، وApache Cassandra، وScylla؛
  • يمكن تخزين الفهارس المعقدة في Lucene وElasticsearch وSolr.

يكتب مؤلفو JanusGraph أنه مناسب لكل من OLTP وOLAP.

لقد عملت مع BerkeleyDB وApache Cassandra وScylla وES، وغالبًا ما تُستخدم هذه المنتجات في أنظمتنا، لذلك كنت متفائلًا بشأن اختبار نظام إدارة قواعد البيانات هذا الرسم البياني. لقد وجدت أنه من الغريب اختيار BerkeleyDB بدلاً من RocksDB، ولكن ربما يرجع ذلك إلى متطلبات المعاملة. على أية حال، من أجل استخدام المنتج القابل للتطوير، يُقترح استخدام الواجهة الخلفية على Cassandra أو Scylla.

لم أفكر في Neo4j لأن التجميع يتطلب إصدارًا تجاريًا، أي أن المنتج ليس مفتوح المصدر.

تقول أنظمة إدارة قواعد البيانات البيانية: "إذا كان يبدو وكأنه رسم بياني، فتعامل معه كرسم بياني!" - جمال!

أولاً، قمت برسم رسم بياني، والذي تم تصميمه وفقًا لشرائع الرسم البياني لأنظمة إدارة قواعد البيانات (DBMSs):

تجربة اختبار إمكانية تطبيق نظام JanusGraph DBMS لحل مشكلة إيجاد المسارات المناسبة

هناك جوهر Zone، المسؤول عن المنطقة. لو ZoneStep ينتمي إلى هذا Zone، ثم يشير إليه. على الجوهر Area, ZoneTrack, Person لا تنتبه، فهي تابعة للمجال ولا تعتبر جزءًا من الاختبار. في المجمل، سيبدو استعلام البحث المتسلسل لهيكل الرسم البياني هذا كما يلي:

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

ما هو باللغة الروسية هو شيء من هذا القبيل: ابحث عن منطقة بمعرف = 0، خذ جميع القمم التي تذهب منها الحافة إليها (ZoneStep)، دوس دون الرجوع إلى الوراء حتى تجد تلك ZoneSteps التي توجد منها حافة للمنطقة ذات المعرف = 19، قم بإحصاء عدد هذه السلاسل.

لا أدعي أنني أعرف كل تعقيدات البحث في الرسوم البيانية، ولكن تم إنشاء هذا الاستعلام بناءً على هذا الكتاب (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

لقد قمت بتحميل 50 ألف مسار يتراوح طولها من 3 إلى 20 نقطة في قاعدة بيانات الرسم البياني JanusGraph باستخدام الواجهة الخلفية لـ BerkeleyDB، وقمت بإنشاء فهارس وفقًا لـ قيادة.

سكريبت تحميل بايثون:


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)

استخدمنا جهازًا افتراضيًا يحتوي على 4 مراكز وذاكرة وصول عشوائي (RAM) سعة 16 جيجابايت على محرك أقراص SSD. تم نشر JanusGraph باستخدام هذا الأمر:

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

في هذه الحالة، يتم تخزين البيانات والفهارس المستخدمة في عمليات البحث ذات المطابقة التامة في BerkeleyDB. بعد تنفيذ الطلب المقدم سابقًا، تلقيت وقتًا يساوي عدة عشرات من الثواني.

من خلال تشغيل البرامج النصية الأربعة المذكورة أعلاه بالتوازي، تمكنت من تحويل نظام إدارة قواعد البيانات (DBMS) إلى قرع مع دفق مبهج من Java stacktraces (ونحن جميعًا نحب قراءة Java stacktraces) في سجلات Docker.

بعد تفكير طويل، قررت تبسيط الرسم البياني إلى ما يلي:

تجربة اختبار إمكانية تطبيق نظام JanusGraph DBMS لحل مشكلة إيجاد المسارات المناسبة

تحديد أن البحث حسب سمات الكيان سيكون أسرع من البحث حسب الحواف. ونتيجة لذلك تحول طلبي إلى ما يلي:

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

ما هو باللغة الروسية هو شيء من هذا القبيل: ابحث عن ZoneStep بالمعرف = 0، دوس دون الرجوع إلى الوراء حتى تجد ZoneStep بالمعرف = 19، احسب عدد هذه السلاسل.

لقد قمت أيضًا بتبسيط نص التحميل المذكور أعلاه حتى لا أقوم بإنشاء اتصالات غير ضرورية، وأقتصر على السمات.

لا يزال الطلب يستغرق عدة ثوانٍ حتى يكتمل، وهو أمر غير مقبول تمامًا لمهمتنا، لأنه لم يكن مناسبًا على الإطلاق لأغراض الطلبات المخصصة من أي نوع.

لقد حاولت نشر JanusGraph باستخدام Scylla باعتباره أسرع تطبيق لـ Cassandra، لكن هذا أيضًا لم يؤدي إلى أي تغييرات مهمة في الأداء.

لذلك على الرغم من حقيقة أنه "يبدو وكأنه رسم بياني"، لم أتمكن من جعل نظام إدارة قواعد البيانات (DBMS) للرسم البياني يعالجه بسرعة. أفترض تمامًا أن هناك شيئًا لا أعرفه وأنه يمكن جعل JanusGraph يقوم بهذا البحث في جزء من الثانية، ومع ذلك، لم أتمكن من القيام بذلك.

نظرًا لأن المشكلة لا تزال بحاجة إلى حل، فقد بدأت بالتفكير في وصلات ومحاور الجداول، والتي لم تكن توحي بالتفاؤل من حيث الأناقة، ولكنها يمكن أن تكون خيارًا عمليًا تمامًا في الممارسة العملية.

يستخدم مشروعنا بالفعل Apache ClickHouse، لذلك قررت اختبار بحثي حول نظام إدارة قواعد البيانات التحليلي هذا.

تم نشر 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

لقد قمت بإنشاء قاعدة بيانات وجدول فيها مثل هذا:

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

لقد ملأتها بالبيانات باستخدام البرنامج النصي التالي:

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
    )

نظرًا لأن الإدخالات تأتي على دفعات، كان الملء أسرع بكثير من JanusGraph.

تم إنشاء استعلامين باستخدام JOIN. للانتقال من النقطة أ إلى النقطة ب:

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 نقاط:

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

الطلبات، بالطبع، تبدو مخيفة للغاية، للاستخدام الحقيقي، تحتاج إلى إنشاء مجموعة من مولدات البرامج. ومع ذلك، فإنهم يعملون ويعملون بسرعة. يتم إكمال الطلبين الأول والثاني في أقل من 0.1 ثانية. فيما يلي مثال على وقت تنفيذ الاستعلام لعدد (*) يمر عبر 3 نقاط:

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. عند تعبئة البيانات، قام JanusGraph بإنشاء عدد كبير إلى حد ما من IOPS (1000-1300 لأربعة مؤشرات ترابط لعدد البيانات) وكان IOWAIT مرتفعًا جدًا. في الوقت نفسه، أنشأ ClickHouse الحد الأدنى من التحميل على النظام الفرعي للقرص.

اختتام

قررنا استخدام ClickHouse لخدمة هذا النوع من الطلبات. يمكننا دائمًا تحسين الاستعلامات بشكل أكبر باستخدام طرق العرض المادية والتوازي من خلال المعالجة المسبقة لتدفق الأحداث باستخدام Apache Flink قبل تحميلها في ClickHouse.

الأداء جيد جدًا لدرجة أننا ربما لن نضطر إلى التفكير في تدوير الجداول برمجيًا. في السابق، كان علينا إجراء محاور للبيانات المستردة من Vertica عبر التحميل إلى Apache Parquet.

لسوء الحظ، لم تنجح محاولة أخرى لاستخدام نظام إدارة قواعد البيانات البياني (DBMS). لم أجد أن لدى JanusGraph نظامًا بيئيًا ودودًا يجعل من السهل التعامل مع المنتج بسرعة. في الوقت نفسه، لتكوين الخادم، يتم استخدام طريقة Java التقليدية، والتي ستجعل الأشخاص الذين ليسوا على دراية بـ Java يبكون بالدموع:

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}

تمكنت عن طريق الخطأ من "وضع" إصدار BerkeleyDB من JanusGraph.

الوثائق ملتوية تمامًا من حيث الفهارس، نظرًا لأن إدارة الفهارس تتطلب منك إجراء بعض الشامانية الغريبة إلى حد ما في Groovy. على سبيل المثال، يجب أن يتم إنشاء فهرس عن طريق كتابة التعليمات البرمجية في وحدة تحكم Gremlin (والتي، بالمناسبة، لا تعمل خارج الصندوق). من وثائق 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()

خاتمة

بمعنى ما، التجربة المذكورة أعلاه هي مقارنة بين الدافئة والناعمة. إذا فكرت في الأمر، فإن الرسم البياني لنظام إدارة قواعد البيانات (DBMS) ينفذ عمليات أخرى للحصول على نفس النتائج. ومع ذلك، كجزء من الاختبارات، أجريت أيضًا تجربة بطلب مثل:

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

مما يعكس مسافة المشي. ومع ذلك، حتى في مثل هذه البيانات، أظهر الرسم البياني DBMS نتائج تتجاوز بضع ثوان... وهذا بالطبع يرجع إلى حقيقة وجود مسارات مثل 0 -> X -> Y ... -> 1، والذي فحصه محرك الرسم البياني أيضًا.

حتى بالنسبة لاستعلام مثل:

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

لم أتمكن من الحصول على استجابة مثمرة خلال وقت معالجة أقل من ثانية.

المغزى من القصة هو أن الفكرة الجميلة والنمذجة النموذجية لا تؤدي إلى النتيجة المرجوة، وهو ما يظهر بكفاءة أعلى بكثير باستخدام مثال ClickHouse. حالة الاستخدام المعروضة في هذه المقالة هي نمط مضاد واضح لأنظمة إدارة قواعد البيانات البيانية، على الرغم من أنها تبدو مناسبة للنمذجة في نموذجها.

المصدر: www.habr.com

إضافة تعليق