Тохиромжтой замыг олох асуудлыг шийдвэрлэхэд JanusGraph график DBMS-ийг ашиглах боломжтой эсэхийг шалгах туршилт.

Тохиромжтой замыг олох асуудлыг шийдвэрлэхэд JanusGraph график DBMS-ийг ашиглах боломжтой эсэхийг шалгах туршилт.

Сайн уу. Бид офлайн замын хөдөлгөөний шинжилгээнд зориулсан бүтээгдэхүүн боловсруулж байна. Төсөл нь бүс нутгуудад зочлох маршрутын статистик дүн шинжилгээ хийх үүрэгтэй.

Энэхүү даалгаврын нэг хэсэг болгон хэрэглэгчид дараах төрлийн системийн асуулгыг асууж болно.

  • "А" бүсээс "Б" хэсэгт хэдэн зочин өнгөрсөн;
  • "А" бүсээс "В" хэсэг рүү "С" талбайгаар, дараа нь "D" талбайгаар хэдэн зочин өнгөрсөн;
  • тодорхой төрлийн зочин “А” бүсээс “Б” бүс рүү аялахад хэр хугацаа зарцуулсан.

мөн ижил төстэй хэд хэдэн аналитик асуулга.

Бүс нутаг дахь зочдын хөдөлгөөн нь чиглэсэн график юм. Интернетийг уншсаны дараа би график DBMS-ийг аналитик тайланд ашигладаг болохыг олж мэдсэн. График DBMS нь ийм асуултуудыг хэрхэн даван туулахыг харахыг хүсч байсан (TL; DR; муу).

Би DBMS ашиглахаар сонгосон JanusGraph, гүйцсэн технологиуд дээр тулгуурласан нээлттэй эхийн DBMS графикийн шилдэг төлөөлөгчийн хувьд (миний бодлоор) түүнийг зохистой үйл ажиллагааны шинж чанараар хангах ёстой:

  • BerkeleyDB хадгалах сан, Apache Cassandra, Scylla;
  • нарийн төвөгтэй индексүүдийг Lucene, Elasticsearch, Solr-д хадгалах боломжтой.

JanusGraph-ийн зохиогчид үүнийг OLTP болон OLAP-д тохиромжтой гэж бичжээ.

Би BerkeleyDB, Apache Cassandra, Scylla болон ES-тэй ажиллаж байсан бөгөөд эдгээр бүтээгдэхүүнийг манай системд ихэвчлэн ашигладаг тул энэ график DBMS-ийг турших талаар өөдрөг үзэлтэй байсан. RocksDB-ээс илүү BerkeleyDB-г сонгох нь надад хачирхалтай санагдаж байсан ч энэ нь гүйлгээний шаардлагуудтай холбоотой байж магадгүй юм. Ямар ч тохиолдолд, өргөтгөх боломжтой, бүтээгдэхүүнийг ашиглахын тулд Кассандра эсвэл Скилла дээр арын хэсгийг ашиглахыг зөвлөж байна.

Кластер хийхэд арилжааны хувилбар шаардлагатай, өөрөөр хэлбэл бүтээгдэхүүн нь нээлттэй эх сурвалж биш учраас би 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), буцаж явахгүйгээр дэвсээд, тэндээс нь бүс рүү гарах ирмэг байгаа ZoneStep-ийг олох хүртлээ. ID=19, ийм гинжний тоог тоол.

Би график хайх бүх нарийн ширийнийг мэддэг дүр эсгэдэггүй, гэхдээ энэ асуултыг энэ ном дээр үндэслэн үүсгэсэн (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Би BerkeleyDB backend ашиглан 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 ГБ RAM бүхий VM ашигласан. 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-г ашиглан JanusGraph-ийг Кассандрагийн хамгийн хурдан хэрэгжүүлэлт болгон ашиглахыг оролдсон боловч энэ нь гүйцэтгэлд мэдэгдэхүйц өөрчлөлт оруулаагүй.

Тиймээс "энэ нь график шиг харагдаж байна" гэсэн хэдий ч би графикийг хурдан боловсруулахын тулд DBMS-ийг авч чадсангүй. Миний мэдэхгүй зүйл байгаа бөгөөд JanusGraph-ийг хэдхэн секундын дотор хайлт хийх боломжтой гэж би бүрэн бодож байна, гэхдээ би үүнийг хийж чадаагүй.

Асуудлыг шийдэх шаардлагатай хэвээр байгаа тул би JOINs болон Pivots of tables-ийн талаар бодож эхэлсэн нь дэгжин байдлын хувьд өөдрөг үзлийг төрүүлээгүй ч практикт бүрэн хэрэгжих боломжтой хувилбар байж болох юм.

Манай төсөл аль хэдийн 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-г ашиглахаар шийдсэн. Бид үйл явдлын урсгалыг ClickHouse-д ачаалахаас өмнө Apache Flink ашиглан урьдчилан боловсруулснаар материалжуулсан харагдац болон параллелчлалыг ашиглан асуултуудыг оновчтой болгох боломжтой.

Гүйцэтгэл нь маш сайн тул бид хүснэгтүүдийг програмын дагуу эргүүлэх талаар бодох шаардлагагүй болно. Өмнө нь бид 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}

Би 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()

Дараах үгс

Нэг ёсондоо дээрх туршилт нь дулаан, зөөлөн хоёрын харьцуулалт юм. Хэрэв та энэ талаар бодож үзвэл 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

сэтгэгдэл нэмэх