Сәйкес жолдарды табу мәселесін шешу үшін JanusGraph граф ДҚБЖ қолдану мүмкіндігін сынайтын эксперимент

Сәйкес жолдарды табу мәселесін шешу үшін JanusGraph граф ДҚБЖ қолдану мүмкіндігін сынайтын эксперимент

Бәріңе сәлем. Біз желіден тыс трафикті талдауға арналған өнімді әзірлеп жатырмыз. Жобада өңірлер бойынша келушілер маршруттарын статистикалық талдаумен байланысты тапсырма бар.

Осы тапсырманың бөлігі ретінде пайдаланушылар келесі түрдегі жүйелік сұрауларды сұрай алады:

  • «А» аймағынан «В» аймағына қанша келушілер өтті;
  • «А» аймағынан «В» аймағына «С» аймағы арқылы, одан кейін «D» аймағы арқылы қанша келушілер өтті;
  • белгілі бір келушінің түріне «А» аймағынан «В» аймағына қанша уақыт барды.

және бірнеше ұқсас аналитикалық сұраулар.

Келушінің аумақтар бойынша қозғалысы бағытталған график болып табылады. Интернетті оқығаннан кейін мен графикалық ДҚБЖ аналитикалық есептер үшін де қолданылатынын білдім. Менде ДҚБЖ графигі осындай сұраулармен қалай күресетінін көргім келді (TL; DR; нашар).

Мен ДҚБЖ пайдалануды таңдадым JanusGraph, (менің ойымша) оны лайықты операциялық сипаттамалармен қамтамасыз етуі керек жетілген технологиялар стесіне негізделген ашық бастапқы ДҚБЖ графикасының көрнекті өкілі ретінде:

  • BerkeleyDB сақтау сервері, Apache Cassandra, Scylla;
  • күрделі индекстерді Lucene, Elasticsearch, Solr ішінде сақтауға болады.

JanusGraph авторлары оның OLTP және OLAP үшін де қолайлы екенін жазады.

Мен BerkeleyDB, Apache Cassandra, Scylla және ES-пен жұмыс істедім және бұл өнімдер біздің жүйелерімізде жиі пайдаланылады, сондықтан мен бұл графикті ДҚБЖ сынауға оптимистік көзқараспен қарадым. Мен RocksDB орнына BerkeleyDB таңдауды біртүрлі деп таптым, бірақ бұл транзакция талаптарына байланысты болуы мүмкін. Кез келген жағдайда, ауқымды, өнімді пайдалану үшін Cassandra немесе Scylla-да серверді пайдалану ұсынылады.

Мен Neo4j-ді қарастырмадым, себебі кластерлеу коммерциялық нұсқаны қажет етеді, яғни өнім ашық бастапқы код емес.

Графикалық ДҚБЖ: «Егер ол графикке ұқсайтын болса, оны график ретінде қарастырыңыз!» дейді. - сұлулық!

Біріншіден, мен графикті сыздым, ол ДҚБЖ графигінің канондарына сәйкес жасалған:

Сәйкес жолдарды табу мәселесін шешу үшін 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), қай аймаққа дейін шеті бар аймақты тапқанша артқа қайтпай еңкейіңіз. ID=19, мұндай тізбектердің санын есептеңіз.

Мен графиктер бойынша іздеудің барлық қыр-сырын білемін деп ойламаймын, бірақ бұл сұрау осы кітаптың негізінде жасалған (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Мен BerkeleyDB серверін пайдаланып JanusGraph графикалық дерекқорына ұзындығы 50-тен 3 нүктеге дейінгі 20 мың жолды жүктедім, сәйкес индекстерді жасадым. көшбасшылық.

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)

Біз SSD дискісінде 4 ядросы және 16 ГБ жедел жады бар VM қолдандық. JanusGraph осы пәрменді пайдаланып орналастырылған:

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

Бұл жағдайда дәл сәйкестік іздеулері үшін пайдаланылатын деректер мен индекстер BerkeleyDB ішінде сақталады. Бұрын берілген сұрауды орындап, мен бірнеше ондаған секундқа тең уақытты алдым.

Жоғарыда аталған 4 сценарийді параллель орындау арқылы мен ДҚБЖ-ны Docker журналдарында Java стектректерінің көңілді ағыны бар асқабаққа айналдыра алдым (және бәріміз Java стектректерін оқығанды ​​жақсы көреміз).

Біраз ойланып, мен графикалық диаграмманы келесіге оңайлатуды шештім:

Сәйкес жолдарды табу мәселесін шешу үшін JanusGraph граф ДҚБЖ қолдану мүмкіндігін сынайтын эксперимент

Нысан атрибуттары бойынша іздеу жиектер бойынша іздеуге қарағанда жылдамырақ болатынын шешу. Нәтижесінде менің өтінішім келесідей болды:

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 сұрауларының мақсаттарына мүлдем сәйкес келмеді.

Мен JanusGraph қолданбасын Scylla арқылы Cassandra-ны ең жылдам іске асыру ретінде қолдануға тырыстым, бірақ бұл да айтарлықтай өнімділік өзгерістеріне әкелмеді.

Сондықтан «бұл графикке ұқсайды» дегеніне қарамастан, мен оны жылдам өңдеу үшін ДҚБЖ графигін ала алмадым. Мен білмейтін нәрсе бар деп ойлаймын және JanusGraph бұл іздеуді секундтың бір бөлігінде орындауға болады, бірақ мен мұны істей алмадым.

Мәселе әлі де шешілуі керек болғандықтан, мен JOIN және кестелердің шеңберлері туралы ойлана бастадым, бұл талғампаздық тұрғысынан оптимизмді шабыттандырмады, бірақ іс жүзінде толығымен жұмыс істейтін нұсқа болуы мүмкін.

Біздің жобамыз 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 нүкте арқылы өтетін count(*) сұрауының орындалу уақытының мысалы келтірілген:

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 арқылы оқиғалар ағынын алдын ала өңдеу арқылы.

Өнімділік соншалықты жақсы, біз кестелерді бағдарламалық түрде айналдыру туралы ойлаудың қажеті жоқ шығар. Бұрын біз Vertica-дан алынған деректерді Apache Parquet-ке жүктеп салу арқылы айналдыруға тура келді.

Өкінішке орай, ДҚБЖ графигін пайдаланудың тағы бір әрекеті сәтсіз аяқталды. Мен 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}

Мен JanusGraph бағдарламасының BerkeleyDB нұсқасын кездейсоқ «қойып» алдым.

Құжаттама индекстер тұрғысынан өте қисық, өйткені индекстерді басқару 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()

Кейінгі сөз

Белгілі бір мағынада, жоғарыдағы эксперимент жылы және жұмсақ арасындағы салыстыру болып табылады. Егер сіз бұл туралы ойласаңыз, ДҚБЖ графигі бірдей нәтижелерді алу үшін басқа әрекеттерді орындайды. Дегенмен, сынақтардың бір бөлігі ретінде мен келесідей сұраумен эксперимент жүргіздім:

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

бұл жүру қашықтығын көрсетеді. Дегенмен, мұндай деректерде де ДҚБЖ графигі бірнеше секундтан асатын нәтижелерді көрсетті... Бұл, әрине, сияқты жолдар болғанымен байланысты. 0 -> X -> Y ... -> 1, оны графикалық қозғалтқыш те тексерді.

Тіпті келесідей сұрау үшін:

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

Мен бір секундтан аз өңдеу уақытымен нәтижелі жауап ала алмадым.

Әңгіменің моральдық мәні - әдемі идея мен парадигмалық модельдеу қажетті нәтижеге әкелмейді, бұл ClickHouse мысалында әлдеқайда жоғары тиімділікпен көрсетіледі. Осы мақалада ұсынылған пайдалану жағдайы графикалық ДҚБЖ үшін айқын анти-үлгі болып табылады, бірақ олардың парадигмасында модельдеу үшін қолайлы болып көрінеді.

Ақпарат көзі: www.habr.com

пікір қалдыру