Zdravo svima. Razvijamo proizvod za analizu offline prometa. Zadatak projekta je statistička analiza ruta posjetilaca po regijama.
Kao dio ovog zadatka, korisnici mogu postavljati sistemske upite sljedećeg tipa:
- koliko je posjetilaca prešlo iz područja "A" u područje "B";
- koliko je posjetilaca prošlo iz područja "A" u područje "B" kroz područje "C", a zatim kroz područje "D";
- koliko je vremena bilo potrebno određenom tipu posjetioca da putuje od područja “A” do područja “B”.
i niz sličnih analitičkih upita.
Kretanje posjetitelja kroz područja je usmjereni graf. Nakon čitanja interneta, otkrio sam da se grafički DBMS-ovi koriste i za analitičke izvještaje. Imao sam želju da vidim kako će se grafički DBMS nositi sa takvim upitima (TL; DR; loše).
Odlučio sam koristiti DBMS
- BerkeleyDB pozadina za pohranu, Apache Cassandra, Scylla;
- kompleksni indeksi se mogu pohraniti u Lucene, Elasticsearch, Solr.
Autori JanusGrapha pišu da je pogodan i za OLTP i za OLAP.
Radio sam sa BerkeleyDB, Apache Cassandrom, Scyllom i ES, i ovi proizvodi se često koriste u našim sistemima, tako da sam bio optimističan u pogledu testiranja ovog grafikona DBMS. Bilo mi je čudno izabrati BerkeleyDB u odnosu na RocksDB, ali to je vjerovatno zbog zahtjeva za transakcijama. U svakom slučaju, za skalabilno korištenje proizvoda, predlaže se korištenje backenda na Cassandra ili Scylla.
Nisam razmatrao Neo4j jer je za klasterisanje potrebna komercijalna verzija, odnosno proizvod nije otvorenog koda.
Grafički DBMS-ovi kažu: "Ako izgleda kao graf, tretirajte ga kao graf!" - ljepota!
Prvo sam nacrtao graf, koji je napravljen tačno po kanonima grafskih DBMS-a:
Postoji suština Zone
, odgovorna za područje. Ako ZoneStep
pripada ovome Zone
, onda se on poziva na to. O suštini Area
, ZoneTrack
, Person
Ne obraćajte pažnju, oni pripadaju domeni i ne smatraju se dijelom testa. Ukupno, lančani upit za pretragu za takvu strukturu grafa bi izgledao ovako:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()
Ono što je na ruskom je otprilike ovako: pronađite zonu sa ID=0, uzmite sve vrhove iz kojih ivica ide do nje (ZoneStep), gazite bez vraćanja nazad dok ne pronađete one ZoneSteps od kojih postoji ivica u zonu sa ID=19, izbroji broj takvih lanaca.
Ne pretvaram se da znam sve zamršenosti pretraživanja na grafovima, ali ovaj upit je generiran na osnovu ove knjige (
Učitao sam 50 hiljada numera dužine od 3 do 20 tačaka u JanusGraph bazu podataka grafova koristeći pozadinu BerkeleyDB, kreirao indekse prema
Python skripta za preuzimanje:
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)
Koristili smo VM sa 4 jezgra i 16 GB RAM-a na SSD-u. JanusGraph je raspoređen pomoću ove naredbe:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest
U ovom slučaju, podaci i indeksi koji se koriste za traženje tačnih podudaranja pohranjuju se u BerkeleyDB. Nakon što sam izvršio ranije dat zahtjev, dobio sam vrijeme od nekoliko desetina sekundi.
Pokretanjem 4 gornje skripte paralelno, uspio sam pretvoriti DBMS u bundevu sa veselim nizom Java stacktraces (a svi volimo čitati Java stacktraces) u Docker logovima.
Nakon malo razmišljanja, odlučio sam da pojednostavim dijagram grafikona na sljedeće:
Odlučivanje da bi pretraživanje po atributima entiteta bilo brže od pretraživanja po rubovima. Kao rezultat toga, moj zahtjev se pretvorio u sljedeće:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()
Ono što je na ruskom je otprilike ovako: pronađite ZoneStep sa ID=0, gazite bez vraćanja dok ne nađete ZoneStep sa ID=19, izbrojite broj takvih lanaca.
Također sam pojednostavio gore datu skriptu za učitavanje kako ne bih stvarao nepotrebne veze, ograničavajući se na atribute.
Zahtjev je i dalje trajao nekoliko sekundi, što je za naš zadatak bilo potpuno neprihvatljivo, jer uopće nije bilo prikladno za potrebe AdHoc zahtjeva bilo koje vrste.
Pokušao sam implementirati JanusGraph koristeći Scyllu kao najbržu implementaciju Cassandre, ali ni to nije dovelo do značajnijih promjena u performansama.
Dakle, uprkos činjenici da "izgleda kao graf", nisam mogao da nateram DBMS da ga brzo obradi. U potpunosti pretpostavljam da nešto ne znam i moguće je natjerati JanusGraph da izvrši ovu pretragu u djeliću sekunde, međutim, ja to nisam uspio.
Pošto je problem još trebalo riješiti, počeo sam razmišljati o JOIN-ovima i Pivot-ovima tablica, što nije ulijevalo optimizam u smislu elegancije, ali bi moglo biti potpuno izvodljiva opcija u praksi.
Naš projekat već koristi Apache ClickHouse, pa sam odlučio da testiram svoje istraživanje na ovom analitičkom DBMS-u.
Postavljen ClickHouse pomoću jednostavnog recepta:
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
Napravio sam bazu podataka i tabelu u njoj ovako:
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
Popunio sam ga podacima koristeći sljedeću 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
)
Pošto umetci dolaze u serijama, punjenje je bilo mnogo brže nego za JanusGraph.
Napravljena su dva upita koristeći JOIN. Za prelazak od tačke A do tačke 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
Da biste prošli kroz 3 tačke:
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
Zahtjevi, naravno, izgledaju prilično zastrašujuće; za stvarnu upotrebu, potrebno je kreirati svežanj softverskog generatora. Međutim, rade i rade brzo. I prvi i drugi zahtjev se završavaju za manje od 0.1 sekunde. Evo primjera vremena izvršenja upita za count(*) koji prolazi kroz 3 tačke:
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.)
Napomena o IOPS-u. Prilikom popunjavanja podataka, JanusGraph je generirao prilično visok broj IOPS (1000-1300 za četiri niti populacije podataka) i IOWAIT je bio prilično visok. U isto vrijeme, ClickHouse je generirao minimalno opterećenje na podsistemu diska.
zaključak
Odlučili smo koristiti ClickHouse za servisiranje ove vrste zahtjeva. Uvijek možemo dodatno optimizirati upite korištenjem materijaliziranih pogleda i paralelizacije tako što ćemo unaprijed obraditi tok događaja pomoću Apache Flink-a prije nego što ih učitamo u ClickHouse.
Performanse su toliko dobre da vjerovatno nećemo morati ni razmišljati o programskom okretanju tabela. Ranije smo morali da radimo pivotove podataka preuzetih iz Vertice preko upload-a na Apache Parquet.
Nažalost, drugi pokušaj korištenja grafičkog DBMS-a bio je neuspješan. Nisam smatrao da JanusGraph ima prijateljski ekosistem koji je olakšao upoznavanje sa proizvodom. Istovremeno, za konfiguraciju servera koristi se tradicionalni Java način, koji će natjerati ljude koji nisu upoznati sa Javom isplakati krvne suze:
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}
Uspio sam slučajno da "stavim" BerkeleyDB verziju JanusGrapha.
Dokumentacija je prilično kriva u smislu indeksa, budući da upravljanje indeksima zahtijeva od vas da izvedete prilično čudan šamanizam u Groovyju. Na primjer, kreiranje indeksa mora biti obavljeno pisanjem koda u konzoli Gremlin (koja, inače, ne radi izvan kutije). Iz službene JanusGraph dokumentacije:
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()
Posle reči
U određenom smislu, gornji eksperiment je poređenje između toplog i mekog. Ako razmislite o tome, grafički DBMS izvodi druge operacije kako bi dobio iste rezultate. Međutim, u sklopu testova, izvršio sam i eksperiment sa zahtjevom poput:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()
što odražava pješačku udaljenost. Međutim, čak i na takvim podacima, graf DBMS je pokazao rezultate koji su prelazili nekoliko sekundi... To je, naravno, zbog činjenice da su postojali putevi poput 0 -> X -> Y ... -> 1
, što je grafski mehanizam također provjerio.
Čak i za upit poput:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()
Nisam mogao dobiti produktivan odgovor s vremenom obrade kraćim od jedne sekunde.
Moral priče je da lijepa ideja i paradigmatsko modeliranje ne dovode do željenog rezultata, što se na primjeru ClickHouse-a pokazuje s mnogo većom efikasnošću. Slučaj upotrebe predstavljen u ovom članku je jasan anti-uzorak za grafičke DBMS-ove, iako se čini prikladnim za modeliranje u njihovoj paradigmi.
izvor: www.habr.com