
Përshëndetje të gjithëve. Ne po zhvillojmë një produkt për analizën e trafik të jashtëm. Në projekt ka një detyrë që lidhet me analizën statistikore të rrugëve të lëvizjes së vizitorëve nëpër zona.
Në kuadër të kësaj detyre, përdoruesit mund të bëjnë kërkesa të tilla si:
- sa vizitorë kaluan nga zona "A" në zonën "B";
- sa vizitorë kaluan nga zona "A" në zonën "B" përmes zonës "C", pastaj përmes zonës "D";
- sa kohë ka marrë kalimi i një vizitori të caktuar nga zona "A" në zonën "B".
dhe një sërë kërkesash analitike të ngjashme.
Lëvizja e vizitorëve nëpër zona paraqet një grafik të orientuar. Pas leximit të disa burimeve në internet, zbulova se DB-të grafike përdoren edhe për raportet analitike. Më erdhi dëshira të shoh si do të përballen me këto kërkesa DB-të grafike (TL;DR; keq).
Kam zgjedhur për përdorim DB-në , si një përfaqësues të shkëlqyer të bazave të të dhënave grafike open-source, e cila mbështetet në një teknologji të pjekur, që (mendimi im) duhet t'i sigurojë asaj performancë të pranueshme operacionale:
- backend-i i magazinës BerkeleyDB, Apache Cassandra, Scylla;
- indekse të komplikuara mund të ruhen në Lucene, Elasticsearch, Solr.
Autoret e JanusGraph thonë se ajo është e përshtatshme për OLTP dhe OLAP.
Unë kam punuar me BerkeleyDB, Apache Cassandra, Scylla dhe ES, për më tepër, këto produkte shpesh përdoren në sistemet tona, kështu që shikoja me optimizëm testimin e kësaj baze të të dhënash grafike. Më dukej e çuditshme zgjedhja e BerkeleyDB-së në vend të RocksDB-së, por ndoshta kjo është për shkak të kërkesave për transaksionet. Në çdo rast, për përdorim të shkallëzueshëm dhe produktiv, rekomandohet të përdoret backend-i në Cassandra ose Scylla.
Neo4j nuk e shqyrtova, pasi për klasterizimin kërkohet versioni komercial, pra produkti nuk është i hapur.
Baza tĂ« dhĂ«nash grafike thonĂ«: "NĂ«se diçka duket si njĂ« grafik â trajtojeni si njĂ« grafik!" â bukuri!
Së pari, kam vizatuar një grafik që është bërë pikërisht sipas kanoneve të bazave të të dhënave grafike:

Ka një entitet Zone, që është përgjegjës për një zonë. Nëse ZoneStep i përket kësaj Zone, atëherë ai e referon atë. Në entitet Zona, ZoneTrack, Person mos i kushtoni vëmendje, ato i përkasin domainit dhe në kuadër të testit nuk shqyrtohen. Pra, për një strukturë të tillë grafike, kërkesa për të gjetur zinxhirë do të dukej si:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()ĂfarĂ« nĂ« rusisht duket si: gjej Zone me ID=0, merr tĂ« gjitha majat tĂ« cilat kanĂ« njĂ« lidhje (ZoneStep) pĂ«r tĂ«, eci pa u kthyer deri sa tĂ« gjej ato ZoneStep, nga tĂ« cilat ka njĂ« lidhje nĂ« Zone me ID=19, numĂ«ro numrin e atyre zinxhirĂ«ve.
Nuk pretendoj se njoh të gjitha nuancat e kërkimit në graf, por kjo kërkesë u gjenerua mbi bazën e kësaj libri ().
Kam ngarkuar 50 mijë njerëz me një gjatësi nga 3 deri në 20 pika në bazën e të dhënave grafike JanusGraph, e cila përdor backend-in BerkeleyDB, krijova indekse sipas .
Skripti për ngarkimin në Python:
nga nga import random
nga nga time import time
nga nga init import g, graph
nëse __name__ == '__main__':
pika = []
maksimale_zone = 19
zcache = dict()
për i në gamën (0, maksimale_zone + 1):
zcache[i] = g.addV('Zone').property('id', i).next()
startZ = zcache[0]
endZ = zcache[maksimale_zone]
për i në gamën (0, 10000):
nëse jo i % 100:
print(i)
start = g.addV('ZoneStep').property('time', int(time())).next()
g.V(start).addE('belongs').to(startZ).iterate()
ndërsa e vërtetë:
pt = g.addV('ZoneStep').property('time', int(time())).next()
end_chain = random()
nëse end_chain < 0.3:
g.V(pt).addE('belongs').to(endZ).iterate()
g.V(start).addE('goes').to(pt).iterate()
break
tjetër:
zone_id = int(random() * maksimale_zone)
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)I është përdorur VM me 4 bërthama dhe 16 GB RAM në SSD. JanusGraph u vendos me këtë komandë:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latestNë këtë rast, të dhënat dhe indekset që përdoren për kërkimin e përputhjes së saktë ruhen në BerkeleyDB. Pas ekzekutimit të kërkesës së dhënë më parë, kam marrë një kohë të barabartë me disa dhjetëra sekonda.
Duke ekzekutuar 4 skriptat e mësipërm në paralel, arrita ta shndërroj DB në një të tillë me një fluks të këndshëm të stectraive Java (dhe ne të gjithë e duam të lexojmë stectraive Java) në logjet e Docker.
Pas mendova, vendosa ta e thjeshtoj diagramën e grafit në këtë formë:

Duke menduar se kërkimi sipas atributeve të entitetit do të ishte më i shpejtë se kërkimi sipas skajeve. Si rezultat, kërkesa ime u shndërrua në këtë:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()Kjo në rusisht do të thoshte: gjej ZoneStep me ID=0, vajti pa u kthyer derisa të gjej ZoneStep me ID=19, llogariti numrin e lidhjeve të tilla.
Skemën e shkarkimit të dhënë më sipër, e thjeshtova gjithashtu, për të mos krijuar lidhje të tepruara, duke u kufizuar në atribute.
Kërkesa gjithsesi e ekzekutuar disa sekonda, që ishte plotësisht e papranueshme për detyrën tonë, pasi për qëllime AdHoc kërkesa e llojeve të rastësishme nuk ishte aspak e përshtatshme.
Kam provuar të zgjas JanusGraph duke përdorur Scylla, si realizimin më të shpejtë të Cassandra, por kjo gjithashtu nuk çoi në ndonjë ndryshim të rëndësishëm në performancë.
Pra, megjithëse "dukesh si graf", nuk mund të bëj që baza e të dhënave grafike të përpunojë këtë shpejt. E kam të qartë që ndoshta nuk di diçka dhe mund të bëj që JanusGraph të realizojë këtë kërkesë për disa sekonda, por nuk më ka dalë.
Pasi që ishte e nevojshme të zgjidhet problemi, fillova të mendoj për JOIN dhe Pivot të tabelave, që nuk jepte optimizëm në aspektin e elegancës, por mund të ishte një mundësi e pranueshme në praktikë.
Në projektin tonë tashmë po përdoret Apache ClickHouse, kështu që vendosa të verifikoj përfundimet e mia në këtë DB analitik.
E lançova ClickHouse në një recetë të thjeshtë:
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-serverKrijova një DB dhe një tabelë si kjo:
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 = 8192E mbusha atë me të dhëna duke përdorur këtë skript:
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
)Duke insertet janë bërë në grupe, mbushja ishte shumë më e shpejtë se për JanusGraph.
Ndërtoi dy kërkesa nëpërmjet JOIN. Për kalimin nga pika A në pikën 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.whenPër kalimin përmes 3 pikave:
Zgjidhni s3.person,
s1z,
s1w,
s2z,
s2w,
s3.zone,
s3.when
FROM
(Zgjidhni s1.person AS person,
s1.zone AS s1z,
s1.when AS s1w,
s2.zone AS s2z,
s2.when AS s2w
FROM
(Zgjidhni *
NGA steps
KU (area = 0)
DHE (zone = 0)) SI s1 ĂDO INNER JOIN
(Zgjidhni *
NGA steps SI s2
KU (area = 0)
DHE (zone = 3)) SI s2 PĂRMBAJTJE person
KU s1.when <= s2.when) p ĂDO INNER JOIN
(Zgjidhni *
NGA steps
KU (area = 0)
DHE (zone = 19)) SI s3 PĂRMBAJTJE person
KU p.s2w <= s3.whenKërkesat, sigurisht, duken mjaft frikësuese, për përdorim real kërkohet një lidhje programore-gjenerator. Megjithatë, ato funksionojnë dhe funksionojnë shpejt. Të dy kërkesat, të parën dhe të dytën, i përfundon për më pak se 0.1 sekondë. Ja një shembull i kohës së ekzekutimit të kërkesës për count(*) kalim nëpër 3 pika:
Zgjidhni count(*)
FROM
(
Zgjidhni
s1.person AS person,
s1.zone AS s1z,
s1.when AS s1w,
s2.zone AS s2z,
s2.when AS s2w
FROM
(
Zgjidhni *
NGA steps
KU (area = 0) DHE (zone = 0)
) SI s1
ĂDO INNER JOIN
(
Zgjidhni *
NGA steps SI s2
KU (area = 0) DHE (zone = 3)
) SI s2 PĂRMBAJTJE (person)
KU s1.when <= s2.when
) SI p
ĂDO INNER JOIN
(
Zgjidhni *
NGA steps
KU (area = 0) DHE (zone = 19)
) SI s3 PĂRMBAJTJE (person)
KU p.s2w <= s3.when
ââcount()ââ
â 11592 â
âââââââââââ1 rreshta nĂ« grup. Koha e kaluar: 0.068 sek. Procesuar 250.03 mijĂ« rreshta, 8.00 MB (3.69 milion rreshta/s., 117.98 MB/s.)VĂ«rejtje nĂ« lidhje me IOPS. GjatĂ« mbushjes sĂ« tĂ« dhĂ«nave, JanusGraph gjeneroi njĂ« numĂ«r tĂ« lartĂ« tĂ« IOPS (1000-1300 pĂ«r katĂ«r kĂ«ngĂ« mbushjeje tĂ« dhĂ«nash), dhe IOWAIT ishte mjaft i lartĂ«. NĂ« tĂ« njĂ«jtĂ«n kohĂ«, ClickHouse gjeneroi njĂ« ngarkesĂ« minimale nĂ« nĂ«nshkrimin e diskut.
Përfundimi
Vendosëm të përdorim ClickHouse për të shërbyer kërkesat e këtij lloji. Ne gjithmonë mund të optimizojmë edhe më shumë kërkesat duke përdorur pamjet e materializuara dhe paralelizimin, duke kryer përpunimin paraprak të rrjedhës së ngjarjeve me Apache Flink para se t'i ngarkojmë në ClickHouse.
Performanca është kaq e mirë, saqë ndoshta nuk do të na nevojitet të mendojmë madje për pivot-et e tabelave me mjete programore. Më parë, na duhej të bënim pivot të të dhënave të nxjerra nga Vertica përmes eksportit në Apache Parquet.
FatkeqĂ«sisht, pĂ«rpjekja e radhĂ«s pĂ«r tĂ« pĂ«rdorur njĂ« DB graf ishte e pasuksesshme. Nuk e gjeta JanusGraph si njĂ« ekosistem miqĂ«sor, qĂ« lejon njĂ« adaptim tĂ« shpejtĂ« me produktin. NdĂ«rkohĂ«, konfigurimi i serverit ndiqet nga mĂ«nyra tradicionale Java, qĂ« do tâi bĂ«jĂ« tĂ« qajnĂ« me lot tĂ« kuq njerĂ«zit qĂ« nuk janĂ« tĂ« njohur me 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 është këtu për të zëvendësuar Gryo dhe 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 dhe Graphson, versionet më të reja
- { 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] }}
# Versione më të vjetra serializimi për kompatibilitetin mbrapa:
- { 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}Më rezultoi rastësi "të vendosja" versionin BerkeleyDB të JanusGraph.
Dokumentacioni është mjaft i çrregullt sa i përket indekseve, pasi administrimi i indekseve kërkon të bëhen disa magji të çuditshme në Groovy. Për shembull, krijimi i një indeksi duhet të bëhet duke shkruar kod në konsolën Gremlin (e cila, për të qenë e saktë, nuk punon direkt). Nga dokumentacioni zyrtar i JanusGraph:
graph.tx().rollback() //Kurrë mos krijoni indekse të reja ndërsa një transaksion është aktiv
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()
//Prisni që indeksi të bëhet i disponueshëm
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
//Rindizajni të dhënat ekzistuese
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()Pasthënie
Në një kuptim, eksperimenti i sipërm është një krahasim i ngrohtë me të butë. Nëse e mendoni, një DB graf-zhvillim kryen operacione të tjera për të arritur të njëjtat rezultate. Megjithatë, gjatë testeve unë bëra edhe një eksperiment me një kërkesë të këtij lloji:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()i cili pasqyron aksesin në hapësira. Megjithatë, edhe me të dhëna të tilla, baza e të dhënave grafike tregoi rezultate që tejkalonin disa sekonda... Kjo, natyrisht, lidhet me faktin se kishte rrugë të tilla 0 -> X -> Y ... -> 1, të cilat motori grafik gjithashtu i kontrollonte.
Madje për një kërkesë të tillë:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()nuk më arriti të merrja një përgjigje produktive me kohë përpunimi më pak se një sekondë.
Moraliteti i fabulës është se një ide e bukur dhe modelizimi paradigmatik nuk çojnë në rezultatin e dëshiruar, i cili demonstrohet me një efikasitet shumë më të lartë në shembullin e ClickHouse. Shembulli i sjellë në këtë artikull është një antipater të qartë për bazat e të dhënave grafike, megjithëse duket se është i përshtatshëm për modelim në paradigmën e tyre.
Burimi: habr.com
