
Halo semuanya. Kami sedang mengembangkan produk untuk analisis lalu lintas offline. Proyek ini melibatkan analisis statistik pola lalu lintas pengunjung di berbagai wilayah.
Sebagai bagian dari tugas ini, pengguna dapat mengajukan pertanyaan sistem jenis berikut:
- berapa banyak pengunjung yang berpindah dari area "A" ke area "B";
- berapa banyak pengunjung yang berpindah dari area "A" ke area "B" melalui area "C" dan kemudian melalui area "D";
- Berapa lama waktu yang dibutuhkan pengunjung jenis tertentu untuk berpindah dari area "A" ke area "B".
dan sejumlah pertanyaan analitis serupa lainnya.
Pergerakan pengunjung melalui berbagai wilayah direpresentasikan oleh grafik berarah. Setelah membaca internet, saya menemukan bahwa DBMS grafik juga digunakan untuk laporan analitis. Saya ingin melihat bagaimana DBMS grafik menangani kueri semacam itu.TL; DR; Dengan buruk).
Saya memilih menggunakan DBMS , sebagai perwakilan terkemuka dari DBMS grafik sumber terbuka yang bergantung pada setumpuk teknologi matang yang (menurut pendapat saya) seharusnya menyediakannya dengan karakteristik operasional yang layak:
- BerkeleyDB, Apache Cassandra, penyimpanan backend Scylla;
- Indeks kompleks dapat disimpan di Lucene, Elasticsearch, Solr.
Penulis JanusGraph menulis bahwa ini cocok untuk OLTP dan OLAP.
Saya pernah menggunakan BerkeleyDB, Apache Cassandra, Scylla, dan ES, dan produk-produk ini sering digunakan di sistem kami, jadi saya optimistis untuk menguji DBMS grafik ini. Pilihan BerkeleyDB daripada RocksDB terasa aneh bagi saya, tetapi kemungkinan besar ini karena persyaratan transaksi. Bagaimanapun, untuk penggunaan skalabel dan produksi, kami merekomendasikan penggunaan backend Cassandra atau Scylla.
Saya tidak mempertimbangkan Neo4j karena pengelompokan memerlukan versi komersial, artinya itu bukan produk sumber terbuka.
DBMS grafik berkata, "Jika sesuatu tampak seperti grafik, perlakukan seperti grafik!" - indah!
Pertama, saya menggambar grafik yang mengikuti aturan DBMS grafik:

Ada esensinya Zone, bertanggung jawab atas area Jika ZoneStep milik ini Zone, lalu dia merujuknya. Kepada entitas Area, ZoneTrack, Person Abaikan mereka, karena mereka termasuk dalam domain dan tidak dipertimbangkan dalam pengujian. Jadi, untuk struktur grafik seperti itu, kueri untuk menemukan rantai akan terlihat seperti ini:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()Yang dalam bahasa Rusia seperti ini: temukan Zona dengan ID=0, ambil semua simpul dari mana sisi menuju ke sana (ZoneStep), bergerak tanpa kembali hingga Anda menemukan ZoneSteps dari mana sisi menuju ke Zona dengan ID=19, hitung jumlah rantai seperti itu.
Saya tidak mengklaim mengetahui semua seluk-beluk pencarian grafik, tetapi kueri ini dibuat berdasarkan buku ini ().
Saya memuat 50 trek dengan panjang 3 hingga 20 poin ke dalam database grafik JanusGraph menggunakan backend BerkeleyDB, membuat indeks sesuai dengan .
Skrip unduhan Python:
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)Sebuah VM dengan 4 inti dan RAM 16 GB pada SSD digunakan. JanusGraph di-deploy menggunakan perintah berikut:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latestDalam kasus ini, data dan indeks yang digunakan untuk pencarian pencocokan persis disimpan di BerkeleyDB. Dengan menjalankan kueri yang dijelaskan sebelumnya, saya mendapatkan waktu proses beberapa puluh detik.
Dengan menjalankan keempat skrip di atas secara paralel, saya dapat mengubah DBMS menjadi labu dengan aliran jejak tumpukan Java yang ceria (dan kita semua suka membaca jejak tumpukan Java) di log Docker.
Setelah berpikir sejenak, saya memutuskan untuk menyederhanakan diagram grafik menjadi berikut:

Saya memutuskan bahwa pencarian berdasarkan atribut entitas akan lebih cepat daripada pencarian berdasarkan tepi. Akhirnya, pertanyaan saya menjadi seperti ini:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()Yang dalam bahasa Rusia kira-kira seperti ini: temukan ZoneStep dengan ID=0, teruskan hingga Anda menemukan ZoneStep dengan ID=19, hitung jumlah rantai seperti itu.
Saya juga menyederhanakan skrip pemuatan yang diberikan di atas untuk menghindari pembuatan koneksi yang tidak diperlukan, dan membatasinya pada atribut.
Permintaan tersebut masih memerlukan waktu beberapa detik untuk dieksekusi, yang sama sekali tidak dapat diterima untuk tugas kami, karena tidak cocok untuk tujuan permintaan AdHoc jenis apa pun.
Saya mencoba menerapkan JanusGraph menggunakan Scylla, implementasi Cassandra tercepat, tetapi itu juga tidak menghasilkan peningkatan kinerja yang signifikan.
Jadi, meskipun "tampak seperti grafik", saya tidak bisa mendapatkan DBMS grafik untuk menanganinya dengan cepat. Saya cukup yakin ada yang terlewat dan memang mungkin untuk memaksa JanusGraph melakukan pencarian ini dalam sepersekian detik, tetapi saya tidak bisa.
Karena masalah tersebut masih perlu dipecahkan, saya mulai berpikir tentang JOIN dan tabel Pivot, yang tidak memunculkan optimisme dalam hal keanggunan, tetapi bisa menjadi pilihan yang sepenuhnya bisa diterapkan dalam praktik.
Proyek kami sudah menggunakan Apache ClickHouse, jadi saya memutuskan untuk menguji temuan saya pada DBMS analitis ini.
Saya menerapkan ClickHouse menggunakan resep sederhana:
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-serverSaya membuat database dan tabel dengan tipe berikut:
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 = 8192Saya mengisinya dengan data menggunakan skrip berikut:
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
)Karena sisipan dikirim secara bertahap, pengisiannya jauh lebih cepat dibandingkan dengan JanusGraph.
Saya membuat dua kueri menggunakan JOIN. Untuk berpindah dari titik A ke titik 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.whenUntuk melewati 3 poin:
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.whenKueri-kueri tersebut memang terlihat cukup menakutkan; untuk penggunaan di dunia nyata, generator perangkat lunak akan diperlukan. Namun, kueri-kueri tersebut berfungsi, dan bekerja dengan cepat. Baik kueri pertama maupun kedua dieksekusi dalam waktu kurang dari 0.1 detik. Berikut contoh waktu eksekusi kueri untuk loop count(*) melalui tiga titik:
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.)Catatan tentang IOPSSaat melakukan seeding data, JanusGraph menghasilkan angka IOPS yang cukup tinggi (1000-1300 untuk empat thread seeding), dan IOWAIT cukup tinggi. Sementara itu, ClickHouse menghasilkan beban minimal pada subsistem disk.
Kesimpulan
Kami memutuskan untuk menggunakan ClickHouse untuk menangani jenis kueri ini. Kami selalu dapat mengoptimalkan kueri lebih lanjut menggunakan tampilan termaterialisasi dan paralelisasi, dengan melakukan pra-pemrosesan aliran peristiwa dengan Apache Flink sebelum memuatnya ke ClickHouse.
Performanya sangat baik sehingga kami mungkin tidak perlu lagi khawatir tentang tabel pivot secara terprogram. Sebelumnya, kami harus melakukan pivot pada data yang diekstrak dari Vertica dengan mengunggahnya ke Apache Parquet.
Sayangnya, upaya terakhir saya menggunakan DBMS grafik tidak berhasil. Saya merasa JanusGraph tidak memiliki ekosistem yang ramah pengguna yang memungkinkan adopsi cepat. Selain itu, konfigurasi server menggunakan pendekatan Java tradisional, yang akan membuat mereka yang tidak terbiasa dengan Java menangis tersedu-sedu:
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}Saya tak sengaja membuat JanusGraph versi BerkeleyDB crash.
Dokumentasi untuk indeks agak kurang lengkap, karena mengelola indeks memerlukan beberapa trik Groovy yang agak aneh. Misalnya, membuat indeks memerlukan penulisan kode di konsol Gremlin (yang, omong-omong, tidak langsung berfungsi). Dari dokumentasi resmi JanusGraph:
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()penutup
Dalam arti tertentu, eksperimen di atas merupakan perbandingan antara hangat dan lunak. Jika dipikir-pikir, DBMS grafik melakukan operasi yang berbeda untuk mencapai hasil yang sama. Namun, sebagai bagian dari pengujian saya, saya juga menjalankan eksperimen dengan kueri seperti ini:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()yang mencerminkan jarak berjalan kaki. Namun, bahkan dengan data tersebut, grafik DBMS menunjukkan hasil yang melebihi beberapa detik... Hal ini, tentu saja, disebabkan oleh fakta bahwa ada jalur jenis 0 -> X -> Y ... -> 1, yang juga diperiksa oleh mesin grafik.
Bahkan untuk pertanyaan seperti:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()Saya tidak dapat memperoleh respons yang berkinerja dengan waktu pemrosesan kurang dari satu detik.
Moral dari cerita ini adalah bahwa ide yang indah dan pemodelan paradigmatik tidak mengarah pada hasil yang diinginkan, yang ditunjukkan dengan jauh lebih efektif dalam contoh ClickHouse. Kasus penggunaan yang disajikan dalam artikel ini merupakan anti-pola yang jelas untuk DBMS grafik, meskipun tampaknya cocok untuk pemodelan dalam paradigma mereka.
Sumber: www.habr.com
