Hamıya salam. Biz oflayn trafik təhlili üçün məhsul hazırlayırıq. Layihədə regionlar üzrə ziyarətçi marşrutlarının statistik təhlili ilə bağlı tapşırıq var.
Bu tapşırığın bir hissəsi olaraq istifadəçilər aşağıdakı növ sistem sorğularını verə bilərlər:
- “A” sahəsindən “B” sahəsinə nə qədər ziyarətçi keçdi;
- nə qədər ziyarətçi "A" sahəsindən "B" sahəsinə "C" sahəsi ilə, sonra isə "D" sahəsindən keçib;
- müəyyən bir növ ziyarətçinin “A” sahəsindən “B” sahəsinə səyahət etməsi üçün nə qədər vaxt lazım idi.
və bir sıra oxşar analitik sorğular.
Ziyarətçinin ərazilər arasında hərəkəti istiqamətləndirilmiş qrafikdir. İnterneti oxuduqdan sonra mən kəşf etdim ki, qrafik DBMS-lər analitik hesabatlar üçün də istifadə olunur. Qrafik DBMS-lərin bu cür sorğuların öhdəsindən necə gələcəyini görmək istəyim var idi (TL; DR; zəif).
Mən DBMS-dən istifadə etməyi seçdim
- BerkeleyDB saxlama dəstəyi, Apache Cassandra, Scylla;
- mürəkkəb indekslər Lucene, Elasticsearch, Solr-da saxlanıla bilər.
JanusGraph müəllifləri onun həm OLTP, həm də OLAP üçün uyğun olduğunu yazır.
Mən BerkeleyDB, Apache Cassandra, Scylla və ES ilə işləmişəm və bu məhsullar sistemlərimizdə tez-tez istifadə olunur, ona görə də bu qrafik DBMS-ni sınamaqda optimist idim. RocksDB əvəzinə BerkeleyDB-ni seçmək mənə qəribə gəldi, lakin bu, çox güman ki, əməliyyat tələbləri ilə bağlıdır. İstənilən halda, miqyaslana bilən, məhsul istifadəsi üçün Cassandra və ya Scylla-da arxa hissədən istifadə etmək tövsiyə olunur.
Neo4j-i nəzərə almadım, çünki klasterləşmə kommersiya versiyasını tələb edir, yəni məhsul açıq mənbə deyil.
Qrafik DBMS-lər deyir: "Əgər o, qrafikə bənzəyirsə, onu qrafik kimi qəbul edin!" - gözəllik!
Əvvəlcə DBMS qrafikinin qanunlarına uyğun olaraq hazırlanmış bir qrafik çəkdim:
mahiyyəti var Zone
, əraziyə cavabdehdir. Əgər ZoneStep
buna aiddir Zone
, sonra ona istinad edir. Mahiyyəti üzrə Area
, ZoneTrack
, Person
Diqqət etməyin, onlar domenə aiddir və testin bir hissəsi hesab edilmir. Ümumilikdə, belə bir qrafik strukturu üçün zəncirvari axtarış sorğusu belə görünür:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()
Rus dilində belə bir şey var: ID=0 olan bir Zona tapın, kənarın ona getdiyi bütün təpələri götürün (ZoneStep), Zona ilə kənar olan ZoneAddımları tapana qədər geri dönmədən ayaq üstələyin. ID=19, belə zəncirlərin sayını sayın.
Qrafiklərdə axtarışın bütün incəliklərini bildiyimi iddia etmirəm, lakin bu sorğu bu kitab əsasında yaradılıb (
BerkeleyDB backendindən istifadə edərək JanusGraph qrafik verilənlər bazasına uzunluğu 50 ilə 3 bal arasında dəyişən 20 min treki yüklədim, uyğun olaraq indekslər yaratdım.
Python yükləmə skripti:
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)
SSD-də 4 nüvəli və 16 GB RAM olan VM-dən istifadə etdik. JanusGraph bu əmrdən istifadə edərək yerləşdirilib:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest
Bu halda, dəqiq uyğunluq axtarışları üçün istifadə olunan məlumat və indekslər BerkeleyDB-də saxlanılır. Daha əvvəl verilmiş sorğunu yerinə yetirərək, bir neçə on saniyəyə bərabər vaxt aldım.
Yuxarıdakı 4 skripti paralel olaraq işlətməklə, mən DBMS-ni Docker qeydlərində şən Java stacktraces axını (və biz hamımız Java stacktraces oxumağı sevirik) ilə balqabaq halına gətirə bildim.
Bir az fikirləşdikdən sonra qrafik diaqramı aşağıdakı kimi sadələşdirmək qərarına gəldim:
Müəssisə atributları üzrə axtarışın kənarlar üzrə axtarışdan daha sürətli olacağına qərar vermək. Nəticə etibarı ilə müraciətim belə oldu:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()
Rus dilində belə bir şey var: ID=0 olan ZoneStep-i tapın, ID=19-lu ZoneStep-i tapana qədər geri qayıtmadan ayaq bas, belə zəncirlərin sayını sayın.
Özümü atributlarla məhdudlaşdıraraq, lazımsız əlaqələr yaratmamaq üçün yuxarıda verilmiş yükləmə skriptini də sadələşdirdim.
Sorğunun yerinə yetirilməsi hələ də bir neçə saniyə çəkdi, bu, bizim tapşırığımız üçün tamamilə qəbuledilməz idi, çünki o, istənilən növ AdHoc sorğularının məqsədləri üçün heç də uyğun deyildi.
Ən sürətli Cassandra tətbiqi olaraq Scylla-dan istifadə edərək JanusGraph-ı yerləşdirməyə çalışdım, lakin bu da heç bir əhəmiyyətli performans dəyişikliyinə səbəb olmadı.
Beləliklə, "bu, qrafikə bənzəyir" olmasına baxmayaraq, onu tez bir zamanda emal etmək üçün DBMS qrafikini əldə edə bilmədim. Tamamilə güman edirəm ki, bilmədiyim bir şey var və JanusGraph bu axtarışı saniyənin bir hissəsində həyata keçirmək üçün edilə bilər, lakin bunu edə bilmədim.
Problem hələ də həll edilməli olduğundan, zəriflik baxımından optimizmi ruhlandırmayan, lakin praktikada tamamilə işlək bir seçim ola bilən masaların JOIN və Pivotları haqqında düşünməyə başladım.
Layihəmiz artıq Apache ClickHouse-dan istifadə edir, ona görə də araşdırmalarımı bu analitik DBMS üzərində sınamaq qərarına gəldim.
Sadə bir reseptdən istifadə edərək yerləşdirilmiş 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
Mən verilənlər bazası və orada cədvəl yaratdım:
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
Aşağıdakı skriptdən istifadə edərək onu məlumatlarla doldurdum:
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
)
Daxiletmələr partiyalar şəklində gəldiyi üçün doldurma JanusGraph ilə müqayisədə daha sürətli idi.
JOIN istifadə edərək iki sorğu quruldu. A nöqtəsindən B nöqtəsinə keçmək üçün:
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 nöqtədən keçmək üçün:
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
İstəklər, əlbəttə ki, olduqca qorxulu görünür; real istifadə üçün bir proqram generatoru qoşqu yaratmalısınız. Bununla belə, onlar işləyir və tez işləyirlər. Həm birinci, həm də ikinci sorğular 0.1 saniyədən az müddətdə tamamlanır. 3 nöqtədən keçən count(*) üçün sorğunun icra müddətinə bir nümunə:
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 haqqında qeyd. Məlumatları doldurarkən, JanusGraph kifayət qədər yüksək sayda IOPS (dörd məlumat kütləsi üçün 1000-1300) yaratdı və IOWAIT olduqca yüksək idi. Eyni zamanda, ClickHouse disk alt sistemində minimal yük yaratdı.
Nəticə
Bu tip sorğuya xidmət etmək üçün ClickHouse-dan istifadə etmək qərarına gəldik. Biz həmişə sorğuları ClickHouse-a yükləməzdən əvvəl Apache Flink-dən istifadə edərək hadisə axınını əvvəlcədən emal etməklə maddiləşdirilmiş görünüşlərdən və paralelləşdirmədən istifadə edərək daha da optimallaşdıra bilərik.
Performans o qədər yaxşıdır ki, yəqin ki, cədvəlləri proqramlı şəkildə çevirmək barədə düşünməyə belə ehtiyac qalmayacaq. Əvvəllər biz Vertica-dan Apache Parket-ə yükləmə yolu ilə əldə edilmiş məlumatların pivotlarını etməli idik.
Təəssüf ki, DBMS qrafikindən istifadə etmək üçün başqa bir cəhd uğursuz oldu. Mən JanusGraph-da məhsulun sürətini artırmağı asanlaşdıran dostluq ekosistemine malik olduğunu görmədim. Eyni zamanda, serveri konfiqurasiya etmək üçün ənənəvi Java üsulundan istifadə olunur ki, bu da Java ilə tanış olmayan insanları qan gözyaşı tökməyə məcbur edəcək:
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}
Mən təsadüfən JanusGraph-ın BerkeleyDB versiyasını “qoya bildim”.
Sənədlər indekslər baxımından olduqca əyridir, çünki indeksləri idarə etmək sizdən Groovy-də olduqca qəribə şamanizmi yerinə yetirməyi tələb edir. Məsələn, indeks yaratmaq Gremlin konsolunda kod yazmaqla edilməlidir (bu, yeri gəlmişkən, qutudan kənarda işləmir). Rəsmi JanusGraph sənədlərindən:
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()
Sözündən sonra
Müəyyən mənada yuxarıdakı təcrübə isti və yumşaq arasında müqayisədir. Fikir versəniz, qrafik DBMS eyni nəticələri əldə etmək üçün başqa əməliyyatları yerinə yetirir. Bununla birlikdə, testlərin bir hissəsi olaraq, mən də aşağıdakı kimi bir sorğu ilə bir təcrübə keçirdim:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()
bu da gediş məsafəsini əks etdirir. Bununla belə, belə məlumatlarda belə, DBMS qrafiki bir neçə saniyədən çox davam edən nəticələr göstərdi... Bu, təbii ki, belə yolların olması ilə əlaqədardır. 0 -> X -> Y ... -> 1
, bunu qrafik mühərriki də yoxladı.
Hətta belə bir sorğu üçün:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()
Bir saniyədən az işləmə müddəti ilə məhsuldar cavab ala bilmədim.
Hekayənin əxlaqı ondan ibarətdir ki, gözəl ideya və paradiqmatik modelləşdirmə istənilən nəticəyə gətirib çıxarmır ki, bu da ClickHouse nümunəsindən istifadə etməklə daha yüksək səmərəliliklə nümayiş etdirilir. Bu məqalədə təqdim olunan istifadə nümunəsi, onların paradiqmasında modelləşdirmə üçün uyğun görünsə də, qrafik DBMS-lər üçün aydın bir anti-naxışdır.
Mənbə: www.habr.com