Sveiki visiem. MÄs izstrÄdÄjam produktu bezsaistes trafika analÄ«zei. Projekta uzdevums ir saistÄ«ts ar apmeklÄtÄju ceļu statistisko analÄ«zi reÄ£ionos.
KÄ daļu no Ŕī uzdevuma lietotÄji var uzdot Å”Äda veida sistÄmas vaicÄjumus:
- cik apmeklÄtÄju devÄs no zonas "A" uz zonu "B";
- cik daudz apmeklÄtÄju devÄs no zonas "A" uz zonu "B" caur zonu "C" un pÄc tam caur zonu "D";
- cik ilgs laiks bija nepiecieÅ”ams, lai noteikta veida apmeklÄtÄjs ceļotu no zonas āAā uz zonu āBā.
un vairÄkus lÄ«dzÄ«gus analÄ«tiskos vaicÄjumus.
ApmeklÄtÄja kustÄ«ba pa apgabaliem ir virzÄ«ta diagramma. PÄc interneta lasÄ«Å”anas es atklÄju, ka diagrammu DBVS tiek izmantotas arÄ« analÄ«tiskajÄm atskaitÄm. Man bija vÄlme redzÄt, kÄ grafiku DBVS tiks galÄ ar Å”Ädiem vaicÄjumiem (TL; DR; slikti).
Es izvÄlÄjos izmantot DBVS
- BerkeleyDB krÄtuves aizmugure, Apache Cassandra, Scylla;
- kompleksos indeksus var glabÄt Lucene, Elasticsearch, Solr.
JanusGraph autori raksta, ka tas ir piemÄrots gan OLTP, gan OLAP.
Esmu strÄdÄjis ar BerkeleyDB, Apache Cassandra, Scylla un ES, un Å”ie produkti bieži tiek izmantoti mÅ«su sistÄmÄs, tÄpÄc es biju optimistisks par Ŕī grafika DBVS testÄÅ”anu. Man Ŕķita dÄ«vaini izvÄlÄties BerkeleyDB, nevis RocksDB, taÄu tas, iespÄjams, ir saistÄ«ts ar darÄ«jumu prasÄ«bÄm. JebkurÄ gadÄ«jumÄ mÄrogojama produkta lietoÅ”anai ir ieteicams izmantot Cassandra vai Scylla aizmugursistÄmu.
Neo4j neuzskatÄ«ju, jo klasterÄÅ”anai ir nepiecieÅ”ama komerciÄla versija, tas ir, produkts nav atvÄrtÄ koda.
Diagrammu DBVS saka: āJa tas izskatÄs kÄ grafiks, uzskatiet to par grafiku!ā - skaistums!
Vispirms es uzzÄ«mÄju grafiku, kas tika izveidots precÄ«zi saskaÅÄ ar grafiku DBVS kanoniem:
Ir kÄda bÅ«tÄ«ba Zone
, kas atbild par apgabalu. Ja ZoneStep
pieder pie Ŕī Zone
, tad viÅÅ” uz to atsaucas. PÄc bÅ«tÄ«bas Area
, ZoneTrack
, Person
NepievÄrsiet uzmanÄ«bu, tie pieder domÄnam un netiek uzskatÄ«ti par testa daļu. KopumÄ Ä·Ädes meklÄÅ”anas vaicÄjums Å”Ädai diagrammas struktÅ«rai izskatÄ«tos Å”Ädi:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()
Kas krieviski ir apmÄram Å”Ädi: atrodiet zonu ar ID=0, paÅemiet visas virsotnes, no kurÄm uz to iet mala (ZoneStep), stampÄjieties, neatgriežoties, lÄ«dz atrodat tÄs ZoneSteps, no kurÄm ir mala uz zonu ar ID=19, saskaitiet Å”Ädu Ä·Äžu skaitu.
Es nedomÄju, ka zinu visas grafikas meklÄÅ”anas sarežģītÄ«bas, taÄu Å”is vaicÄjums tika Ä£enerÄts, pamatojoties uz Å”o grÄmatu (
Es ielÄdÄju 50 tÅ«kstoÅ”us ierakstu, kuru garums ir no 3 lÄ«dz 20 punktiem, JanusGraph grafiku datu bÄzÄ, izmantojot BerkeleyDB aizmugursistÄmu, izveidoju indeksus saskaÅÄ ar
Python lejupielÄdes skripts:
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)
MÄs izmantojÄm virtuÄlo maŔīnu ar 4 kodoliem un 16 GB RAM SSD. JanusGraph tika izvietots, izmantojot Å”o komandu:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest
Å ajÄ gadÄ«jumÄ dati un indeksi, kas tiek izmantoti precÄ«zÄs atbilstÄ«bas meklÄÅ”anai, tiek glabÄti BerkeleyDB. IzpildÄ«jis iepriekÅ” izteikto pieprasÄ«jumu, saÅÄmu laiku, kas vienÄds ar vairÄkiem desmitiem sekunžu.
ParalÄli palaižot Äetrus iepriekÅ” minÄtos skriptus, man izdevÄs pÄrvÄrst DBVS par Ä·irbi ar jautru Java stacktraces plÅ«smu (un mums visiem patÄ«k lasÄ«t Java stacktraces) Docker žurnÄlos.
PÄc dažÄm pÄrdomÄm es nolÄmu vienkÄrÅ”ot diagrammas diagrammu lÄ«dz Å”Ädai:
Izlemjot, ka meklÄÅ”ana pÄc entÄ«tijas atribÅ«tiem bÅ«tu ÄtrÄka nekÄ meklÄÅ”ana pÄc malÄm. RezultÄtÄ mans pieprasÄ«jums izvÄrtÄs Å”Ädi:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()
Kas krieviski ir apmÄram Å”Äds: atrodiet ZoneStep ar ID=0, spiediet, neatgriežoties, lÄ«dz atrodat ZoneStep ar ID=19, saskaitiet Å”Ädu Ä·Äžu skaitu.
Es arÄ« vienkÄrÅ”oju iepriekÅ” norÄdÄ«to ielÄdes skriptu, lai neradÄ«tu nevajadzÄ«gus savienojumus, ierobežojot sevi ar atribÅ«tiem.
PieprasÄ«juma izpilde vÄl prasÄ«ja vairÄkas sekundes, kas mÅ«su uzdevumam bija pilnÄ«gi nepieÅemami, jo tas nemaz nebija piemÄrots jebkÄda veida AdHoc pieprasÄ«jumu vajadzÄ«bÄm.
Es mÄÄ£inÄju izvietot JanusGraph, izmantojot Scylla kÄ ÄtrÄko Cassandra ievieÅ”anu, taÄu tas arÄ« neizraisÄ«ja nekÄdas bÅ«tiskas veiktspÄjas izmaiÅas.
TÄpÄc, neskatoties uz to, ka "tas izskatÄs kÄ grafiks", es nevarÄju panÄkt, lai diagramma DBVS to Ätri apstrÄdÄtu. Es pilnÄ«bÄ pieÅemu, ka ir kaut kas, ko es nezinu un ka JanusGraph var likt veikt Å”o meklÄÅ”anu sekundes daļÄ, tomÄr es to nevarÄju izdarÄ«t.
TÄ kÄ problÄma vÄl bija jÄrisina, sÄku domÄt par tabulu JOIN un Pivots, kas elegances ziÅÄ optimismu neviesa, bet praksÄ varÄtu bÅ«t pilnÄ«gi darbojams variants.
MÅ«su projekts jau izmanto Apache ClickHouse, tÄpÄc es nolÄmu pÄrbaudÄ«t savu pÄtÄ«jumu par Å”o analÄ«tisko DBVS.
Izvietots ClickHouse, izmantojot vienkÄrÅ”u recepti:
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
Es izveidoju datu bÄzi un tabulu tajÄ Å”Ädi:
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
Es to aizpildÄ«ju ar datiem, izmantojot Å”Ädu skriptu:
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
)
TÄ kÄ ieliktÅi tiek piegÄdÄti partijÄs, aizpildÄ«Å”ana bija daudz ÄtrÄka nekÄ JanusGraph.
Izveidoti divi vaicÄjumi, izmantojot JOIN. Lai pÄrvietotos no punkta A uz punktu B:
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
Lai izietu cauri 3 punktiem:
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
PieprasÄ«jumi, protams, izskatÄs diezgan biedÄjoÅ”i; reÄlai lietoÅ”anai jums ir jÄizveido programmatÅ«ras Ä£eneratora instalÄcija. TomÄr viÅi strÄdÄ un strÄdÄ Ätri. Gan pirmais, gan otrais pieprasÄ«jums tiek izpildÄ«ts mazÄk nekÄ 0.1 sekundÄ. Å eit ir piemÄrs vaicÄjuma izpildes laikam count (*), kas iet cauri 3 punktiem:
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.)
PiezÄ«me par IOPS. Aizpildot datus, JanusGraph Ä£enerÄja diezgan lielu IOPS skaitu (1000ā1300 Äetriem datu kopas pavedieniem), un IOWAIT bija diezgan augsts. TajÄ paÅ”Ä laikÄ ClickHouse radÄ«ja minimÄlu diska apakÅ”sistÄmas slodzi.
SecinÄjums
MÄs nolÄmÄm izmantot ClickHouse, lai apkalpotu Å”Äda veida pieprasÄ«jumus. MÄs vienmÄr varam vÄl vairÄk optimizÄt vaicÄjumus, izmantojot materializÄtus skatus un paralÄli, iepriekÅ” apstrÄdÄjot notikumu straumi, izmantojot Apache Flink, pirms to ielÄdÄÅ”anas pakalpojumÄ ClickHouse.
VeiktspÄja ir tik laba, ka mums, iespÄjams, pat nevajadzÄs domÄt par tabulu programmÄÅ”anu. IepriekÅ” mums bija jÄveic datu pagriezieni, kas iegÅ«ti no Vertica, augÅ”upielÄdÄjot tos Apache Parquet.
DiemžÄl vÄl viens mÄÄ£inÄjums izmantot diagrammu DBVS bija neveiksmÄ«gs. Es neuzskatÄ«ju, ka JanusGraph ir draudzÄ«ga ekosistÄma, kas atvieglotu produkta lietoÅ”anu. TajÄ paÅ”Ä laikÄ servera konfigurÄÅ”anai tiek izmantots tradicionÄlais Java veids, kas liks cilvÄkiem, kuri nav pazÄ«stami ar Java, raudÄt asins asaras:
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}
Man izdevÄs nejauÅ”i "ielikt" JanusGraph BerkeleyDB versiju.
DokumentÄcija indeksu ziÅÄ ir diezgan greiza, jo, lai pÄrvaldÄ«tu indeksus, Groovy programmÄ ir jÄveic diezgan dÄ«vains Å”amanisms. PiemÄram, indeksa izveide ir jÄveic, ierakstot kodu Gremlin konsolÄ (kas, starp citu, nedarbojas no kastes). No oficiÄlÄs JanusGraph dokumentÄcijas:
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()
PÄcvÄrds
SavÄ ziÅÄ iepriekÅ”minÄtais eksperiments ir salÄ«dzinÄjums starp siltu un mÄ«kstu. Ja padomÄjat par to, diagramma DBVS veic citas darbÄ«bas, lai iegÅ«tu tÄdus paÅ”us rezultÄtus. TomÄr testu ietvaros es arÄ« veicu eksperimentu ar Å”Ädu pieprasÄ«jumu:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()
kas atspoguļo pastaigas attÄlumu. TomÄr pat uz Å”Ädiem datiem grafiks DBVS uzrÄdÄ«ja rezultÄtus, kas pÄrsniedza dažas sekundes... Tas, protams, ir saistÄ«ts ar to, ka bija tÄdi ceļi kÄ 0 -> X -> Y ... -> 1
, ko arÄ« pÄrbaudÄ«ja grafika dzinÄjs.
Pat tÄdiem vaicÄjumiem kÄ:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()
Es nevarÄju saÅemt produktÄ«vu atbildi, apstrÄdes laiks bija mazÄks par sekundi.
StÄsta morÄle ir tÄda, ka skaista ideja un paradigmatiskÄ modelÄÅ”ana nenoved pie vÄlamÄ rezultÄta, kas tiek demonstrÄts ar daudz lielÄku efektivitÄti, izmantojot ClickHouse piemÄru. Å ajÄ rakstÄ aprakstÄ«tais lietoÅ”anas gadÄ«jums ir skaidrs pretmodelis grafu DBVS, lai gan Ŕķiet piemÄrots modelÄÅ”anai to paradigmÄ.
Avots: www.habr.com