Halo semua. Kami sedang mengembangkan produk untuk analisis lalu lintas offline. Proyek ini mempunyai tugas terkait analisis statistik rute pengunjung lintas wilayah.
Sebagai bagian dari tugas ini, pengguna dapat menanyakan pertanyaan sistem jenis berikut:
- berapa pengunjung yang melintas dari area "A" ke area "B";
- berapa banyak pengunjung yang lewat dari area "A" ke area "B" melalui area "C" dan kemudian melalui area "D";
- berapa lama waktu yang dibutuhkan oleh jenis pengunjung tertentu untuk melakukan perjalanan dari area βAβ ke area βBβ.
dan sejumlah pertanyaan analitis serupa.
Pergerakan pengunjung melintasi area merupakan grafik berarah. Setelah membaca Internet, saya menemukan bahwa DBMS grafik juga digunakan untuk laporan analitis. Saya mempunyai keinginan untuk melihat bagaimana DBMS grafik akan mengatasi pertanyaan seperti itu (TL; DR; buruk).
Saya memilih untuk menggunakan DBMS
- Backend penyimpanan BerkeleyDB, Apache Cassandra, Scylla;
- indeks kompleks dapat disimpan di Lucene, Elasticsearch, Solr.
Penulis JanusGraph menulis bahwa ini cocok untuk OLTP dan OLAP.
Saya telah bekerja dengan BerkeleyDB, Apache Cassandra, Scylla dan ES, dan produk ini sering digunakan di sistem kami, jadi saya optimis untuk menguji DBMS grafik ini. Saya merasa aneh memilih BerkeleyDB daripada RocksDB, tapi itu mungkin karena persyaratan transaksi. Bagaimanapun, untuk penggunaan produk yang skalabel, disarankan untuk menggunakan backend di Cassandra atau Scylla.
Saya tidak mempertimbangkan Neo4j karena clustering memerlukan versi komersial, yaitu produknya bukan open source.
DBMS Grafik mengatakan: βJika terlihat seperti grafik, perlakukan seperti grafik!β - kecantikan!
Pertama, saya menggambar grafik, yang dibuat persis sesuai dengan aturan DBMS grafik:
Ada intinya Zone
, bertanggung jawab atas area tersebut. Jika ZoneStep
milik ini Zone
, lalu dia merujuknya. Intinya Area
, ZoneTrack
, Person
Jangan perhatikan, mereka termasuk dalam domain dan tidak dianggap sebagai bagian dari pengujian. Secara total, kueri penelusuran berantai untuk struktur grafik seperti itu akan terlihat seperti:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()
Yang dalam bahasa Rusia kira-kira seperti ini: temukan Zona dengan ID=0, ambil semua simpul dari mana sebuah tepi menuju ke sana (ZoneStep), injak tanpa kembali sampai Anda menemukan ZoneSteps yang darinya terdapat tepi ke Zona dengan ID=19, hitung jumlah rantai tersebut.
Saya tidak berpura-pura mengetahui semua seluk-beluk pencarian grafik, tetapi kueri ini dibuat berdasarkan buku ini (
Saya memuat 50 ribu trek dengan panjang mulai dari 3 hingga 20 poin ke dalam database grafik JanusGraph menggunakan backend BerkeleyDB, membuat indeks sesuai dengan
Skrip pengunduhan 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)
Kami menggunakan VM dengan 4 core dan RAM 16 GB pada SSD. JanusGraph dikerahkan menggunakan perintah ini:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest
Dalam hal ini, data dan indeks yang digunakan untuk pencarian pencocokan tepat disimpan di BerkeleyDB. Setelah menjalankan permintaan yang diberikan sebelumnya, saya menerima waktu yang setara dengan beberapa puluh detik.
Dengan menjalankan 4 skrip di atas secara paralel, saya berhasil mengubah DBMS menjadi labu dengan aliran Java stacktraces yang ceria (dan kita semua suka membaca Java stacktraces) di log Docker.
Setelah berpikir beberapa lama, saya memutuskan untuk menyederhanakan diagram grafik menjadi berikut:
Memutuskan bahwa pencarian berdasarkan atribut entitas akan lebih cepat daripada pencarian berdasarkan tepian. Hasilnya, permintaan saya berubah menjadi sebagai berikut:
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, injak tanpa kembali sampai Anda menemukan ZoneStep dengan ID=19, hitung jumlah rantai tersebut.
Saya juga menyederhanakan skrip pemuatan yang diberikan di atas agar tidak membuat koneksi yang tidak perlu, membatasi diri pada atribut.
Permintaan tersebut masih membutuhkan waktu beberapa detik untuk diselesaikan, yang sama sekali tidak dapat diterima untuk tugas kami, karena permintaan tersebut sama sekali tidak sesuai untuk tujuan permintaan AdHoc dalam bentuk apa pun.
Saya mencoba menerapkan JanusGraph menggunakan Scylla sebagai implementasi Cassandra tercepat, tetapi ini juga tidak menghasilkan perubahan kinerja yang signifikan.
Jadi meskipun "terlihat seperti grafik", saya tidak bisa membuat DBMS grafik memprosesnya dengan cepat. Saya berasumsi sepenuhnya bahwa ada sesuatu yang tidak saya ketahui dan JanusGraph dapat dibuat untuk melakukan pencarian ini dalam sepersekian detik, namun saya tidak dapat melakukannya.
Karena masalahnya masih perlu diselesaikan, saya mulai memikirkan tentang GABUNG dan Pivot tabel, yang tidak menginspirasi optimisme dalam hal keanggunan, namun bisa menjadi pilihan yang sepenuhnya bisa diterapkan dalam praktiknya.
Proyek kami sudah menggunakan Apache ClickHouse, jadi saya memutuskan untuk menguji penelitian saya pada DBMS analitis ini.
ClickHouse dikerahkan 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-server
Saya membuat database dan tabel di dalamnya seperti ini:
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
Saya 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 datang dalam jumlah banyak, pengisian menjadi jauh lebih cepat dibandingkan JanusGraph.
Membuat dua kueri menggunakan GABUNG. 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.when
Untuk 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.when
Permintaannya tentu saja terlihat cukup menakutkan, untuk penggunaan nyata, Anda perlu membuat perangkat lunak generator harness. Namun, mereka bekerja dan bekerja dengan cepat. Permintaan pertama dan kedua diselesaikan dalam waktu kurang dari 0.1 detik. Berikut adalah contoh waktu eksekusi query untuk count(*) yang melewati 3 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 IOPS. Saat melakukan populasi data, JanusGraph menghasilkan jumlah IOPS yang cukup tinggi (1000-1300 untuk empat thread populasi data) dan IOWAIT yang cukup tinggi. Pada saat yang sama, ClickHouse menghasilkan beban minimal pada subsistem disk.
Kesimpulan
Kami memutuskan untuk menggunakan ClickHouse untuk melayani permintaan jenis ini. Kami selalu dapat lebih mengoptimalkan kueri menggunakan tampilan material dan paralelisasi dengan melakukan pra-pemrosesan aliran peristiwa menggunakan Apache Flink sebelum memuatnya ke ClickHouse.
Performanya sangat bagus sehingga kami mungkin tidak perlu memikirkan tentang memutar tabel secara terprogram. Sebelumnya, kami harus melakukan pivot data yang diambil dari Vertica melalui upload ke Apache Parket.
Sayangnya, upaya lain untuk menggunakan DBMS grafik tidak berhasil. Menurut saya JanusGraph tidak memiliki ekosistem ramah yang memudahkan untuk mendapatkan informasi terbaru tentang produk. Pada saat yang sama, untuk mengkonfigurasi server, digunakan cara tradisional Java, yang akan membuat orang yang tidak terbiasa dengan Java menangis darah:
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 berhasil secara tidak sengaja "meletakkan" JanusGraph versi BerkeleyDB.
Dokumentasinya cukup buruk dalam hal indeks, karena mengelola indeks mengharuskan Anda melakukan perdukunan yang agak aneh di Groovy. Misalnya, membuat indeks harus dilakukan dengan menulis 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
Bisa dibilang, eksperimen di atas adalah perbandingan antara hangat dan lembut. Jika Anda memikirkannya, DBMS grafik melakukan operasi lain untuk mendapatkan hasil yang sama. Namun, sebagai bagian dari pengujian, saya juga melakukan eksperimen dengan permintaan seperti:
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 pada data seperti itu, grafik DBMS menunjukkan hasil yang melampaui beberapa detik... Hal ini, tentu saja, disebabkan oleh fakta bahwa terdapat jalur seperti 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 bisa mendapatkan respons produktif dengan waktu pemrosesan kurang dari satu detik.
Pesan moral dari cerita ini adalah bahwa ide yang indah dan pemodelan paradigmatik tidak memberikan hasil yang diinginkan, yang ditunjukkan dengan efisiensi yang jauh lebih tinggi menggunakan contoh ClickHouse. Kasus penggunaan yang disajikan dalam artikel ini merupakan anti-pola yang jelas untuk DBMS grafik, meskipun tampaknya cocok untuk pemodelan dalam paradigmanya.
Sumber: www.habr.com