Skip to main content

typed_store/rocks/
options.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::BTreeMap, env};
6
7use iota_macros::nondeterministic;
8use rocksdb::{BlockBasedOptions, Cache, ReadOptions};
9use sysinfo::{MemoryRefreshKind, RefreshKind, System};
10use tap::TapFallible;
11use tracing::{debug, info, warn};
12
13// Write buffer size per RocksDB instance can be set via the env var below.
14// If the env var is not set, use the default value in MiB.
15const ENV_VAR_DB_WRITE_BUFFER_SIZE: &str = "DB_WRITE_BUFFER_SIZE_MB";
16const DEFAULT_DB_WRITE_BUFFER_SIZE: usize = 1024;
17
18// Write ahead log size per RocksDB instance can be set via the env var below.
19// If the env var is not set, use the default value in MiB.
20const ENV_VAR_DB_WAL_SIZE: &str = "DB_WAL_SIZE_MB";
21const DEFAULT_DB_WAL_SIZE: usize = 1024;
22
23// Environment variable to control behavior of write throughput optimized
24// tables.
25const ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER: &str = "L0_NUM_FILES_COMPACTION_TRIGGER";
26const DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 4;
27const DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 80;
28const ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB: &str = "MAX_WRITE_BUFFER_SIZE_MB";
29const DEFAULT_MAX_WRITE_BUFFER_SIZE_MB: usize = 256;
30const ENV_VAR_MAX_WRITE_BUFFER_NUMBER: &str = "MAX_WRITE_BUFFER_NUMBER";
31const DEFAULT_MAX_WRITE_BUFFER_NUMBER: usize = 6;
32const ENV_VAR_TARGET_FILE_SIZE_BASE_MB: &str = "TARGET_FILE_SIZE_BASE_MB";
33const DEFAULT_TARGET_FILE_SIZE_BASE_MB: usize = 128;
34
35// Set to 1 to disable blob storage for transactions and effects.
36const ENV_VAR_DISABLE_BLOB_STORAGE: &str = "DISABLE_BLOB_STORAGE";
37const ENV_VAR_DB_PARALLELISM: &str = "DB_PARALLELISM";
38
39#[derive(Clone, Debug, Default)]
40pub struct ReadWriteOptions {
41    /// When set, debug log the hash of the key and value bytes when inserting
42    /// to this table.
43    pub log_value_hash: bool,
44}
45
46impl ReadWriteOptions {
47    pub fn readopts(&self) -> ReadOptions {
48        ReadOptions::default()
49    }
50
51    pub fn set_log_value_hash(mut self, log_value_hash: bool) -> Self {
52        self.log_value_hash = log_value_hash;
53        self
54    }
55}
56
57#[derive(Default, Clone)]
58pub struct DBOptions {
59    pub options: rocksdb::Options,
60    pub rw_options: ReadWriteOptions,
61}
62
63/// Write options tuned for one-shot bulk ingestion into a freshly created
64/// store.
65pub fn bulk_ingestion_write_options() -> rocksdb::WriteOptions {
66    let mut opts = rocksdb::WriteOptions::default();
67    opts.disable_wal(true);
68    opts
69}
70
71/// RocksDB options tuned for one-shot bulk ingestion into a freshly created
72/// store, e.g. initial index building, inserting the genesis snapshot, or
73/// restoring a formal snapshot.
74pub struct BulkIngestionOptions {
75    pub db_options: rocksdb::Options,
76    pub column_family_options: DBOptions,
77    pub batch_size_limit: usize,
78}
79
80pub fn bulk_ingestion_options() -> BulkIngestionOptions {
81    let total_memory_bytes = available_memory_bytes();
82    let num_cpus = num_cpus::get();
83
84    let mut db_options = rocksdb::Options::default();
85
86    // `unordered_write` gives a large speedup for bulk writes; relaxed write
87    // ordering is acceptable because the store is rebuilt from scratch on
88    // failure.
89    db_options.set_unordered_write(true);
90
91    // Allow CPU-intensive flushing to use all cores.
92    db_options.set_max_background_jobs(num_cpus as i32);
93
94    // Upper bound on memtable memory across all column families: 80% of RAM.
95    // Large memtables give flushing threads enough buffer to keep up with the
96    // writers so the CPUs stay busy.
97    let db_write_buffer_size = (total_memory_bytes as f64 * 0.8) as usize;
98    db_options.set_db_write_buffer_size(db_write_buffer_size);
99
100    // Per-column-family options: Create options with compactions disabled and large
101    // write buffers. Each CF can use up to 25% of system RAM, but total is
102    // still limited by set_db_write_buffer_size configured above.
103    let mut column_family_options = default_db_options();
104    column_family_options
105        .options
106        .set_disable_auto_compactions(true);
107
108    // Disable the write backpressure that kicks in as L0 files build up: `-1`
109    // turns off the compaction and slowdown triggers, and the stop trigger is
110    // pushed out of reach. These are per-column-family options, so they must be
111    // set here rather than on `db_options` to reach the index CFs. Compaction
112    // itself is disabled above, so no L0 files are ever compacted away.
113    column_family_options
114        .options
115        .set_level_zero_file_num_compaction_trigger(-1);
116    column_family_options
117        .options
118        .set_level_zero_slowdown_writes_trigger(-1);
119    column_family_options
120        .options
121        .set_level_zero_stop_writes_trigger(i32::MAX);
122
123    let cf_memory_budget = (total_memory_bytes as f64 * 0.25) as usize;
124    const MIN_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MiB
125    // More CPUs means more parallel flushing capacity, so target more buffers,
126    // but never shrink a buffer below the minimum.
127    let target_buffer_count = num_cpus.max(2);
128    let buffer_size = (cf_memory_budget / target_buffer_count).max(MIN_BUFFER_SIZE);
129    let buffer_count = (cf_memory_budget / buffer_size).clamp(2, target_buffer_count) as i32;
130    column_family_options
131        .options
132        .set_write_buffer_size(buffer_size);
133    column_family_options
134        .options
135        .set_max_write_buffer_number(buffer_count);
136
137    // Calculate batch size limit: default to half the buffer size or 128MB,
138    // whichever is smaller
139    let batch_size_limit = (buffer_size / 2).min(1 << 27);
140
141    debug!(
142        total_memory_bytes,
143        num_cpus,
144        db_write_buffer_size,
145        buffer_size,
146        buffer_count,
147        batch_size_limit,
148        "configured bulk ingestion options"
149    );
150
151    BulkIngestionOptions {
152        db_options,
153        column_family_options,
154        batch_size_limit,
155    }
156}
157
158/// Available memory in bytes, honoring cgroup limits in containerized
159/// environments and falling back to total system memory.
160fn available_memory_bytes() -> u64 {
161    // Under the simulator sysinfo must run off the test thread, where the
162    // intercepted clock and rng would make its probes a source of
163    // non-determinism. No-op outside msim.
164    nondeterministic!({
165        // `RefreshKind::nothing().with_memory(..)` avoids collecting other,
166        // slower stats.
167        let mut sys = System::new_with_specifics(
168            RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
169        );
170        sys.refresh_memory();
171
172        if let Some(cgroup_limits) = sys.cgroup_limits() {
173            // `total_memory` is 0 when there is no cgroup limit.
174            if cgroup_limits.total_memory > 0 {
175                debug!(
176                    limit = cgroup_limits.total_memory,
177                    "using cgroup memory limit"
178                );
179                return cgroup_limits.total_memory;
180            }
181        }
182
183        let total = sys.total_memory();
184        debug!(total, "using total system memory");
185        total
186    })
187}
188
189#[derive(Clone)]
190pub struct DBMapTableConfigMap(BTreeMap<String, DBOptions>);
191impl DBMapTableConfigMap {
192    pub fn new(map: BTreeMap<String, DBOptions>) -> Self {
193        Self(map)
194    }
195
196    pub fn to_map(&self) -> BTreeMap<String, DBOptions> {
197        self.0.clone()
198    }
199}
200
201impl DBOptions {
202    // Optimize lookup perf for tables where no scans are performed.
203    // If non-trivial number of values can be > 512B in size, it is beneficial to
204    // also specify optimize_for_large_values_no_scan().
205    pub fn optimize_for_point_lookup(mut self, block_cache_size_mb: usize) -> DBOptions {
206        // NOTE: this overwrites the block options.
207        self.options
208            .optimize_for_point_lookup(block_cache_size_mb as u64);
209        self
210    }
211
212    // Optimize write and lookup perf for tables which are rarely scanned, and have
213    // large values. https://rocksdb.org/blog/2021/05/26/integrated-blob-db.html
214    pub fn optimize_for_large_values_no_scan(mut self, min_blob_size: u64) -> DBOptions {
215        if env::var(ENV_VAR_DISABLE_BLOB_STORAGE).is_ok() {
216            info!("Large value blob storage optimization is disabled via env var.");
217            return self;
218        }
219
220        // Blob settings.
221        self.options.set_enable_blob_files(true);
222        self.options
223            .set_blob_compression_type(rocksdb::DBCompressionType::Lz4);
224        self.options.set_enable_blob_gc(true);
225        // Since each blob can have non-trivial size overhead, and compression does not
226        // work across blobs, set a min blob size in bytes to so small
227        // transactions and effects are kept in sst files.
228        self.options.set_min_blob_size(min_blob_size);
229
230        // Increase write buffer size to 256MiB.
231        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
232            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
233            * 1024
234            * 1024;
235        self.options.set_write_buffer_size(write_buffer_size);
236        // Since large blobs are not in sst files, reduce the target file size and base
237        // level target size.
238        let target_file_size_base = 64 << 20;
239        self.options
240            .set_target_file_size_base(target_file_size_base);
241        // Level 1 default to 64MiB * 4 ~ 256MiB.
242        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
243            .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
244        self.options
245            .set_max_bytes_for_level_base(target_file_size_base * max_level_zero_file_num as u64);
246
247        self
248    }
249
250    // Optimize tables with a mix of lookup and scan workloads.
251    pub fn optimize_for_read(mut self, block_cache_size_mb: usize) -> DBOptions {
252        self.options
253            .set_block_based_table_factory(&get_block_options(block_cache_size_mb, 16 << 10));
254        self
255    }
256
257    // Optimize DB receiving significant insertions.
258    pub fn optimize_db_for_write_throughput(mut self, db_max_write_buffer_gb: u64) -> DBOptions {
259        self.options
260            .set_db_write_buffer_size(db_max_write_buffer_gb as usize * 1024 * 1024 * 1024);
261        self.options
262            .set_max_total_wal_size(db_max_write_buffer_gb * 1024 * 1024 * 1024);
263        self
264    }
265
266    // Optimize tables receiving significant insertions.
267    pub fn optimize_for_write_throughput(mut self) -> DBOptions {
268        // Increase write buffer size to 256MiB.
269        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
270            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
271            * 1024
272            * 1024;
273        self.options.set_write_buffer_size(write_buffer_size);
274        // Increase write buffers to keep to 6 before slowing down writes.
275        let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
276            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
277        self.options
278            .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
279        // Keep 1 write buffer so recent writes can be read from memory.
280        self.options
281            .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
282
283        // Increase compaction trigger for level 0 to 6.
284        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
285            .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
286        self.options.set_level_zero_file_num_compaction_trigger(
287            max_level_zero_file_num.try_into().unwrap(),
288        );
289        self.options.set_level_zero_slowdown_writes_trigger(
290            (max_level_zero_file_num * 12).try_into().unwrap(),
291        );
292        self.options
293            .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
294
295        // Increase sst file size to 128MiB.
296        self.options.set_target_file_size_base(
297            read_size_from_env(ENV_VAR_TARGET_FILE_SIZE_BASE_MB)
298                .unwrap_or(DEFAULT_TARGET_FILE_SIZE_BASE_MB) as u64
299                * 1024
300                * 1024,
301        );
302
303        // Increase level 1 target size to 256MiB * 6 ~ 1.5GiB.
304        self.options
305            .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
306
307        self
308    }
309
310    // Optimize tables receiving significant insertions, without any deletions.
311    // TODO: merge this function with optimize_for_write_throughput(), and use a
312    // flag to indicate if deletion is received.
313    pub fn optimize_for_write_throughput_no_deletion(mut self) -> DBOptions {
314        // Increase write buffer size to 256MiB.
315        let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
316            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
317            * 1024
318            * 1024;
319        self.options.set_write_buffer_size(write_buffer_size);
320        // Increase write buffers to keep to 6 before slowing down writes.
321        let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
322            .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
323        self.options
324            .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
325        // Keep 1 write buffer so recent writes can be read from memory.
326        self.options
327            .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
328
329        // Switch to universal compactions.
330        self.options
331            .set_compaction_style(rocksdb::DBCompactionStyle::Universal);
332        let mut compaction_options = rocksdb::UniversalCompactOptions::default();
333        compaction_options.set_max_size_amplification_percent(10000);
334        compaction_options.set_stop_style(rocksdb::UniversalCompactionStopStyle::Similar);
335        self.options
336            .set_universal_compaction_options(&compaction_options);
337
338        let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
339            .unwrap_or(DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER);
340        self.options.set_level_zero_file_num_compaction_trigger(
341            max_level_zero_file_num.try_into().unwrap(),
342        );
343        self.options.set_level_zero_slowdown_writes_trigger(
344            (max_level_zero_file_num * 12).try_into().unwrap(),
345        );
346        self.options
347            .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
348
349        // Increase sst file size to 128MiB.
350        self.options.set_target_file_size_base(
351            read_size_from_env(ENV_VAR_TARGET_FILE_SIZE_BASE_MB)
352                .unwrap_or(DEFAULT_TARGET_FILE_SIZE_BASE_MB) as u64
353                * 1024
354                * 1024,
355        );
356
357        // This should be a no-op for universal compaction but increasing it to be safe.
358        self.options
359            .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
360
361        self
362    }
363
364    // Overrides the block options with different block cache size and block size.
365    pub fn set_block_options(
366        mut self,
367        block_cache_size_mb: usize,
368        block_size_bytes: usize,
369    ) -> DBOptions {
370        self.options
371            .set_block_based_table_factory(&get_block_options(
372                block_cache_size_mb,
373                block_size_bytes,
374            ));
375        self
376    }
377
378    // Disables write stalling and stopping based on pending compaction bytes.
379    pub fn disable_write_throttling(mut self) -> DBOptions {
380        self.options.set_soft_pending_compaction_bytes_limit(0);
381        self.options.set_hard_pending_compaction_bytes_limit(0);
382        self
383    }
384}
385
386/// Creates a default RocksDB option, to be used when RocksDB option is
387/// unspecified.
388pub fn default_db_options() -> DBOptions {
389    let mut opt = rocksdb::Options::default();
390
391    // One common issue when running tests on Mac is that the default ulimit is too
392    // low, leading to I/O errors such as "Too many open files". Raising fdlimit
393    // to bypass it.
394    if let Some(limit) = fdlimit::raise_fd_limit() {
395        // on windows raise_fd_limit return None
396        opt.set_max_open_files((limit / 8) as i32);
397    }
398
399    // The table cache is locked for updates and this determines the number
400    // of shards, ie 2^10. Increase in case of lock contentions.
401    opt.set_table_cache_num_shard_bits(10);
402
403    // LSM compression settings
404    opt.set_compression_type(rocksdb::DBCompressionType::Lz4);
405    opt.set_bottommost_compression_type(rocksdb::DBCompressionType::Zstd);
406    opt.set_bottommost_zstd_max_train_bytes(1024 * 1024, true);
407
408    // IOTA uses multiple RocksDB in a node, so total sizes of write buffers and WAL
409    // can be higher than the limits below.
410    //
411    // RocksDB also exposes the option to configure total write buffer size across
412    // multiple instances via `write_buffer_manager`. But the write buffer flush
413    // policy (flushing the buffer receiving the next write) may not work well.
414    // So sticking to per-db write buffer size limit for now.
415    //
416    // The environment variables are only meant to be emergency overrides. They may
417    // go away in future. It is preferable to update the default value, or
418    // override the option in code.
419    opt.set_db_write_buffer_size(
420        read_size_from_env(ENV_VAR_DB_WRITE_BUFFER_SIZE).unwrap_or(DEFAULT_DB_WRITE_BUFFER_SIZE)
421            * 1024
422            * 1024,
423    );
424    opt.set_max_total_wal_size(
425        read_size_from_env(ENV_VAR_DB_WAL_SIZE).unwrap_or(DEFAULT_DB_WAL_SIZE) as u64 * 1024 * 1024,
426    );
427
428    // Num threads for compactions and memtable flushes.
429    opt.increase_parallelism(read_size_from_env(ENV_VAR_DB_PARALLELISM).unwrap_or(8) as i32);
430
431    opt.set_enable_pipelined_write(true);
432
433    // Increase block size to 16KiB.
434    // https://github.com/EighteenZi/rocksdb_wiki/blob/master/Memory-usage-in-RocksDB.md#indexes-and-filter-blocks
435    opt.set_block_based_table_factory(&get_block_options(128, 16 << 10));
436
437    // Set memtable bloomfilter.
438    opt.set_memtable_prefix_bloom_ratio(0.02);
439
440    DBOptions {
441        options: opt,
442        rw_options: ReadWriteOptions::default(),
443    }
444}
445
446fn get_block_options(block_cache_size_mb: usize, block_size_bytes: usize) -> BlockBasedOptions {
447    // Set options mostly similar to those used in optimize_for_point_lookup(),
448    // except non-default binary and hash index, to hopefully reduce lookup
449    // latencies without causing any regression for scanning, with slightly more
450    // memory usages. https://github.com/facebook/rocksdb/blob/11cb6af6e5009c51794641905ca40ce5beec7fee/options/options.cc#L611-L621
451    let mut block_options = BlockBasedOptions::default();
452    // Overrides block size.
453    block_options.set_block_size(block_size_bytes);
454    // Configure a block cache.
455    block_options.set_block_cache(&Cache::new_lru_cache(block_cache_size_mb << 20));
456    // Set a bloomfilter with 1% false positive rate.
457    block_options.set_bloom_filter(10.0, false);
458    // From https://github.com/EighteenZi/rocksdb_wiki/blob/master/Block-Cache.md#caching-index-and-filter-blocks
459    block_options.set_pin_l0_filter_and_index_blocks_in_cache(true);
460    block_options
461}
462
463pub fn list_tables(path: std::path::PathBuf) -> eyre::Result<Vec<String>> {
464    const DB_DEFAULT_CF_NAME: &str = "default";
465
466    let opts = rocksdb::Options::default();
467    rocksdb::DBWithThreadMode::<rocksdb::MultiThreaded>::list_cf(&opts, path)
468        .map_err(|e| e.into())
469        .map(|q| {
470            q.iter()
471                .filter_map(|s| {
472                    // The `default` table is not used
473                    if s != DB_DEFAULT_CF_NAME {
474                        Some(s.clone())
475                    } else {
476                        None
477                    }
478                })
479                .collect()
480        })
481}
482
483pub fn read_size_from_env(var_name: &str) -> Option<usize> {
484    env::var(var_name)
485        .ok()?
486        .parse::<usize>()
487        .tap_err(|e| {
488            warn!(
489                "Env var {} does not contain valid usize integer: {}",
490                var_name, e
491            )
492        })
493        .ok()
494}