
Harga dari Penulisan Murah: Bagaimana RocksDB membaca?
Daftar Isi
- Bagaimana RocksDB membaca sebuah key?
- Operasi baca apa saja yang ditawarkan RocksDB?
- Get
- MultiGet
- Iterator
- Snapshot
- Apa itu bloom filter?
- Bagaimana dengan prefix seek RocksDB?
- Apa itu block cache RocksDB?
- Berapa biaya tombstone dan merge chain bagi operasi baca?
- Segitiga Amplifikasi
- Panduan Menelusuri Read Path
- Bagaimana Solana membebani read path?
- Kesimpulan
Membaca dari penyimpanan key-value terdengar mudah: berikan key dan dapatkan value. Namun, artikel sebelumnya dalam seri ini menjelaskan mengapa pemikiran tersebut keliru. LSM tree membuat kompromi agar penulisan menjadi murah dengan menunda pekerjaan pengorganisasian data.
Operasi baca harus menanggung biayanya.
Value terkini dari sebuah key mungkin berada di memtable, file pada level 0, atau salah satu level terurut di bawahnya. Tugas read path adalah menemukan versi terbaru dari value ini tanpa memeriksa semuanya.
Artikel ini membahas mekanisme yang menjaga pencarian tersebut tetap cepat, serta perhitungan yang menentukan biayanya.
Bagaimana RocksDB membaca sebuah key?
Operasi baca di RocksDB berjalan dari data terbaru ke data terlama, lalu berhenti pada versi pertama key yang ditemukan. Sesederhana itu; semua hal lainnya membuat proses ini lebih cepat.
Urutan pencariannya tetap dan mencerminkan write path yang dijelaskan dalam artikel sebelumnya, tetapi dalam arah sebaliknya: active memtable, immutable memtable, file SST di level 0, lalu file SST terurut mulai dari level 1 ke bawah.
RocksDB pertama-tama memeriksa active memtable karena struktur ini menyimpan penulisan terbaru, berada di memori, dan menggunakan skiplist. Karena itu, lookup merupakan pencarian terurut dalam memori yang hampir tidak memerlukan I/O. Pencarian langsung berhenti begitu key ditemukan.
Setelah active memtable, RocksDB memeriksa setiap immutable memtable yang sudah penuh tetapi belum di-flush ke disk. Pemeriksaan dilakukan dari yang terbaru hingga terlama.
Berikutnya, disk diperiksa. Setiap file level 0 harus diperiksa satu per satu, dari yang terbaru hingga terlama, karena rentang key-nya saling tumpang tindih. Ini terjadi karena setiap file merupakan snapshot dari apa pun yang kebetulan diserap memtable, sehingga key yang sama dapat berada dalam dua file level 0 sekaligus.
Setiap file level 0 menjadi tempat terpisah yang mungkin menyembunyikan sebuah key. Inilah alasan jumlah file level 0 membebani operasi baca dan RocksDB menghentikan sementara penulisan agar jumlahnya tetap terbatas.
Mulai dari level 1 ke bawah, file SST dalam level tertentu terurut dan tidak saling tumpang tindih, sehingga maksimal hanya satu file per level yang dapat memuat key tersebut. File itu ditemukan melalui binary search pada metadata file, lalu pencarian masuk ke dalam file tersebut. Index block menemukan data block yang tepat, dan data block tersebut akan memuat key atau menunjukkan bahwa level ini tidak memilikinya.
Sequence number penting bagi read path RocksDB karena memungkinkan pencarian berhenti pada versi pertama yang ditemukan. Ini menjamin bahwa versi tersebut adalah yang terbaru. Jika operasi baca menemukan tombstone, misalnya, operasi itu berhasil dan mengembalikan informasi bahwa key telah dihapus. Dengan demikian, pencarian berakhir di tombstone dan membiarkan value yang lebih lama tetap berada di disk.
Operasi baca apa saja yang ditawarkan RocksDB?
RocksDB menyediakan empat cara untuk membaca, dan masing-masing menggunakan read path secara berbeda: Get, MultiGet, Iterators, dan Snapshots.
Get
Get adalah point lookup yang kita telusuri pada bagian sebelumnya. Operasi ini mencari satu key dari yang terbaru hingga terlama, dan versi pertama yang ditemukan akan digunakan.
MultiGet
MultiGet mengelompokkan point lookup ke dalam batch. Operasi baca dapat memberikan sekumpulan key kepada RocksDB, lalu RocksDB mengurutkan dan mengelompokkannya berdasarkan file yang mungkin diakses.
Pada praktiknya, ini membantu RocksDB membuka satu SST, memeriksa index block dan filter block-nya satu kali, serta mengelompokkan I/O ke dalam batch. Read path-nya sama seperti Get, tetapi biayanya tetap dan diamortisasi ke seluruh batch.
Iterator
Iterators memindai rentang, yang berarti iterator mencari sebuah key lalu bergerak maju atau mundur dalam urutan terurut.
Pada dasarnya, iterator adalah k-way merge (yaitu menggabungkan k daftar terurut yang berbeda menjadi satu urutan yang sepenuhnya terurut). Setiap sumber data aktif—memtable, setiap immutable memtable, setiap file L0, dan satu file per level yang lebih dalam—menyumbangkan cursor terurutnya sendiri, lalu sebuah heap berulang kali menghasilkan cursor yang memegang key terkecil berikutnya.
Aturan yang memprioritaskan versi terbaru ini berlaku pada setiap langkah. Jadi, jika beberapa sumber menawarkan key yang sama, versi terbaru digunakan dan sisanya dibuang.
Value yang tertutupi dan tombstone dilewati saat iterator bergerak maju. Artinya, rentang yang penuh dengan key terhapus harus dilalui iterator secara diam-diam tanpa mengembalikan apa pun. Keyspace dengan banyak penghapusan membuat pemindaian lambat dengan cara yang tidak pernah terlihat oleh point lookup.
Snapshot
Snapshot adalah sequence number yang diambil pada suatu waktu. Setiap operasi baca yang membawanya cukup mengabaikan versi yang lebih baru daripada nomor tersebut dan mengembalikan versi terbaru yang sama dengan atau lebih rendah darinya. Ini memungkinkan pembacaan konsisten pada titik waktu tertentu, yang diimplementasikan sebagai perbandingan bilangan bulat.
Mengambil snapshot pada dasarnya gratis karena biayanya baru terakumulasi kemudian; snapshot yang masih aktif tetap dapat melihat versi yang tidak bisa direklamasi, berapa pun usianya.
Baik Get maupun MultiGet mengetahui persis key yang dicari. Artinya, pencarian dapat mengajukan pertanyaan yang lebih murah daripada “di mana key ini?” Pertanyaannya adalah, “file mana yang sudah pasti tidak memilikinya?” Iterators tidak pernah mendapatkan jalan pintas tersebut. Menjawab pertanyaan yang lebih murah ini adalah seluruh tugas bloom filter.
Apa itu bloom filter?
Bloom filter menjawab satu pertanyaan tentang sebuah kumpulan: apakah elemen ini sudah pasti tidak ada di dalamnya? Jawabannya adalah pasti tidak ada atau mungkin ada, tetapi tidak pernah ya dengan kepastian penuh. File yang sudah pasti tidak memiliki sebuah key tidak akan pernah dibuka oleh pencarian, sehingga menghemat I/O.
Saat SST dibuat, setiap key diproses melalui beberapa fungsi hash, dan setiap hash menetapkan sebuah bit dalam compact bit array (filter block).
Pada saat pembacaan, lookup melakukan hash pada key yang dicari dengan cara yang sama lalu memeriksa bit-bit tersebut. Jika ada bit yang tidak ditetapkan, key itu tidak pernah dimasukkan, yang berarti key tersebut sudah pasti tidak berada di sini.
Jika semua bit ditetapkan, key tersebut mungkin ada—disebut mungkin karena hash dari key lain dapat saja kebetulan mencakup bit yang sama.
Artinya, false positive dapat terjadi—pencarian membuka file, memeriksanya, lalu tidak menemukan apa pun setelah menghabiskan I/O yang seharusnya dihemat oleh filter.
Dengan pengaturan konvensional RocksDB sebesar sepuluh bit per key, tingkat false positive kira-kira satu persen. Sepuluh bit memberikan peluang 99% untuk melewati file yang tidak memiliki key tersebut. Filter untuk SST dengan satu juta key berukuran sedikit lebih dari satu megabyte, berada di memori bersama index, dan diperiksa tanpa menyentuh disk yang dilindunginya. Hanya sedikit struktur dalam rekayasa sistem yang memberikan manfaat lebih besar dengan biaya serendah ini.
Ini juga menjelaskan mengapa artikel sebelumnya dalam seri ini menemukan ukuran filter block sebesar nol. Bloom filter bersifat opsional, dikonfigurasi melalui filter_policy dengan jumlah bit per key yang dipilih aplikasi, dan secara default tidak ada yang ditetapkan.
Default ini dapat dipertahankan untuk library yang tidak dapat mengetahui workload-nya, tetapi tidak dapat dibenarkan di lingkungan produksi. Di sana, filter menjadi pembeda antara key yang tidak ditemukan dengan biaya satu pemeriksaan memori per file dan biaya pembacaan index block per file.
Salah satu keterbatasannya adalah bloom filter hanya membantu ketika pencarian mengetahui key yang tepat. Bloom filter dapat menjawab apakah key:0301 ada dalam file ini, tetapi tidak dapat menjawab apakah file ini memuat sesuatu di antara key:0090 dan key:0110 karena hash menghilangkan urutan.
Iterators, yang tidak pernah mengetahui key berikutnya terlebih dahulu, tidak mendapatkan bantuan dari struktur ini. Bloom filter adalah teknologi point lookup; workload yang didominasi rentang harus mencari solusi lain.
Bagaimana dengan prefix seek RocksDB?
Solusi parsial RocksDB untuk masalah pemindaian adalah mengubah apa yang di-hash. Extractor prefix memberi tahu RocksDB bahwa key memiliki struktur yang bermakna (misalnya, delapan byte pertama atau semua yang berada sebelum separator). Setelah RocksDB mengetahui apa itu prefix, RocksDB dapat membuat bloom filter untuk prefix, sebagai pengganti atau pendamping key utuh. Filter yang menjawab apakah sebuah file memuat key yang diawali prefix tertentu dapat membantu iterator.
Namun, jalan pintas ini hanya berfungsi dengan satu prefix.
Pemindaian untuk prefix tertentu dapat melewati file dan block dengan cara yang sama seperti point lookup. Pemindaian yang berpindah dari satu prefix ke prefix berikutnya tidak mendapatkan bantuan yang sama. Cara mempertahankan jaminan urutan iterasi antar-prefix memerlukan pembahasan tersendiri tentang detail konfigurasi yang memungkinkannya.
Prefix seek tidak membuat range scan menjadi murah secara umum.
Sebaliknya, prefix seek membuat satu bentuk range scan tertentu yang sangat umum menjadi murah untuk prefix tertentu.
Apa itu block cache RocksDB?
Block cache adalah cache block yang tidak dikompresi di dalam memori. Saat data block dibaca dari SST, block tersebut didekompresi dan disimpan, sehingga operasi baca berikutnya yang memerlukan block yang sama dapat menemukannya tanpa menyentuh disk. Artinya, hot key mengelompok dalam hot block, dan hot block tidak lagi memerlukan biaya I/O.
Jadi, bloom filter berusaha mencegah pencarian memasuki file, sedangkan block cache membuat file yang harus dimasuki menjadi murah untuk dikunjungi kembali.
Ada satu detail penting: sebenarnya terdapat dua cache.
Block cache adalah cache milik RocksDB sendiri, yang menyimpan block tidak terkompresi dan dikelola secara eksplisit. Di bawahnya terdapat page cache sistem operasi, yang menyimpan halaman file mentah dan terkompresi yang baru-baru ini dibaca RocksDB. Data yang sama dapat menempati keduanya sekaligus (yaitu tidak terkompresi di block cache dan terkompresi di page cache). Inilah alasan jejak memori RocksDB yang sebenarnya selalu diremehkan, dan diskusi tentang ukuran cache yang mengabaikan page cache hanya mempertimbangkan separuh sistem.
Index block dan filter block mendapatkan perlakuan khusus. Keduanya berukuran kecil, diperiksa pada hampir setiap operasi baca yang mencapai file terkait, dan kegagalan menemukannya di cache menimbulkan biaya yang sangat besar. Karena itu, menjaga keduanya tetap berada di memori hampir tidak dapat ditawar.
Apakah keduanya berada di dalam anggaran block cache atau di luarnya, dalam memori milik table reader, merupakan pilihan konfigurasi (yaitu cache_index_and_filter_blocks).
Berapa biaya tombstone dan merge chain bagi operasi baca?
Tombstone dan merge chain adalah jenis record dari write path yang memengaruhi read path, tetapi hanya dapat dibereskan melalui compaction. Tombstone terus terakumulasi hingga compaction membuangnya beserta bangkai yang ditutupinya, sementara merge chain terus bertambah hingga compaction menggabungkannya menjadi value final.
Tombstone murah bagi point lookup yang menemukannya. Pencarian berhenti dan memberikan jawaban karena “dihapus” adalah hasil yang valid. Namun, iterator tidak dapat berhenti pada tombstone. Sebaliknya, iterator harus menyelesaikan tombstone tersebut, melewati versi yang ditutupinya, dan terus bergerak hingga menemukan live key berikutnya. Keyspace dengan banyak penghapusan menjadi ladang ranjau berisi entri yang harus dilalui dan dibuang tanpa mengembalikan apa pun; pemindaian di atasnya dapat melakukan banyak pekerjaan tak menentu untuk menghasilkan sedikit output. Point lookup tidak pernah menyadarinya, sedangkan range scan tidak memiliki pertahanan terhadapnya.
Merge operand menimbulkan biaya serupa, tetapi dalam arah sebaliknya. Operasi baca terhadap merged key tidak dapat berhenti pada record terbaru karena operand adalah deskripsi perubahan, bukan value. Jadi, pencarian harus terus mengumpulkan operand hingga mencapai full value, tombstone, atau dasar tree, lalu menggabungkan chain tersebut melalui merge operator.
Panduan dalam artikel sebelumnya menggabungkan chain sepanjang tiga, tetapi perhatikan bahwa tidak ada bagian desain yang membatasi chain hingga tiga. Counter yang di-merge sepuluh ribu kali di antara compaction menghasilkan operasi baca yang mengumpulkan dan menggabungkan sepuluh ribu operand, dan write path tidak melakukan apa pun untuk mencegahnya.
Segitiga Amplifikasi
Segitiga amplifikasi adalah kompromi tiga arah yang mengatur setiap LSM tree: write amplification, read amplification, dan space amplification. Memperbaiki dua di antaranya akan memperburuk yang ketiga.
Athanassoulis et al. memformalkan kompromi ini sebagai konjektur RUM (2016), yang menyatakan bahwa sebuah metode akses hanya dapat meminimalkan maksimal dua dari overhead pembacaan, overhead pembaruan, dan overhead memori atau ruang, sementara yang ketiga menanggung biayanya. B-tree berada pada satu posisi (yaitu pembacaan murah, pembaruan mahal), sedangkan LSM tree berada pada posisi lain (yaitu pembaruan murah, pembacaan mahal), dan segitiga ini memetakan area di antaranya.
Semua hal dalam artikel ini merupakan upaya salah satu sudut segitiga untuk mempertahankan dirinya.
Write amplification adalah rasio antara jumlah byte yang secara fisik ditulis ke disk dan jumlah byte yang secara logis ditulis aplikasi. Satu Put ditulis satu kali ke WAL, satu kali saat di-flush ke L0, dan setiap kali compaction membawanya turun satu level. Artinya, satu penulisan logis dapat secara rutin menjadi sepuluh hingga tiga puluh penulisan fisik selama masa pakainya dalam LSM berlevel dengan belasan level. Walaupun artikel sebelumnya merayakan fakta bahwa semua penulisan tersebut bersifat sekuensial, artikel ini meratapinya.
Read amplification mengacu pada jumlah tempat yang harus diperiksa query untuk menemukan jawabannya. Untuk point lookup, skenario terburuknya adalah setiap memtable, file L0, dan satu file per level yang lebih dalam (yaitu pencarian yang ditelusuri di awal artikel ini), sedangkan biaya aktualnya berasal dari apa pun yang lolos dari bloom filter dan block cache. Untuk range scan, biayanya adalah k-way merge yang membuka semua sumber sekaligus. Read amplification merupakan sebagian besar topik artikel ini: filter, cache, dan prefix extractor menjadi mekanisme untuk menguranginya.
Space amplification adalah rasio antara ruang disk yang digunakan dan data logis yang disimpan. Versi lama yang menunggu compaction, tombstone dan bangkai yang ditutupinya, merge chain yang belum digabungkan, serta setiap versi yang dipertahankan oleh snapshot aktif, semuanya menghabiskan ruang disk untuk data yang tidak lagi dapat dibaca atau tidak lagi diinginkan aplikasi. Leveled compaction yang dikonfigurasi dengan baik mempertahankan rasio ini di sekitar 1,1x, sedangkan pengaturan lain mengorbankan jauh lebih banyak ruang.
Segitiga ini menyatakan bahwa memperbaiki dua sudut akan memperburuk sudut ketiga. Artinya,
- Lakukan compaction secara agresif: pembacaan menjadi lebih cepat dan penggunaan ruang lebih rapat, tetapi setiap key terus-menerus ditulis ulang dan write amplification meningkat
- Lakukan compaction secara longgar: penulisan berjalan mulus, tetapi file menumpuk untuk diperiksa saat pembacaan dan data mati menetap di disk
- Gunakan memori untuk filter: cache dan pembacaan membaik, tetapi memori adalah sumber daya keempat yang diam-diam diperebutkan semua sudut segitiga
Tidak ada pengaturan berlabel “cepat”. Hanya ada posisi pada segitiga yang sesuai, atau tidak sesuai, dengan workload tertentu.
Panduan Menelusuri Read Path
Bagian berikut menggunakan kembali proyek dari panduan artikel sebelumnya, dengan statistik yang diaktifkan.
Ganti isi src/main.rs dengan:
use rocksdb::{BlockBasedOptions, Options, DB};
fn main() {
let with_bloom = std::env::args().nth(1).as_deref() == Some("bloom");
let path = if with_bloom { "/tmp/read-bloom" } else { "/tmp/read-nobloom" };
let mut opts = Options::default();
opts.create_if_missing(true);
opts.enable_statistics();
if with_bloom {
let mut bbt = BlockBasedOptions::default();
bbt.set_bloom_filter(10.0, false);
opts.set_block_based_table_factory(&bbt);
}
let db = DB::open(&opts, path).unwrap();
// Three L0 SSTs with fully overlapping ranges: file b holds keys where
// key % 4 == b, all spanning 0..600. Keys where key % 4 == 3 exist nowhere
for batch in 0..3u64 {
let mut k = batch;
while k < 600 {
let key = format!("key:{:04}", k);
db.put(key.as_bytes(), b"value").unwrap();
k += 4;
}
db.flush().unwrap();
}
// key:0404 lives only in the oldest file, meaning the search must get past
// the two newer files to reach it
let hit = db.get(b"key:0404").unwrap();
println!("get(key:0404) = {:?}", hit.map(|b| String::from_utf8(b).unwrap()));
// key:0303 is inside every file's range and inside no file
let miss = db.get(b"key:0303").unwrap();
println!("get(key:0303) = {:?}", miss);
if let Some(stats) = db.property_value("rocksdb.options-statistics").unwrap() {
for line in stats.lines() {
if line.contains("bloom.filter.useful")
|| line.contains("bloom.filter.full.positive")
|| line.contains("block.cache.data") {
println!("{}", line);
}
}
}
}Di atas, kita memiliki tiga file L0 yang sudah di-flush dengan rentang key yang sepenuhnya tumpang tindih. Jadi, tidak ada file yang dapat dikecualikan hanya berdasarkan metadata rentang karena setiap file mungkin menyembunyikan kedua key tersebut.
Hit (yaitu key:0404) hanya berada di file tertua, sehingga pencarian dari yang terbaru harus menangani dua file yang tidak memilikinya sebelum mencapai file yang memilikinya.
Miss (yaitu key:0303) berada di dalam rentang setiap file tetapi tidak berada dalam file mana pun. Artinya, hanya bloom filter yang dapat menghemat operasi baca tersebut.
Jalankan apa adanya, lalu jalankan dengan argumen bloom.
Tanpa filter, output-nya adalah:
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 3
rocksdb.block.cache.data.hit COUNT : 3
rocksdb.bloom.filter.useful COUNT : 0Kita memerlukan enam akses data block untuk menjawab dua lookup. Hit membaca satu data block dari masing-masing dari tiga file, dan dua di antaranya tidak membantu, sehingga menghabiskan tiga pembacaan disk. Miss kemudian memeriksa tiga file yang sama, dan ketiga akses data block-nya menjadi cache hit hanya karena lookup pertama baru saja memanaskan cache. Block cache menjalankan tugasnya, dan hanya block cache yang bekerja.
Dengan filter, output-nya adalah:
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 1
rocksdb.block.cache.data.hit COUNT : 0
rocksdb.bloom.filter.useful COUNT : 5
rocksdb.bloom.filter.full.positive COUNT : 1Secara total, kita hanya memiliki satu akses data block. Kita dapat memverifikasi secara manual perhitungan useful yang menghasilkan 5:
- Hit memeriksa filter file terbaru dan filter file kedua, lalu keduanya mengembalikan tidak. Filter file tertua mengembalikan satu
full.positive, yang terbukti merupakan true positive. Ini menghasilkan dua file yang dilewati - Miss memeriksa ketiga filter dan mendapatkan tiga jawaban tidak. Artinya, ada tiga file yang dilewati dan tidak ada file yang dibuka
Jika hit dan miss dijumlahkan, kita mendapatkan lima pemeriksaan file. Lima pemeriksaan file yang dijawab filter dari memori adalah lima pembacaan data block yang harus dilakukan saat dijalankan tanpa filter. Jadi, sepuluh bit per key mengubah enam akses block menjadi satu.
Pada dasarnya, ini adalah segitiga amplifikasi dalam skala lebih kecil. Proses tanpa filter membayar penuh biaya read amplification, proses dengan filter membayarnya menggunakan memori (yaitu beberapa ratus byte filter per file), dan write path tidak membayar apa pun dalam kedua kasus. Setiap keputusan tuning di RocksDB adalah kompromi ini dalam skala yang lebih besar.
Bagaimana Solana membebani read path?
Artikel sebelumnya menggambarkan validator sebagai stress test untuk write path. Shred terus masuk dengan kecepatan penuh tanpa henti. Sisi baca adalah separuh lainnya dari kompromi tersebut karena ledger yang tidak dapat dibaca kembali hanyalah cara mahal untuk menghangatkan pusat data.
Setiap pernyataan dalam artikel ini tentang urutan pencarian, filter, dan desain key terlihat dalam cara data Solana benar-benar di-query.
Di dalam validator, operasi baca Blockstore memiliki dua bentuk: melayani repair request serta memutar ulang atau melakukan streaming sebuah block.
Melayani repair request berarti validator lain meminta shred yang terlewat. Ini merupakan point lookup berdasarkan (slot, index). Memutar ulang atau melakukan streaming ledger senilai satu block merupakan range scan pada slot tertentu.
Karena shred key diawali nomor slot, setiap shred dari sebuah slot tersimpan berurutan di disk. Jadi, “berikan slot n” menjadi pergerakan iterator yang rapat dan sekuensial, bukan kumpulan point read yang tersebar.
Desain key dari write path menjadi optimasi read path. Layout yang mengutamakan slot, yang membuat proses flush menghasilkan file L0 dengan sedikit tumpang tindih, juga membuat pembacaan block menjadi satu sapuan berurutan.
Lapisan RPC mengajukan pertanyaan yang lebih sulit. getTransaction adalah point lookup berdasarkan signature ke data status transaction. Inilah kasus yang menjadi alasan bloom filter dibuat, karena signature yang tidak ada dalam SST tertentu semestinya cukup dieliminasi dengan satu pemeriksaan memori, bukan pembacaan index.
getSignaturesForAddress adalah pemindaian berbentuk prefix yang dibahas sebelumnya dalam artikel ini—index address-to-signature diawali dengan address, sehingga seluruh riwayat satu account menempati satu rentang key yang berurutan, dan seluruh aktivitas sebuah address menjadi bentuk range scan yang dapat ditangani struktur ini dengan baik.
Namun, pertanyaan yang benar-benar diajukan aplikasi berada di atas keduanya. Tidak ada primitif untuk “Berikan semua transaction untuk address ini.” Pertanyaan semacam ini menjadi prefix scan untuk signature, lalu satu point lookup per signature untuk transaction.
Pada praktiknya, satu range query menyebar menjadi ratusan atau ribuan Get. Inilah workload yang menjadi alasan batching MultiGet tersedia. Signature diurutkan, dikelompokkan berdasarkan file yang diakses, lalu index dan filter setiap file diperiksa satu kali.
Query yang dari luar terlihat seperti satu panggilan API sebenarnya memicu setiap mekanisme yang dijelaskan artikel ini secara berurutan di read path: iterator, pengurutan prefix, bloom filter, amortisasi batch, dan block cache.
Kami membuat panggilan API tersebut.
Sebelumnya, developer harus menjalankan loop di sisi client yang terdiri dari getSignaturesForAddress dan n panggilan getTransaction, beserta logika retry yang selalu dibutuhkan pola n+1.
getTransactionsForAddress menyatukan loop tersebut menjadi satu method dengan berbagai fitur yang sesuai dengan mekanisme read path yang dibahas dalam artikel ini.
Urutan ascending dan descending adalah dua arah iterator. Pagination berbasis cursor mengembalikan token dalam bentuk slot:position, yang merupakan posisi key. Jadi, melanjutkan query dengan pagination berarti melakukan seek ulang untuk memulai pemindaian dari posisi terakhir. Filter slot dan waktu membatasi pekerjaan sejak awal, bukan membuang hasil setelahnya.
Sistem archival yang melayani method tersebut menyimpan seluruh riwayat transaction Solana dalam RocksDB—ratusan terabyte. Kami memigrasikannya dari ClickHouse ke RocksDB, dan sistem inilah alasan seri ini dibuat: storage engine yang dijelaskan dalam artikel-artikel ini adalah sistem yang kami tuning, column family demi column family, agar seluruh masa aktif sebuah account menjadi satu pemindaian berurutan dan lookup signature hanya memerlukan satu pemeriksaan memori. Inilah alasan kami memiliki sistem archival dengan performa terbaik di seluruh Solana.
Kesimpulan
Read path adalah jadwal pembayaran kembali LSM tree, dan artikel ini menyajikannya sebagai tagihan terperinci. Sebuah lookup berjalan dari yang terbaru hingga terlama melalui memtable, L0, dan semua level terurut, lalu berhenti pada versi pertama yang ditemukan. Setiap fitur dan alat lainnya tersedia untuk meminimalkan jumlah akses yang diperlukan pencarian: bloom filter menjawab dari memori apakah sebuah key sudah pasti tidak ada dalam file, sehingga file tetap tertutup; block cache membuat file yang harus dibuka menjadi murah untuk dibuka kembali; dan desain prefix mengubah satu bentuk range scan yang terjangkau menjadi bentuk yang benar-benar dibutuhkan aplikasi.
Segitiga amplifikasi adalah buku besar yang mencatat semuanya: biaya pembacaan dibayar dengan memori, biaya penulisan dibayar lebih awal dalam cicilan sekuensial. Sementara itu, biaya ruang diam-diam terakumulasi dalam setiap tombstone dan versi tertutupi yang dilewati pencarian.
Perhatikan bahwa setiap struktur dalam artikel ini hanya mengelola biaya dari file yang terakumulasi, versi mati, dan merge chain yang belum digabungkan. Tidak ada hal yang dibahas dalam artikel ini yang benar-benar menghapusnya. Mekanisme yang menghapusnya terus berjalan di latar belakang, dan menentukan seberapa agresif mekanisme tersebut berjalan adalah keputusan tuning yang paling berdampak di RocksDB. Artikel berikutnya dalam seri ini membahas compaction: apa fungsinya, tiga gaya yang ditawarkan RocksDB, serta alasan Blockstore Solana pernah mengganti gayanya lalu kembali ke gaya sebelumnya.
Jika menghabiskan sore Anda dengan membaca panduan tentang read path database terdengar menyenangkan, berkaryalah bersama kami. Read path yang dijelaskan di sini adalah sistem yang kami operasikan untuk menangani seluruh riwayat Solana, tempat perbedaan antara memeriksa bloom filter dan membuka file diukur dalam hardware dan biaya nyata. Kami sedang merekrut untuk berbagai posisi di tim engineering. Lihat semua posisi yang tersedia di helius.dev/careers
Artikel Terkait
Berlangganan Helius
Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan


