
Tere kõigile. Me arendame toodet offline-liikluse analüüsimiseks. Projekti raames on ülesanne, mis on seotud külastajate liikumisteede statistilise analüüsiga.
Selle ülesande raames saavad kasutajad esitada süsteemile järgmise tüüpi päringuid:
- kui palju külastajaid liikus alalt "A" alale "B";
- kui palju külastajaid liikus alalt "A" alale "B" üle ala "C" ja seejärel ala "D";
- kui kaua kulus teatud tüüpi külastaja liikumiseks alalt "A" alale "B."
ja veel mitmeid sarnaseid analüütilisi päringuid.
Külastaja liikumine alade vahel esindab suunatud graafi. Internetis lugedes avastasin, et graafikandjad on kasutusel ka analüütilistes aruannetes. Mul tekkis huvi vaadata, kuidas selliste päringutega toime tulevad graafikandjad (TL;DR; halvasti).
Valisin kasutamiseks andmebaasi , kui silmapaistva esindaja graafilistest avatud lähtekoodiga andmebaasidest, mis toetuvad küpsete tehnoloogiate kogumile, mis (minu arvates) peaks tagama korralikud operatiivsed omadused:
- salvestustase BerkeleyDB, Apache Cassandra, Scylla;
- komplekse indekseerimist saab salvestada Lucenes, Elasticsearchis, Solris.
JanusGraph'i autorid kirjutavad, et see sobib nii OLTP kui ka OLAP jaoks.
Olen töötanud BerkeleyDB, Apache Cassandra, Scylla ja ES-ga, lisaks on need tooted sageli meie süsteemides kasutusel, seetõttu vaatasin graafikandjat testimisele optimistlikult. Mulle tundus kummaline valida BerkeleyDB, mitte RocksDB, kuid tõenäoliselt on see seotud nõudmistega tehingute osas. Igatahes, skaleeritava, tootmis kasutuse jaoks soovitatakse kasutada Cassandrat või Scyllat taustana.
Neo4j ei olnud ma vaadanud, kuna klastri jaoks on vajalik kommertsversioon, seega pole toode avatud.
Graafikandjad ütlevad: "Kui midagi näeb välja nagu graaf, töödelge seda nagu graafi!" — ilus!
Esmalt joonistasin graafi, mis on tõeliselt graafikandjate kanonite järgi loodud:

On olemas entiteet Zone, mis vastutab ala eest. Kui ZoneStep kuulub sellele, siis viitab see sellele. Entiteedid ZoneArea ZoneTrack, Person, ei pane tähele, need kuuluvad domeeni ja ei ole testi raames käsitletud. Seega, sellise graafilise struktuuri puhul näeks otsinguahelate päring välja nagu: внимание не обращайте, они принадлежат домену и в рамках теста не рассматриваются. Итого, к такой графовой структуре запрос поиска цепочек выглядел бы как:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()See eesti keeles: leia Zone id-ga 0, võta kõik tipud, millel on serv (ZoneStep), kulge tagasi pöördumata, kuni leiad sellised ZoneStep'id, millel on serv Zone'iga id-ga 19, loe selliste ahelate arv.
Ma ei pretendeeri graafide otsimise kõigi nüansside teadmisele, kuid see päring genereeriti selle raamatu põhjal ().
Olen laadinud 50 000 lugu, mille pikkus on 3 kuni 20 punkti, graafandmebaasi JanusGraph, mis kasutab BerkeleyDB tagapinda, ja loonud indekseid vastavalt .
Laadimiscript Pythonis:
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)Kasutati VM-i, millel on 4 tuuma ja 16 GB RAM SSD-l. JanusGraph pandi üles järgmise käsu abil:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latestSellisel juhul hoitakse andmeid ja indekseid, mida kasutatakse täpse vaste otsimiseks, BerkeleyDB-s. Tehes eelnevalt toodu päringu, sain tulemuseks mitukümmend sekundit.
Käivitasin 4 eespool mainitud skripti paralleelselt, suutsin muuta andmebaasi kõrvitsaks koos rõõmsa Java stekki jäljendiga (ja me kõik armastame lugeda Java stekki jälgi) Dockeri logides.
Mõeldes sellele, otsustasin lihtsustada graafi skeemi järgmiseks:

Otsustades, et objekti atribuutide järgi otsimine on kiirem kui servade järgi. Lõpuks mu päring muutus järgmiseks:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()See eesti keeles: leia ZoneStep id-ga 0, kulge tagasi pöördumata, kuni leiate ZoneStep id-ga 19, loe selliste ahelate arv.
Eespool toodud laadimiscripti olen samuti lihtsustanud, et mitte luua tarbetuid seoseid, piirdudes ainult atributidega.
Küsimus kestis siiski mitu sekundit, mis meie ülesande jaoks oli täiesti vastuvõetamatu, kuna ad-hoc küsimustega ei sobinud see üldse.
Proovisin JanusGraphi käivitada Scylla abil, mis on Cassandra kõige kiirem rakendus, kuid see ei andnud mingit olulist tulemuslikkuse tõusu.
Seega, hoolimata sellest, et "see näeb välja nagu graaf", ei suutnud ma graafipõhist andmebaasi selle kiiresti töötama panna. Oletan, et ma ei tea midagi ja JanusGraphi saab selle otsinguga töötama panna sekundite jooksul, kuid mul see ei õnnestunud.
Kuna ülesanne pidi olema lahendatud, hakkasin mõtlema JOIN-idele ja Pivot-tabelitele, mis ei pakkunud palju optimismi stiili osas, kuid võisid praktikas olla üsna toimivad.
Meie projektis kasutatakse juba Apache ClickHouse'i, seega otsustasin oma avastusi selle analüütilise andmebaasi abil testida.
Käivitamine ClickHouse'i lihtsa retsepti järgi:
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-serverLoomiskäigus andmebaas ja tabel järgmise struktuuriga:
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 = 8192Täitsin selle andmetega järgmise skriptiga:
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
)Kuna sisestused toimuvad partiidena, oli täitmine palju kiirem kui JanusGraphiga.
Konstrueerisin kaks päringut JOIN-ide abil. A-st B-sse liikumiseks:
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.whenKolme punkti kaudu liikumiseks:
VALI s3.inimene,
s1z,
s1w,
s2z,
s2w,
s3.tsooni,
s3.kui
FROM
(VALI s1.inimene KUI nimed,
s1.tsoon AS s1z,
s1.kui AS s1w,
s2.tsoon AS s2z,
s2.kui AS s2w
FROM
(VALI *
FROM sammud
WHERE (ala = 0)
AND (tsoon = 0)) NIMEDEGA s1 KUID INNER JOIN
(VALI *
FROM sammud NIMEDEGA s2
WHERE (ala = 0)
AND (tsoon = 3)) NIMEDEGA s2 KASUTADES inimene
KUS s1.kui <= s2.kui) p KUID INNER JOIN
(VALI *
FROM sammud
WHERE (ala = 0)
AND (tsoon = 19)) NIMEDEGA s3 KASUTADES inimene
KUS p.s2w <= s3.kuiKüsitlused näivad muidugi üsna hirmutavad, kuid tegelikuks kasutuseks on vajalik tuua programmiline raamistiku generaator. Siiski, need töötavad ja töötavad kiiresti. Nii esimene kui ka teine päring täidetakse vähem kui 0,1 sekundiga. Siin on näide päringu täitmise ajast count(*) läbimise jaoks 3 punkti kaudu:
VALI count(*)
FROM
(
VALI
s1.inimene AS inimene,
s1.tsoon AS s1z,
s1.kui AS s1w,
s2.tsoon AS s2z,
s2.kui AS s2w
FROM
(
VALI *
FROM sammud
WHERE (ala = 0) JA (tsoon = 0)
) NIMEDEGA s1
KUID INNER JOIN
(
VALI *
FROM sammud NIMEDEGA s2
WHERE (ala = 0) JA (tsoon = 3)
) NIMEDEGA s2 KASUTADES (inimene)
KUS s1.kui <= s2.kui
) NIMEDEGA p
KUID INNER JOIN
(
VALI *
FROM sammud
WHERE (ala = 0) JA (tsoon = 19)
) NIMEDEGA s3 KASUTADES (inimene)
KUS p.s2w <= s3.kui
┌─count()─┐
│ 11592 │
└─────────┘1 rida komplektis. Kulunud: 0,068 s. Töödeldud 250,03 tuhat rida, 8,00 MB (3,69 miljonit rida/s., 117,98 MB/s.)Märkus IOPS kohta. Andmete täitmisel genereeris JanusGraph suhteliselt kõrge IOPS (1000-1300 nelja andmete täitmise voolu jaoks), samas kui IOWAIT oli üsna kõrge. Samal ajal genereeris ClickHouse minimaalset koormust kettasüsteemile.
Kokkuvõte
Otsustasime kasutada ClickHouse'i selliste päringute teenindamiseks. Saame alati veelgi optimeerida päringuid, kasutades materialiseeritud vaateid ja paralleliseerimist, tehes eelnevat töötlemist sündmuste voogude jaoks Apache Flinki abil enne nende laadimist ClickHouse'i.
Jõudlus on nii hea, et me tõenäoliselt ei pea isegi mõtlema pivot-table'ide programmilise läbiviimise peale. Varem pidime me tegema andmete pivot'e, mis pärinevad Verticast läbi eksportimise Apache Parquet'i.
Kahjuks ei olnud veel üks katse grafitabelite DB kasutamisel edukas. Ma ei leidnud, et JanusGraph'i ekosüsteem oleks sõbralik, mis võimaldaks toote kiiret omandamist. Samuti kasutab serveri konfiguratsioon traditsioonilist Java-viisi, mis paneks inimesi, kes ei tunne Java-d, veretuks.
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 on asend Gryo ja 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 ja Graphson, uusimad versioonid
- { 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] }}
# Vana serialiseerimise versioonid tagurpidi ühilduvuse jaoks:
- { 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}Olin juhuslikult "panema" BerkeleyDB versiooni JanusGraph.
Dokumentatsioon on indeksite osas üsna kohmakas, kuna indeksite haldamiseks on vaja teha üsna kummalisi rituaale Groovy-s. Näiteks indeksi loomine peab toimuma Gremlini konsoolis koodi kirjutamise kaudu (mille tööle saamine ei toimi muide kohe). JanusGraphi ametlikest dokumentidest:
graph.tx().rollback() // Ära loo uusi indekseid, kui tehing on aktiivne
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()
// Oota, kuni indeks on saadaval
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
// Uuenda olemasolevaid andmeid
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()Eessõna
Teatud mõttes on eelpooltoodud eksperiment võrreldav sooja ja pehme võrdlemisega. Kui süveneda, siis graafiku andmebaas teostab teisi operatsioone, et saavutada samu tulemusi. Siiski tegin testide raames ka eksperiment järgmise taolise päringuga:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next(), mis peegeldab astmelist ligipääsetavust. Siiski näitas graafiku andmebaas isegi selliste andmete puhul tulemust, mis ületas mitut sekundit ... See on muidugi seotud sellega, et olid teed, millel oli 0 -> X -> Y ... -> 1, mida graafiku mootor samuti kontrollis.
Isegi järgmise taolise päringu puhul:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()ei õnnestunud mul saada tõhusat vastust töötlusajaga alla sekundi.
Moraal on see, et ilus ide ja paradigmastik modelleerimine ei viida soovitud tulemusele, mida märksa kõrgema efektiivsusega demonstreeritakse ClickHouse'i näitel. Artiklis toodud näidisjuhtum on selge antipattern graafiku andmebaaside jaoks, kuigi see näeb välja sobiv nende paradigma modelleerimiseks.
Allikas: habr.com
