Thử nghiệm kiểm tra khả năng ứng dụng DBMS đồ thị JanusGraph trong việc giải bài toán tìm đường đi phù hợp

Thử nghiệm kiểm tra khả năng ứng dụng DBMS đồ thị JanusGraph trong việc giải bài toán tìm đường đi phù hợp

Chào mọi người. Chúng tôi đang phát triển một sản phẩm để phân tích lưu lượng truy cập ngoại tuyến. Dự án có nhiệm vụ liên quan đến phân tích thống kê các tuyến đường của du khách giữa các vùng.

Là một phần của nhiệm vụ này, người dùng có thể hỏi các truy vấn hệ thống thuộc loại sau:

  • có bao nhiêu du khách đi từ khu vực “A” đến khu vực “B”;
  • có bao nhiêu du khách đi từ khu “A” đến khu “B” qua khu “C” rồi qua khu “D”;
  • mất bao lâu để một loại khách nhất định đi từ khu vực “A” đến khu vực “B”.

và một số truy vấn phân tích tương tự.

Chuyển động của khách truy cập qua các khu vực là một biểu đồ có hướng. Sau khi đọc trên Internet, tôi phát hiện ra rằng DBMS đồ thị cũng được sử dụng cho các báo cáo phân tích. Tôi mong muốn xem các DBMS đồ thị sẽ xử lý các truy vấn như thế nào (TL; DR; kém).

Tôi đã chọn sử dụng DBMS Đồ thị Janus, với tư cách là một đại diện xuất sắc của DBMS nguồn mở đồ thị, dựa trên một loạt các công nghệ hoàn thiện, (theo ý kiến ​​​​của tôi) sẽ cung cấp cho nó các đặc điểm hoạt động tốt:

  • Phần phụ trợ lưu trữ BerkeleyDB, Apache Cassandra, Scylla;
  • các chỉ mục phức tạp có thể được lưu trữ trong Lucene, Elaticsearch, Solr.

Các tác giả của JanusGraph viết rằng nó phù hợp cho cả OLTP và OLAP.

Tôi đã làm việc với BerkeleyDB, Apache Cassandra, Scylla và ES và những sản phẩm này thường được sử dụng trong hệ thống của chúng tôi, vì vậy tôi rất lạc quan khi thử nghiệm DBMS biểu đồ này. Tôi thấy thật kỳ lạ khi chọn BerkeleyDB thay vì RocksDB, nhưng đó có thể là do các yêu cầu giao dịch. Trong mọi trường hợp, để mở rộng khả năng sử dụng sản phẩm, bạn nên sử dụng phần phụ trợ trên Cassandra hoặc Scylla.

Tôi không xem xét Neo4j vì việc phân cụm yêu cầu phiên bản thương mại, tức là sản phẩm không phải là nguồn mở.

DBMS đồ thị nói: “Nếu nó trông giống một biểu đồ, hãy coi nó như một biểu đồ!” - sắc đẹp!

Đầu tiên, tôi vẽ một biểu đồ, được tạo chính xác theo các tiêu chuẩn của DBMS biểu đồ:

Thử nghiệm kiểm tra khả năng ứng dụng DBMS đồ thị JanusGraph trong việc giải bài toán tìm đường đi phù hợp

Có một bản chất Zone, phụ trách khu vực. Nếu như ZoneStep thuộc về cái này Zone, sau đó anh ấy đề cập đến nó. Về bản chất Area, ZoneTrack, Person Đừng chú ý, chúng thuộc về miền và không được coi là một phần của bài kiểm tra. Nhìn chung, một truy vấn tìm kiếm chuỗi cho cấu trúc biểu đồ như vậy sẽ trông như sau:

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

Trong tiếng Nga nó giống như thế này: tìm một Vùng có ID=0, lấy tất cả các đỉnh mà từ đó một cạnh đi đến nó (ZoneStep), dậm chân mà không quay lại cho đến khi bạn tìm thấy các ZoneSteps mà từ đó có một cạnh cho Vùng có ID=19, đếm số chuỗi như vậy.

Tôi không giả vờ biết tất cả sự phức tạp của việc tìm kiếm trên biểu đồ, nhưng truy vấn này được tạo dựa trên cuốn sách này (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Tôi đã tải 50 nghìn bản nhạc có độ dài từ 3 đến 20 điểm vào cơ sở dữ liệu đồ thị JanusGraph bằng chương trình phụ trợ BerkeleyDB, tạo các chỉ mục theo Khả năng lãnh đạo.

Tập lệnh tải xuống 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)

Chúng tôi đã sử dụng VM có 4 lõi và RAM 16 GB trên ổ SSD. JanusGraph đã được triển khai bằng lệnh này:

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

Trong trường hợp này, dữ liệu và chỉ mục được sử dụng để tìm kiếm đối sánh chính xác được lưu trữ trong BerkeleyDB. Thực hiện yêu cầu được đưa ra trước đó, tôi nhận được thời gian bằng vài chục giây.

Bằng cách chạy song song 4 tập lệnh trên, tôi đã biến DBMS thành một quả bí ngô với một luồng các dấu vết ngăn xếp Java thú vị (và tất cả chúng ta đều thích đọc dấu vết ngăn xếp Java) trong nhật ký Docker.

Sau một hồi suy nghĩ, tôi quyết định đơn giản hóa sơ đồ đồ thị như sau:

Thử nghiệm kiểm tra khả năng ứng dụng DBMS đồ thị JanusGraph trong việc giải bài toán tìm đường đi phù hợp

Quyết định rằng tìm kiếm theo thuộc tính thực thể sẽ nhanh hơn tìm kiếm theo cạnh. Kết quả là, yêu cầu của tôi biến thành như sau:

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

Tiếng Nga đại loại như thế này: tìm ZoneStep có ID=0, dậm chân mà không quay lại cho đến khi bạn tìm thấy ZoneStep có ID=19, đếm số chuỗi như vậy.

Tôi cũng đã đơn giản hóa tập lệnh tải được đưa ra ở trên để không tạo ra các kết nối không cần thiết, giới hạn bản thân trong các thuộc tính.

Yêu cầu vẫn mất vài giây để hoàn thành, điều này hoàn toàn không thể chấp nhận được đối với nhiệm vụ của chúng tôi vì nó hoàn toàn không phù hợp với bất kỳ mục đích nào của yêu cầu AdHoc.

Tôi đã thử triển khai JanusGraph bằng Scylla như cách triển khai Cassandra nhanh nhất nhưng điều này cũng không dẫn đến bất kỳ thay đổi đáng kể nào về hiệu suất.

Vì vậy, mặc dù thực tế là "nó trông giống như một biểu đồ", tôi không thể lấy DBMS biểu đồ để xử lý nhanh chóng. Tôi hoàn toàn cho rằng có điều gì đó mà tôi không biết và JanusGraph có thể được tạo để thực hiện tìm kiếm này trong chưa đầy một giây, tuy nhiên, tôi đã không thể thực hiện được.

Vì vấn đề vẫn cần được giải quyết, tôi bắt đầu nghĩ đến THAM GIA và Xoay bảng, điều này không truyền cảm hứng lạc quan về mặt trang nhã nhưng có thể là một lựa chọn hoàn toàn khả thi trong thực tế.

Dự án của chúng tôi đã sử dụng Apache ClickHouse, vì vậy tôi quyết định thử nghiệm nghiên cứu của mình về DBMS phân tích này.

Đã triển khai ClickHouse bằng công thức đơn giản:

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

Tôi đã tạo một cơ sở dữ liệu và một bảng trong đó như thế này:

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

Tôi đã điền dữ liệu vào đó bằng đoạn script sau:

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
    )

Vì các phần chèn được thực hiện theo đợt nên việc điền nhanh hơn nhiều so với JanusGraph.

Xây dựng hai truy vấn bằng JOIN. Để di chuyển từ điểm A đến điểm 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

Đi qua 3 điểm:

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

Tất nhiên, các yêu cầu trông khá đáng sợ, để sử dụng thực tế, bạn cần tạo một bộ khai thác trình tạo phần mềm. Tuy nhiên, chúng hoạt động và chúng hoạt động nhanh chóng. Cả yêu cầu thứ nhất và thứ hai đều được hoàn thành trong vòng chưa đầy 0.1 giây. Dưới đây là ví dụ về thời gian thực hiện truy vấn cho count(*) đi qua 3 điểm:

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

Lưu ý về IOPS. Khi điền dữ liệu, JanusGraph đã tạo ra số lượng IOPS khá cao (1000-1300 cho bốn luồng tổng hợp dữ liệu) và IOWAIT khá cao. Đồng thời, ClickHouse tạo ra tải tối thiểu trên hệ thống con đĩa.

Kết luận

Chúng tôi quyết định sử dụng ClickHouse để phục vụ loại yêu cầu này. Chúng tôi luôn có thể tối ưu hóa hơn nữa các truy vấn bằng cách sử dụng các chế độ xem cụ thể hóa và song song hóa bằng cách xử lý trước luồng sự kiện bằng Apache Flink trước khi tải chúng vào ClickHouse.

Hiệu suất tốt đến mức có lẽ chúng ta thậm chí sẽ không phải nghĩ đến việc xoay bảng theo chương trình. Trước đây, chúng tôi phải thực hiện xoay vòng dữ liệu được lấy từ Vertica thông qua tải lên Apache Parquet.

Thật không may, một nỗ lực khác để sử dụng DBMS đồ thị đã không thành công. Tôi không thấy JanusGraph có một hệ sinh thái thân thiện giúp tôi dễ dàng bắt kịp tốc độ của sản phẩm. Đồng thời, để cấu hình máy chủ, cách Java truyền thống được sử dụng sẽ khiến những người không quen với Java phải khóc ra máu:

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}

Tôi đã vô tình "đặt" phiên bản BerkeleyDB của JanusGraph.

Tài liệu này khá quanh co về mặt chỉ mục, vì việc quản lý chỉ mục yêu cầu bạn thực hiện một số pháp sư khá kỳ lạ trong Groovy. Ví dụ: việc tạo chỉ mục phải được thực hiện bằng cách viết mã trong bảng điều khiển Gremlin (nhân tiện, bảng điều khiển này không hoạt động bình thường). Từ tài liệu chính thức của 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()

bạt

Ở một khía cạnh nào đó, thí nghiệm trên là sự so sánh giữa ấm áp và mềm mại. Nếu bạn nghĩ về điều đó, DBMS đồ thị sẽ thực hiện các hoạt động khác để thu được kết quả tương tự. Tuy nhiên, là một phần của thử nghiệm, tôi cũng đã tiến hành thử nghiệm với yêu cầu như:

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

phản ánh khoảng cách đi bộ. Tuy nhiên, ngay cả trên dữ liệu như vậy, biểu đồ DBMS đã hiển thị kết quả vượt quá vài giây... Tất nhiên, điều này là do thực tế là đã có những đường dẫn như 0 -> X -> Y ... -> 1, mà công cụ đồ thị cũng đã kiểm tra.

Ngay cả đối với một truy vấn như:

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

Tôi không thể nhận được phản hồi hiệu quả với thời gian xử lý dưới một giây.

Ý nghĩa của câu chuyện là một ý tưởng hay và mô hình mẫu không dẫn đến kết quả mong muốn, điều này được chứng minh với hiệu quả cao hơn nhiều khi sử dụng ví dụ về ClickHouse. Ca sử dụng được trình bày trong bài viết này là một phản mẫu rõ ràng cho các DBMS đồ thị, mặc dù nó có vẻ phù hợp để lập mô hình trong mô hình của chúng.

Nguồn: www.habr.com

Thêm một lời nhận xét