Skip to content

Commit 2938ada

Browse files
shesekphilippem
andcommitted
fix(rocksdb): Apply native backpressure during bulk indexing
- Retain the bulk L0 compaction trigger at 32, but slow and stop writes at 48/64 files and at 8/32 GiB of pending compaction debt. This lets RocksDB regulate ingestion when compaction cannot keep up. - Expose the target SST size and bulk-load pending-compaction limits as tuning knobs, with defaults of 1024 MB and 8/32 GiB. - Cap max_subcompactions at four to limit compaction concurrency and memory pressure under high db_parallelism settings. Spenttxouts processing remains fully parallel, with no application-level polling or chunking. Co-authored-by: Philippe McLean <philippe.mclean@gmail.com>
1 parent e93d17d commit 2938ada

4 files changed

Lines changed: 79 additions & 22 deletions

File tree

src/config.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ pub struct Config {
7777
/// per SST file of unbounded memory.
7878
pub db_cache_index_filter_blocks: bool,
7979

80+
/// RocksDB target_file_size_base in MB (per CF). Smaller values produce smaller
81+
/// L1+ SST files at the cost of more files and more compactions overall. Default
82+
/// 1024 (1 GiB).
83+
pub db_target_file_size_mb: usize,
84+
85+
/// Used during initial sync.
86+
/// RocksDB soft_pending_compaction_bytes_limit in GiB (per CF). When the estimated
87+
/// compaction backlog exceeds this, RocksDB rate-limits writes. 0 disables the limit.
88+
/// A finite value (e.g. 8) provides automatic backpressure when compaction falls behind.
89+
pub db_soft_pending_compaction_gb: u64,
90+
91+
/// Used during initial sync.
92+
/// RocksDB hard_pending_compaction_bytes_limit in GiB (per CF). When the estimated
93+
/// compaction backlog exceeds this, RocksDB stops writes entirely until compaction
94+
/// catches up. 0 disables. Should be 3-4x the soft limit (e.g. 32 with soft=8).
95+
pub db_hard_pending_compaction_gb: u64,
96+
8097
#[cfg(feature = "liquid")]
8198
pub parent_network: BNetwork,
8299
#[cfg(feature = "liquid")]
@@ -274,6 +291,24 @@ impl Config {
274291
Arg::with_name("cache_index_filter_blocks")
275292
.long("cache-index-filter-blocks")
276293
.help("Store index/filter blocks in the block cache instead of on the heap. Bounds memory but allows eviction under cache pressure.")
294+
).arg(
295+
Arg::with_name("db_target_file_size_mb")
296+
.long("db-target-file-size-mb")
297+
.help("RocksDB target_file_size_base in MB per CF. Smaller values produce more, smaller L1+ SST files. Default 1024.")
298+
.takes_value(true)
299+
.default_value("1024")
300+
).arg(
301+
Arg::with_name("db_soft_pending_compaction_gb")
302+
.long("db-soft-pending-compaction-gb")
303+
.help("RocksDB soft_pending_compaction_bytes_limit in GiB per CF during initial sync. RocksDB rate-limits writes above this. 0 disables.")
304+
.takes_value(true)
305+
.default_value("8")
306+
).arg(
307+
Arg::with_name("db_hard_pending_compaction_gb")
308+
.long("db-hard-pending-compaction-gb")
309+
.help("RocksDB hard_pending_compaction_bytes_limit in GiB per CF during initial sync. RocksDB stops writes above this. 0 disables.")
310+
.takes_value(true)
311+
.default_value("32")
277312
).arg(
278313
Arg::with_name("zmq_addr")
279314
.long("zmq-addr")
@@ -529,6 +564,17 @@ impl Config {
529564
db_write_buffer_size_mb: value_t_or_exit!(m, "db_write_buffer_size_mb", usize),
530565
initial_sync_batch_size: value_t_or_exit!(m, "initial_sync_batch_size", usize),
531566
db_cache_index_filter_blocks: m.is_present("cache_index_filter_blocks"),
567+
db_target_file_size_mb: value_t_or_exit!(m, "db_target_file_size_mb", usize),
568+
db_soft_pending_compaction_gb: value_t_or_exit!(
569+
m,
570+
"db_soft_pending_compaction_gb",
571+
u64
572+
),
573+
db_hard_pending_compaction_gb: value_t_or_exit!(
574+
m,
575+
"db_hard_pending_compaction_gb",
576+
u64
577+
),
532578
zmq_addr,
533579

534580
#[cfg(feature = "liquid")]

src/new_index/db.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ impl DB {
114114
"sentinel 'F' present in {} — using steady-state L0 triggers",
115115
cf_name
116116
);
117+
db.apply_steady_state_triggers();
117118
}
118119
db
119120
}
@@ -159,29 +160,23 @@ impl DB {
159160
//
160161
// With bloom filters at 10 bits/key and a 128 MB write buffer, each L0
161162
// file has ~3.9 M keys, so its filter block is ~4.9 MB. At the slowdown
162-
// threshold (96 files) that is ~470 MB of pinned filter blocks per CF,
163-
// ~1.41 GB across 3 data CFs — within a 2 GB cache. At the stop threshold
164-
// (128 files) it is ~628 MB per CF / ~1.88 GB total, still within bounds.
163+
// threshold (48 files) that is ~235 MB of pinned filter blocks per CF,
164+
// ~705 MB across 3 data CFs — within a 2 GB cache. At the stop threshold
165+
// (64 files) it is ~314 MB per CF / ~940 MB total, still within bounds.
165166
// Previously trigger=64 with 256 MB buffers caused pinned metadata to
166167
// overflow the 2 GB cache at L0=128 (~3.7 GB), spilling to uncontrolled
167-
// heap and triggering OOM. Trigger=32 + slowdown=96 keeps the peak safe
168-
// while allowing enough L0 accumulation for good bulk-load throughput.
168+
// heap and triggering OOM.
169169
//
170-
// Set slowdown/stop triggers well above the compaction trigger so writes
171-
// are never stalled while background compaction catches up.
172-
// Disable the pending-compaction-bytes stall so the large backlog that
173-
// builds up during the bulk load does not block writes.
174-
const L0_BULK_TRIGGER: u32 = 32;
175-
let trigger = L0_BULK_TRIGGER.to_string();
176-
let slowdown = (L0_BULK_TRIGGER * 3).to_string();
177-
let stop = (L0_BULK_TRIGGER * 4).to_string();
170+
// Set slowdown/stop triggers above the compaction trigger so RocksDB
171+
// throttles writes while background compaction catches up.
172+
const L0_BULK_TRIGGER: &str = "32";
173+
const L0_BULK_SLOWDOWN: &str = "48";
174+
const L0_BULK_STOP: &str = "64";
178175

179176
let opts = [
180-
("level0_file_num_compaction_trigger", trigger.as_str()),
181-
("level0_slowdown_writes_trigger", slowdown.as_str()),
182-
("level0_stop_writes_trigger", stop.as_str()),
183-
("soft_pending_compaction_bytes_limit", "0"),
184-
("hard_pending_compaction_bytes_limit", "0"),
177+
("level0_file_num_compaction_trigger", L0_BULK_TRIGGER),
178+
("level0_slowdown_writes_trigger", L0_BULK_SLOWDOWN),
179+
("level0_stop_writes_trigger", L0_BULK_STOP),
185180
];
186181
self.db.set_options_cf(self.cf(), &opts).unwrap();
187182
}
@@ -497,9 +492,10 @@ pub fn open_rocksdb(path: &Path, config: &Config) -> rocksdb::DB {
497492

498493
// Parallelize sub-ranges within a single compaction job (including the one-time
499494
// full_compaction at the end of initial sync). Without this, compact_range() is
500-
// single-threaded regardless of increase_parallelism(). Setting it equal to the
501-
// parallelism level keeps all background threads busy during the final compaction.
502-
db_opts.set_max_subcompactions(parallelism as u32);
495+
// single-threaded regardless of increase_parallelism(). Cap it at four because
496+
// subcompactions can multiply the configured background-job concurrency:
497+
// https://github.com/facebook/rocksdb/wiki/Subcompaction#options
498+
db_opts.set_max_subcompactions(parallelism.min(4) as u32);
503499

504500
// Create a single shared LRU cache for all CFs. The total size is
505501
// --db-block-cache-mb (not multiplied by 3). RocksDB's LRU cache is
@@ -563,7 +559,17 @@ fn data_cf_options(config: &Config, shared_cache: &rocksdb::Cache) -> rocksdb::O
563559
cf_opts.set_compaction_style(rocksdb::DBCompactionStyle::Level);
564560
cf_opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
565561
cf_opts.set_bottommost_compression_type(rocksdb::DBCompressionType::Zstd);
566-
cf_opts.set_target_file_size_base(1_073_741_824);
562+
cf_opts.set_target_file_size_base((config.db_target_file_size_mb as u64) * 1024 * 1024);
563+
// 0 disables the limit. Set finite values via
564+
// --db-{soft,hard}-pending-compaction-gb to engage RocksDB's automatic write
565+
// throttling when the compaction backlog grows past the threshold. Steady-state
566+
// defaults are restored after full compaction.
567+
cf_opts.set_soft_pending_compaction_bytes_limit(
568+
(config.db_soft_pending_compaction_gb as usize) << 30,
569+
);
570+
cf_opts.set_hard_pending_compaction_bytes_limit(
571+
(config.db_hard_pending_compaction_gb as usize) << 30,
572+
);
567573
// L0 compaction triggers are left at RocksDB defaults (4/20/36) here.
568574
// After open, apply_bulk_load_triggers() widens them for initial sync
569575
// when the full-compaction sentinel 'F' is absent.

src/new_index/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,8 @@ pub mod bench {
22192219
index_unspendables: false,
22202220
network: crate::chain::Network::Regtest,
22212221
block_batch_size: 250,
2222+
#[cfg(not(feature = "liquid"))]
2223+
use_spenttxouts: false,
22222224
};
22232225
let height = 702861;
22242226
let hash = block.block_hash();

tests/common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ impl TestRunner {
124124
db_write_buffer_size_mb: 256,
125125
initial_sync_batch_size: 250,
126126
db_cache_index_filter_blocks: false,
127+
db_target_file_size_mb: 1024,
128+
db_soft_pending_compaction_gb: 0,
129+
db_hard_pending_compaction_gb: 0,
127130
//#[cfg(feature = "electrum-discovery")]
128131
//electrum_public_hosts: Option<crate::electrum::ServerHosts>,
129132
//#[cfg(feature = "electrum-discovery")]

0 commit comments

Comments
 (0)