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

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

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

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 ішінде сақталады. Жоғарыда сипатталған сұрауды орындай отырып, мен бірнеше ондаған секундты орындау уақытын алдым.

Жоғарыдағы төрт сценарийді қатар іске қосу арқылы мен ДҚБЖ-ны 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 арқылы қолдануға тырыстым, бірақ бұл да өнімділікті айтарлықтай жақсартуға әкелмеді.

Сонымен, «бұл графикке ұқсайды» дегеніне қарамастан, оны жылдам өңдеу үшін ДҚБЖ графигін ала алмадым. Мен бірдеңені жоғалтып алғаныма сенімдімін және 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 секундтан аз уақыт ішінде орындалады. Мұнда үш нүкте арқылы санау(*) циклі үшін сұрауды орындау уақытының мысалы келтірілген:

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

DDoS қорғауы бар сайттар үшін сенімді хостинг, VPS VDS серверлерін сатып алыңыз 🔥 DDoS қорғанысы, VPS VDS серверлері бар сенімді веб-сайт хостингін сатып алыңыз | ProHoster