測試 JanusGraph 圖 DBMS 解決尋找合適路徑問題的適用性的實驗

測試 JanusGraph 圖 DBMS 解決尋找合適路徑問題的適用性的實驗

大家好。 我們正在開發一款用於離線流量分析的產品。 該計畫的任務是對跨地區的遊客路線進行統計分析。

作為此任務的一部分,使用者可以詢問以下類型的系統查詢:

  • 有多少訪客從「A」區前往「B」區;
  • 有多少訪客從區域“A”通過區域“C”到達區域“B”,然後通過區域“D”;
  • 某種類型的訪客從區域“A”前往區域“B”需要多長時間。

以及許多類似的分析查詢。

訪客跨區域的移動是一個有向圖。 上網查了一下,發現圖DBMS也可以用來做分析報告。 我很想看看圖形 DBMS 如何處理此類查詢(TL; DR; 不良)。

我選擇使用DBMS 兩面神圖作為圖開源DBMS的傑出代表,它依賴一堆成熟的技術,(在我看來)應該為其提供良好的操作特性:

  • BerkeleyDB 儲存後端、Apache Cassandra、Scylla;
  • 複雜的索引可以儲存在Lucene、Elasticsearch、Solr中。

JanusGraph 的作者寫道,它同時適用於 OLTP 和 OLAP。

我曾使用過 BerkeleyDB、Apache Cassandra、Scylla 和 ES,這些產品經常在我們的系統中使用,所以我對測試這張圖 DBMS 持樂觀態度。 我發現選擇 BerkeleyDB 而不是 RocksDB 很奇怪,但這可能是由於事務需求所致。 無論如何,為了可擴展的產品使用,建議使用 Cassandra 或 Scylla 上的後端。

我沒有考慮Neo4j,因為叢集需要商業版本,即該產品不是開源的。

圖 DBMS 說:“如果它看起來像圖,就將其視為圖!” - 美麗!

首先,我畫了一個圖表,它是完全按照圖形 DBMS 的規格製作的:

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

俄語中的內容是這樣的:找到一個 ID=0 的區域,獲取有一條邊通往該區域的所有頂點(ZoneStep),不返回地踩踏,直到找到與該區域有一條邊的那些 ZoneSteps ID=19 ,統計這樣的鏈的數量。

我並不假裝知道在圖表上搜尋的所有複雜性,但這個查詢是根據這本書產生的(https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

我使用 BerkeleyDB 後端將 50 個長度從 3 到 20 點不等的曲目載入到 JanusGraph 圖形資料庫中,根據以下內容建立索引 領導.

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)

我們使用了配備 4 核心和 16 GB RAM、SSD 的虛擬機器。 JanusGraph 使用以下命令部署:

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

在這種情況下,用於精確匹配搜尋的資料和索引儲存在 BerkeleyDB 中。 執行了先前給予的請求後,我收到了相當於幾十秒的時間。

透過並行運行上述 4 個腳本,我成功地將 DBMS 變成了一個南瓜,在 Docker 日誌中帶有令人愉悅的 Java 堆疊追蹤流(我們都喜歡閱讀 Java 堆疊追蹤)。

經過一番思考,我決定將圖表簡化為如下:

測試 JanusGraph 圖 DBMS 解決尋找合適路徑問題的適用性的實驗

決定按實體屬性搜尋比按邊搜尋更快。 結果我的要求就變成這樣:

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

俄語的意思是這樣的:找到ID=0的ZoneStep,踩下去,直到找到ID=19的ZoneStep,數一下這樣的鏈的數量。

我還簡化了上面給出的加載腳本,以免創建不必要的連接,將自己限制在屬性上。

該請求仍然需要幾秒鐘才能完成,這對於我們的任務來說是完全不可接受的,因為它根本不適合任何類型的 AdHoc 請求的目的。

我嘗試使用 Scylla 作為最快的 Cassandra 實作來部署 JanusGraph,但這也沒有導致任何重大的效能變化。

因此,儘管“它看起來像一個圖”,但我無法讓圖 DBMS 快速處理它。 我完全假設有一些我不知道的事情,並且可以讓 JanusGraph 在不到一秒鐘的時間內執行此搜索,但是,我無法做到這一點。

由於問題仍然需要解決,我開始考慮表格的 JOIN 和 Pivots,這在優雅方面並沒有激發樂觀情緒,但在實踐中可能是一個完全可行的選擇。

我們的專案已經使用 Apache ClickHouse,因此我決定在這個分析 DBMS 上測試我的研究。

使用簡單的配方部署 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 建構了兩個查詢。 從 A 點移動到 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

要經過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 秒的時間內完成。 以下是 count(*) 經過 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 來處理此類請求。 在將事件流載入到 ClickHouse 之前,我們始終可以使用 Apache Flink 預處理事件流,從而使用物化視圖和並行化進一步優化查詢。

性能非常好,我們甚至可能不需要考慮以程式設計方式旋轉表。 以前,我們必須透過上傳到 Apache Parquet 來對從 Vertica 檢索的資料進行資料透視。

不幸的是,另一次使用圖形 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 的例子以更高的效率證明了這一點。 本文中介紹的用例是圖 DBMS 的明顯反模式,儘管它似乎適合在其範例中進行建模。

來源: www.habr.com

添加評論