Здраво на сите. Развиваме производ за офлајн анализа на сообраќајот. Проектот има задача поврзана со статистичка анализа на патеките на посетителите низ регионите.
Како дел од оваа задача, корисниците можат да поставуваат системски прашања од следниот тип:
- колку посетители поминале од областа „А“ до областа „Б“;
- колку посетители поминале од областа „А“ во областа „Б“ низ областа „Ц“, а потоа низ областа „Д“;
- колку време му било потребно на одреден тип посетител да патува од областа „А“ до областа „Б“.
и голем број слични аналитички прашања.
Движењето на посетителот низ областите е насочен график. По читањето на Интернет, открив дека графичките DBMS се користат и за аналитички извештаи. Имав желба да видам како графички DBMS ќе се справат со такви прашања (TL; DR; слабо).
Избрав да користам DBMS
- Заднина за складирање на BerkeleyDB, Apache Cassandra, Scylla;
- комплексните индекси може да се складираат во Lucene, Elasticsearch, Solr.
Авторите на JanusGraph пишуваат дека е погоден и за OLTP и за OLAP.
Работев со BerkeleyDB, Apache Cassandra, Scylla и ES, и овие производи често се користат во нашите системи, па затоа бев оптимист за тестирање на овој графички DBMS. Ми падна чудно да се избере BerkeleyDB наместо RocksDB, но тоа веројатно се должи на барањата за трансакцијата. Во секој случај, за скалабилна употреба на производот, се предлага да се користи заднина на Cassandra или Scylla.
Не го земав предвид Neo4j бидејќи кластерирањето бара комерцијална верзија, односно производот не е со отворен код.
Графички DBMS велат: „Ако изгледа како график, третирајте го како график!“ - убавина!
Прво, нацртав графикон, кој беше направен точно според каноните на графичките 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 од кои има раб до зоната со ИД=19, изброј го бројот такви синџири.
Не се преправам дека ги знам сите сложености на пребарувањето на графикони, но ова барање е создадено врз основа на оваа книга (
Вчитав 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.
По малку размислување, решив да го поедноставам графичкиот дијаграм на следново:
Одлуката дека пребарувањето по атрибути на ентитети би било побрзо отколку пребарувањето по рабови. Како резултат на тоа, моето барање се претвори во следново:
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