typed_store/rocks/
options.rs1use std::{
6 collections::BTreeMap,
7 env,
8 sync::atomic::{AtomicBool, Ordering},
9};
10
11use iota_macros::nondeterministic;
12use rocksdb::{BlockBasedOptions, Cache, ReadOptions};
13use sysinfo::{MemoryRefreshKind, RefreshKind, System};
14use tap::TapFallible;
15use tracing::{debug, info, warn};
16
17const ENV_VAR_DB_WRITE_BUFFER_SIZE: &str = "DB_WRITE_BUFFER_SIZE_MB";
20const DEFAULT_DB_WRITE_BUFFER_SIZE: usize = 1024;
21
22const ENV_VAR_DB_WAL_SIZE: &str = "DB_WAL_SIZE_MB";
25const DEFAULT_DB_WAL_SIZE: usize = 1024;
26
27const ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER: &str = "L0_NUM_FILES_COMPACTION_TRIGGER";
30const DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 4;
31const DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER: usize = 80;
32const ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB: &str = "MAX_WRITE_BUFFER_SIZE_MB";
33const DEFAULT_MAX_WRITE_BUFFER_SIZE_MB: usize = 256;
34const ENV_VAR_MAX_WRITE_BUFFER_NUMBER: &str = "MAX_WRITE_BUFFER_NUMBER";
35const DEFAULT_MAX_WRITE_BUFFER_NUMBER: usize = 6;
36const ENV_VAR_TARGET_FILE_SIZE_BASE_MB: &str = "TARGET_FILE_SIZE_BASE_MB";
37const DEFAULT_TARGET_FILE_SIZE_BASE_MB: usize = 128;
38
39const ENV_VAR_DISABLE_BLOB_STORAGE: &str = "DISABLE_BLOB_STORAGE";
41const ENV_VAR_DB_PARALLELISM: &str = "DB_PARALLELISM";
42
43static FALLOCATE_DISABLED: AtomicBool = AtomicBool::new(false);
44
45pub fn disable_fallocate() {
51 FALLOCATE_DISABLED.store(true, Ordering::Relaxed);
52}
53
54pub(crate) fn base_db_options() -> rocksdb::Options {
57 let mut options = rocksdb::Options::default();
58 if !FALLOCATE_DISABLED.load(Ordering::Relaxed) {
59 return options;
60 }
61 options
64 .get_options_from_string("allow_fallocate=false")
65 .expect("allow_fallocate should be a valid RocksDB option")
66}
67
68#[derive(Clone, Debug, Default)]
69pub struct ReadWriteOptions {
70 pub log_value_hash: bool,
73}
74
75impl ReadWriteOptions {
76 pub fn readopts(&self) -> ReadOptions {
77 ReadOptions::default()
78 }
79
80 pub fn set_log_value_hash(mut self, log_value_hash: bool) -> Self {
81 self.log_value_hash = log_value_hash;
82 self
83 }
84}
85
86#[derive(Default, Clone)]
87pub struct DBOptions {
88 pub options: rocksdb::Options,
89 pub rw_options: ReadWriteOptions,
90}
91
92pub fn synced_write_options() -> rocksdb::WriteOptions {
95 let mut opts = rocksdb::WriteOptions::default();
96 opts.set_sync(true);
97 opts
98}
99
100const MIN_SST_BUFFER_SIZE: usize = 64 << 20;
103
104pub struct BulkIngestionOptions {
108 pub db_options: rocksdb::Options,
109 pub column_family_options: DBOptions,
110 cf_memory_budget: usize,
112}
113
114impl BulkIngestionOptions {
115 pub fn sst_buffer_limit(&self, writers: usize) -> usize {
119 (self.cf_memory_budget / writers.max(1)).max(MIN_SST_BUFFER_SIZE)
120 }
121
122 pub fn table_config(
125 &self,
126 table_names: impl IntoIterator<Item = String>,
127 ) -> DBMapTableConfigMap {
128 DBMapTableConfigMap::new(
129 table_names
130 .into_iter()
131 .map(|name| (name, self.column_family_options.clone()))
132 .collect(),
133 )
134 }
135}
136
137fn write_buffer_budget(total_memory_bytes: u64) -> (usize, usize) {
141 const FALLBACK_TOTAL_MEMORY_BYTES: u64 = 4 << 30;
144
145 let total_memory_bytes = if total_memory_bytes == 0 {
146 FALLBACK_TOTAL_MEMORY_BYTES as f64
147 } else {
148 total_memory_bytes as f64
149 };
150 (
151 (total_memory_bytes * 0.8) as usize,
152 (total_memory_bytes * 0.25) as usize,
153 )
154}
155
156pub fn bulk_ingestion_options() -> BulkIngestionOptions {
157 let total_memory_bytes = available_memory_bytes();
158 let num_cpus = num_cpus::get();
159 let (db_write_buffer_size, cf_memory_budget) = write_buffer_budget(total_memory_bytes);
160
161 let mut db_options = base_db_options();
162
163 db_options.set_unordered_write(true);
167
168 db_options.set_max_background_jobs(num_cpus as i32);
170
171 db_options.set_db_write_buffer_size(db_write_buffer_size);
174
175 let mut column_family_options = default_db_options();
179 column_family_options
180 .options
181 .set_disable_auto_compactions(true);
182
183 column_family_options
189 .options
190 .set_level_zero_file_num_compaction_trigger(-1);
191 column_family_options
192 .options
193 .set_level_zero_slowdown_writes_trigger(-1);
194 column_family_options
195 .options
196 .set_level_zero_stop_writes_trigger(i32::MAX);
197
198 const MIN_BUFFER_SIZE: usize = 64 * 1024 * 1024; let target_buffer_count = num_cpus.max(2);
202 let buffer_size = (cf_memory_budget / target_buffer_count).max(MIN_BUFFER_SIZE);
203 let buffer_count = (cf_memory_budget / buffer_size).clamp(2, target_buffer_count) as i32;
204 column_family_options
205 .options
206 .set_write_buffer_size(buffer_size);
207 column_family_options
208 .options
209 .set_max_write_buffer_number(buffer_count);
210
211 debug!(
212 total_memory_bytes,
213 num_cpus,
214 db_write_buffer_size,
215 buffer_size,
216 buffer_count,
217 "configured bulk ingestion options"
218 );
219
220 BulkIngestionOptions {
221 db_options,
222 column_family_options,
223 cf_memory_budget,
224 }
225}
226
227fn available_memory_bytes() -> u64 {
230 nondeterministic!({
234 let mut sys = System::new_with_specifics(
237 RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
238 );
239 sys.refresh_memory();
240
241 if let Some(cgroup_limits) = sys.cgroup_limits() {
242 if cgroup_limits.total_memory > 0 {
244 debug!(
245 limit = cgroup_limits.total_memory,
246 "using cgroup memory limit"
247 );
248 return cgroup_limits.total_memory;
249 }
250 }
251
252 let total = sys.total_memory();
253 debug!(total, "using total system memory");
254 total
255 })
256}
257
258#[derive(Clone)]
259pub struct DBMapTableConfigMap(BTreeMap<String, DBOptions>);
260impl DBMapTableConfigMap {
261 pub fn new(map: BTreeMap<String, DBOptions>) -> Self {
262 Self(map)
263 }
264
265 pub fn to_map(&self) -> BTreeMap<String, DBOptions> {
266 self.0.clone()
267 }
268}
269
270impl DBOptions {
271 pub fn optimize_for_point_lookup(mut self, block_cache_size_mb: usize) -> DBOptions {
275 self.options
277 .optimize_for_point_lookup(block_cache_size_mb as u64);
278 self
279 }
280
281 pub fn optimize_for_large_values_no_scan(mut self, min_blob_size: u64) -> DBOptions {
284 if env::var(ENV_VAR_DISABLE_BLOB_STORAGE).is_ok() {
285 info!("Large value blob storage optimization is disabled via env var.");
286 return self;
287 }
288
289 self.options.set_enable_blob_files(true);
291 self.options
292 .set_blob_compression_type(rocksdb::DBCompressionType::Lz4);
293 self.options.set_enable_blob_gc(true);
294 self.options.set_min_blob_size(min_blob_size);
298
299 let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
301 .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
302 * 1024
303 * 1024;
304 self.options.set_write_buffer_size(write_buffer_size);
305 let target_file_size_base = 64 << 20;
308 self.options
309 .set_target_file_size_base(target_file_size_base);
310 let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
312 .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
313 self.options
314 .set_max_bytes_for_level_base(target_file_size_base * max_level_zero_file_num as u64);
315
316 self
317 }
318
319 pub fn optimize_for_read(mut self, block_cache_size_mb: usize) -> DBOptions {
321 self.options
322 .set_block_based_table_factory(&get_block_options(block_cache_size_mb, 16 << 10));
323 self
324 }
325
326 pub fn optimize_db_for_write_throughput(mut self, db_max_write_buffer_gb: u64) -> DBOptions {
328 self.options
329 .set_db_write_buffer_size(db_max_write_buffer_gb as usize * 1024 * 1024 * 1024);
330 self.options
331 .set_max_total_wal_size(db_max_write_buffer_gb * 1024 * 1024 * 1024);
332 self
333 }
334
335 pub fn optimize_for_write_throughput(mut self) -> DBOptions {
337 let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
339 .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
340 * 1024
341 * 1024;
342 self.options.set_write_buffer_size(write_buffer_size);
343 let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
345 .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
346 self.options
347 .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
348 self.options
350 .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
351
352 let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
354 .unwrap_or(DEFAULT_L0_NUM_FILES_COMPACTION_TRIGGER);
355 self.options.set_level_zero_file_num_compaction_trigger(
356 max_level_zero_file_num.try_into().unwrap(),
357 );
358 self.options.set_level_zero_slowdown_writes_trigger(
359 (max_level_zero_file_num * 12).try_into().unwrap(),
360 );
361 self.options
362 .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
363
364 self.options
366 .set_target_file_size_base(target_file_size_base());
367
368 self.options
370 .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
371
372 self
373 }
374
375 pub fn optimize_for_write_throughput_no_deletion(mut self) -> DBOptions {
379 let write_buffer_size = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_SIZE_MB)
381 .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_SIZE_MB)
382 * 1024
383 * 1024;
384 self.options.set_write_buffer_size(write_buffer_size);
385 let max_write_buffer_number = read_size_from_env(ENV_VAR_MAX_WRITE_BUFFER_NUMBER)
387 .unwrap_or(DEFAULT_MAX_WRITE_BUFFER_NUMBER);
388 self.options
389 .set_max_write_buffer_number(max_write_buffer_number.try_into().unwrap());
390 self.options
392 .set_max_write_buffer_size_to_maintain((write_buffer_size).try_into().unwrap());
393
394 self.options
396 .set_compaction_style(rocksdb::DBCompactionStyle::Universal);
397 let mut compaction_options = rocksdb::UniversalCompactOptions::default();
398 compaction_options.set_max_size_amplification_percent(10000);
399 compaction_options.set_stop_style(rocksdb::UniversalCompactionStopStyle::Similar);
400 self.options
401 .set_universal_compaction_options(&compaction_options);
402
403 let max_level_zero_file_num = read_size_from_env(ENV_VAR_L0_NUM_FILES_COMPACTION_TRIGGER)
404 .unwrap_or(DEFAULT_UNIVERSAL_COMPACTION_L0_NUM_FILES_COMPACTION_TRIGGER);
405 self.options.set_level_zero_file_num_compaction_trigger(
406 max_level_zero_file_num.try_into().unwrap(),
407 );
408 self.options.set_level_zero_slowdown_writes_trigger(
409 (max_level_zero_file_num * 12).try_into().unwrap(),
410 );
411 self.options
412 .set_level_zero_stop_writes_trigger((max_level_zero_file_num * 16).try_into().unwrap());
413
414 self.options
416 .set_target_file_size_base(target_file_size_base());
417
418 self.options
420 .set_max_bytes_for_level_base((write_buffer_size * max_level_zero_file_num) as u64);
421
422 self
423 }
424
425 pub fn set_block_options(
427 mut self,
428 block_cache_size_mb: usize,
429 block_size_bytes: usize,
430 ) -> DBOptions {
431 self.options
432 .set_block_based_table_factory(&get_block_options(
433 block_cache_size_mb,
434 block_size_bytes,
435 ));
436 self
437 }
438
439 pub fn disable_write_throttling(mut self) -> DBOptions {
441 self.options.set_soft_pending_compaction_bytes_limit(0);
442 self.options.set_hard_pending_compaction_bytes_limit(0);
443 self.options.set_level_zero_slowdown_writes_trigger(512);
444 self.options.set_level_zero_stop_writes_trigger(1024);
445 self
446 }
447}
448
449pub fn default_db_options() -> DBOptions {
452 let mut opt = base_db_options();
453
454 if let Some(limit) = fdlimit::raise_fd_limit() {
458 opt.set_max_open_files((limit / 8) as i32);
460 }
461
462 opt.set_table_cache_num_shard_bits(10);
465
466 opt.set_compression_type(rocksdb::DBCompressionType::Lz4);
468 opt.set_bottommost_compression_type(rocksdb::DBCompressionType::Zstd);
469 opt.set_bottommost_zstd_max_train_bytes(1024 * 1024, true);
470
471 opt.set_db_write_buffer_size(
483 read_size_from_env(ENV_VAR_DB_WRITE_BUFFER_SIZE).unwrap_or(DEFAULT_DB_WRITE_BUFFER_SIZE)
484 * 1024
485 * 1024,
486 );
487 opt.set_max_total_wal_size(
488 read_size_from_env(ENV_VAR_DB_WAL_SIZE).unwrap_or(DEFAULT_DB_WAL_SIZE) as u64 * 1024 * 1024,
489 );
490
491 opt.increase_parallelism(read_size_from_env(ENV_VAR_DB_PARALLELISM).unwrap_or(8) as i32);
493
494 opt.set_enable_pipelined_write(true);
495
496 opt.set_block_based_table_factory(&get_block_options(128, 16 << 10));
499
500 opt.set_memtable_prefix_bloom_ratio(0.02);
502
503 DBOptions {
504 options: opt,
505 rw_options: ReadWriteOptions::default(),
506 }
507}
508
509fn get_block_options(block_cache_size_mb: usize, block_size_bytes: usize) -> BlockBasedOptions {
510 let mut block_options = BlockBasedOptions::default();
515 block_options.set_block_size(block_size_bytes);
517 block_options.set_block_cache(&Cache::new_lru_cache(block_cache_size_mb << 20));
519 block_options.set_bloom_filter(10.0, false);
521 block_options.set_pin_l0_filter_and_index_blocks_in_cache(true);
523 block_options
524}
525
526pub fn list_tables(path: std::path::PathBuf) -> eyre::Result<Vec<String>> {
527 const DB_DEFAULT_CF_NAME: &str = "default";
528
529 let opts = rocksdb::Options::default();
530 rocksdb::DBWithThreadMode::<rocksdb::MultiThreaded>::list_cf(&opts, path)
531 .map_err(|e| e.into())
532 .map(|q| {
533 q.iter()
534 .filter_map(|s| {
535 if s != DB_DEFAULT_CF_NAME {
537 Some(s.clone())
538 } else {
539 None
540 }
541 })
542 .collect()
543 })
544}
545
546pub fn target_file_size_base() -> u64 {
551 read_size_from_env(ENV_VAR_TARGET_FILE_SIZE_BASE_MB).unwrap_or(DEFAULT_TARGET_FILE_SIZE_BASE_MB)
552 as u64
553 * 1024
554 * 1024
555}
556
557pub fn read_size_from_env(var_name: &str) -> Option<usize> {
558 env::var(var_name)
559 .ok()?
560 .parse::<usize>()
561 .tap_err(|e| {
562 warn!(
563 "Env var {} does not contain valid usize integer: {}",
564 var_name, e
565 )
566 })
567 .ok()
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
577 fn write_buffer_budget_scales_with_available_memory() {
578 const TOTAL: u64 = 64 << 30;
579
580 let (db_one, cf_one) = write_buffer_budget(TOTAL);
581 assert_eq!(db_one, (TOTAL as f64 * 0.8) as usize);
582 assert_eq!(cf_one, (TOTAL as f64 * 0.25) as usize);
583
584 let (db_unknown, cf_unknown) = write_buffer_budget(0);
587 assert_eq!(write_buffer_budget(4 << 30), (db_unknown, cf_unknown));
588 assert!(db_unknown > 0 && cf_unknown > 0);
589
590 const SMALL: u64 = 2 << 30;
593 let (db_small, cf_small) = write_buffer_budget(SMALL);
594 assert_eq!(db_small, (SMALL as f64 * 0.8) as usize);
595 assert_eq!(cf_small, (SMALL as f64 * 0.25) as usize);
596 assert!(db_small < db_unknown);
597 }
598}