Експеримент кој ја тестира применливоста на DBMS графиконот JanusGraph за решавање на проблемот со наоѓање соодветни патеки

Експеримент кој ја тестира применливоста на DBMS графиконот JanusGraph за решавање на проблемот со наоѓање соодветни патеки

Здраво на сите. Развиваме производ за офлајн анализа на сообраќајот. Проектот има задача поврзана со статистичка анализа на патеките на посетителите низ регионите.

Како дел од оваа задача, корисниците можат да поставуваат системски прашања од следниот тип:

  • колку посетители поминале од областа „А“ до областа „Б“;
  • колку посетители поминале од областа „А“ во областа „Б“ низ областа „Ц“, а потоа низ областа „Д“;
  • колку време му било потребно на одреден тип посетител да патува од областа „А“ до областа „Б“.

и голем број слични аналитички прашања.

Движењето на посетителот низ областите е насочен график. По читањето на Интернет, открив дека графичките 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:

Експеримент кој ја тестира применливоста на DBMS графиконот JanusGraph за решавање на проблемот со наоѓање соодветни патеки

Постои суштина 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 од кои има раб до зоната со ИД=19, изброј го бројот такви синџири.

Не се преправам дека ги знам сите сложености на пребарувањето на графикони, но ова барање е создадено врз основа на оваа книга (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Вчитав 50 илјади песни со должина од 3 до 20 точки во базата на податоци за графикони на JanusGraph користејќи го заднината на BerkeleyDB, креирав индекси според лидерство.

Скрипта за преземање на 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)

Користивме VM со 4 јадра и 16 GB RAM на SSD. JanusGraph беше распореден со помош на оваа команда:

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

Во овој случај, податоците и индексите што се користат за пребарување на точни совпаѓања се зачувани во BerkeleyDB. Откако го извршив претходно даденото барање, добив време еднакво на неколку десетици секунди.

Со паралелно извршување на 4-те горенаведени скрипти, успеав да го претворам DBMS во тиква со весел тек на Java stacktraces (и сите ние сакаме да читаме Java stacktraces) во дневниците на Docker.

По малку размислување, решив да го поедноставам графичкиот дијаграм на следново:

Експеримент кој ја тестира применливоста на DBMS графиконот JanusGraph за решавање на проблемот со наоѓање соодветни патеки

Одлуката дека пребарувањето по атрибути на ентитети би било побрзо отколку пребарувањето по рабови. Како резултат на тоа, моето барање се претвори во следново:

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

Што на руски е нешто вака: најдете ZoneStep со ID=0, газете без да се враќате назад додека не го најдете ZoneStep со ID=19, избројте го бројот на такви синџири.

Ја поедноставив и скриптата за вчитување дадена погоре за да не создавам непотребни врски, ограничувајќи се на атрибути.

Барањето сепак траеше неколку секунди за да се заврши, што беше целосно неприфатливо за нашата задача, бидејќи воопшто не беше соодветно за целите на AdHoc барањата од било кој вид.

Се обидов да го распоредам JanusGraph користејќи Scylla како најбрза имплементација на Cassandra, но тоа исто така не доведе до некои значајни промени во перформансите.

Така, и покрај фактот дека „изгледа како график“, не можев да добијам 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. За да се движите од точка А до точка Б:

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 за да го сервисираме овој тип на барање. Секогаш можеме дополнително да ги оптимизираме барањата користејќи материјализирани прикази и паралелизација со претходна обработка на преносот на настани користејќи 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. Случајот за употреба претставен во овој напис е јасен анти-шаблон за графички DBMS, иако се чини погоден за моделирање во нивната парадигма.

Извор: www.habr.com

Додадете коментар