Skip to content

Commit 26eec6f

Browse files
authored
perf(cubestore): early compaction split (#11079)
1 parent b42b780 commit 26eec6f

4 files changed

Lines changed: 68 additions & 9 deletions

File tree

rust/cubestore/cubestore/src/config/mod.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ pub trait ConfigObj: DIService {
393393

394394
fn wal_split_threshold(&self) -> u64;
395395

396+
fn wal_split_size_threshold_bytes(&self) -> u64;
397+
396398
fn select_worker_pool_size(&self) -> usize;
397399

398400
fn select_worker_idle_timeout(&self) -> u64;
@@ -587,6 +589,7 @@ pub struct ConfigObjImpl {
587589
pub max_partition_split_threshold: u64,
588590
pub compaction_chunks_total_size_threshold: u64,
589591
pub compaction_chunks_count_threshold: u64,
592+
pub compaction_chunks_threshold_multiplier: f64,
590593
pub compaction_chunks_in_memory_size_threshold: u64,
591594
pub compaction_chunks_max_lifetime_threshold: u64,
592595
pub compaction_in_memory_chunks_max_lifetime_threshold: u64,
@@ -597,6 +600,7 @@ pub struct ConfigObjImpl {
597600
pub compaction_in_memory_chunks_ratio_check_threshold: u64,
598601
pub compaction_in_memory_chunks_schedule_period_secs: u64,
599602
pub wal_split_threshold: u64,
603+
pub wal_split_size_threshold_bytes: u64,
600604
pub data_dir: PathBuf,
601605
pub dump_dir: Option<PathBuf>,
602606
pub store_provider: FileStoreProvider,
@@ -709,11 +713,15 @@ impl ConfigObj for ConfigObjImpl {
709713
}
710714

711715
fn compaction_chunks_total_size_threshold(&self) -> u64 {
712-
self.compaction_chunks_total_size_threshold
716+
(self.compaction_chunks_total_size_threshold as f64
717+
* self.compaction_chunks_threshold_multiplier)
718+
.floor() as u64
713719
}
714720

715721
fn compaction_chunks_count_threshold(&self) -> u64 {
716-
self.compaction_chunks_count_threshold
722+
(self.compaction_chunks_count_threshold as f64
723+
* self.compaction_chunks_threshold_multiplier)
724+
.floor() as u64
717725
}
718726

719727
fn compaction_chunks_in_memory_size_threshold(&self) -> u64 {
@@ -756,6 +764,10 @@ impl ConfigObj for ConfigObjImpl {
756764
self.wal_split_threshold
757765
}
758766

767+
fn wal_split_size_threshold_bytes(&self) -> u64 {
768+
self.wal_split_size_threshold_bytes
769+
}
770+
759771
fn select_worker_pool_size(&self) -> usize {
760772
self.select_worker_pool_size
761773
}
@@ -1330,6 +1342,10 @@ impl Config {
13301342
1048576 * 8,
13311343
),
13321344
compaction_chunks_count_threshold: env_parse("CUBESTORE_CHUNKS_COUNT_THRESHOLD", 4),
1345+
compaction_chunks_threshold_multiplier: env_parse(
1346+
"CUBESTORE_COMPACTION_CHUNKS_THRESHOLD_MULTIPLIER",
1347+
1.0_f64,
1348+
),
13331349
compaction_chunks_in_memory_size_threshold: env_parse_size(
13341350
"CUBESTORE_COMPACTION_CHUNKS_IN_MEMORY_SIZE_THRESHOLD",
13351351
1 * 1024 * 1024 * 1024,
@@ -1533,6 +1549,12 @@ impl Config {
15331549
download_concurrency: env_parse("CUBESTORE_MAX_ACTIVE_DOWNLOADS", 8),
15341550
max_ingestion_data_frames: env_parse("CUBESTORE_MAX_DATA_FRAMES", 4),
15351551
wal_split_threshold: env_parse("CUBESTORE_WAL_SPLIT_THRESHOLD", 1048576 / 2),
1552+
wal_split_size_threshold_bytes: env_parse_size(
1553+
"CUBESTORE_WAL_SPLIT_SIZE_THRESHOLD",
1554+
100 * 1024 * 1024,
1555+
None,
1556+
None,
1557+
) as u64,
15361558
job_runners_count: env_parse("CUBESTORE_JOB_RUNNERS", 4),
15371559
long_term_job_runners_count: env_parse("CUBESTORE_LONG_TERM_JOB_RUNNERS", 32),
15381560
csv_import_job_runners_count: env_parse("CUBESTORE_CSV_IMPORT_JOB_RUNNERS", 0),
@@ -1720,6 +1742,7 @@ impl Config {
17201742
partition_size_split_threshold_bytes: 2 * 1024,
17211743
max_partition_split_threshold: 20,
17221744
compaction_chunks_count_threshold: 1,
1745+
compaction_chunks_threshold_multiplier: 1.0,
17231746
compaction_chunks_in_memory_size_threshold: 3 * 1024 * 1024 * 1024,
17241747
compaction_chunks_total_size_threshold: 10,
17251748
compaction_chunks_max_lifetime_threshold: 600,
@@ -1778,6 +1801,7 @@ impl Config {
17781801
download_concurrency: 8,
17791802
max_ingestion_data_frames: 4,
17801803
wal_split_threshold: 262144,
1804+
wal_split_size_threshold_bytes: 100 * 1024 * 1024,
17811805
connection_timeout: 60,
17821806
server_name: "localhost".to_string(),
17831807
upload_to_remote: true,

rust/cubestore/cubestore/src/import/mod.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ impl ImportFormat {
8787
Ok(rows.boxed())
8888
}
8989

90+
/// Estimated in-memory Arrow buffer footprint of a single appended value, used to
91+
/// cap a data frame by size during import. Mirrors `columns_vec_buffer_size` accounting
92+
/// closely enough to drive the split; exactness is unnecessary for a threshold.
93+
fn estimate_arrow_value_size(column_type: &ColumnType, value: Option<&str>) -> usize {
94+
match column_type {
95+
ColumnType::String | ColumnType::Bytes | ColumnType::HyperLogLog(_) => {
96+
value.map_or(0, |v| v.len()) + 4
97+
}
98+
ColumnType::Int | ColumnType::Timestamp | ColumnType::Float => 8,
99+
ColumnType::Int96 | ColumnType::Decimal { .. } | ColumnType::Decimal96 { .. } => 16,
100+
ColumnType::Boolean => 1,
101+
}
102+
}
103+
90104
pub fn parse_column_value_str(column: &Column, value: &str) -> Result<TableValue, CubeError> {
91105
Ok(match column.get_column_type() {
92106
ColumnType::String => TableValue::String(value.to_string()),
@@ -777,12 +791,17 @@ impl ImportServiceImpl {
777791
};
778792

779793
let table_cols = table.get_row().get_columns().as_slice();
794+
let row_threshold = self.config_obj.wal_split_threshold() as usize;
795+
let size_threshold = self.config_obj.wal_split_size_threshold_bytes() as usize;
780796
let mut builders = create_array_builders(table_cols);
781797
let mut num_rows = 0;
798+
let mut estimated_bytes = 0;
782799
while let Some(line) = lines.next().await {
783800
let line = line?;
784801
let is_data_row = parser.visit_line(line.as_str(), |insert_pos, column, value| {
785802
let builder = builders[insert_pos].as_mut();
803+
estimated_bytes +=
804+
ImportFormat::estimate_arrow_value_size(column.get_column_type(), value);
786805
match value {
787806
None => {
788807
append_value(builder, column.get_column_type(), &TableValue::Null);
@@ -799,10 +818,11 @@ impl ImportServiceImpl {
799818
}
800819
num_rows += 1;
801820

802-
if num_rows >= self.config_obj.wal_split_threshold() as usize {
821+
if num_rows >= row_threshold || estimated_bytes >= size_threshold {
803822
let mut to_add = create_array_builders(table_cols);
804823
mem::swap(&mut builders, &mut to_add);
805824
num_rows = 0;
825+
estimated_bytes = 0;
806826

807827
let builded_rows = finish(to_add);
808828

rust/cubestore/cubestore/src/sql/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3315,7 +3315,9 @@ mod tests {
33153315
Config::test("over_10k_join").update_config(|mut c| {
33163316
c.partition_split_threshold = 1000000;
33173317
c.compaction_chunks_count_threshold = 50;
3318-
c.max_joined_partitions = 10;
3318+
// Eager split-by-file-size fragments the right table further; lift the join cap
3319+
// above the resulting partition count (this test asserts join correctness, not the cap).
3320+
c.max_joined_partitions = 50;
33193321
c
33203322
}).start_test(async move |services| {
33213323
let service = services.sql_service;
@@ -4048,6 +4050,8 @@ mod tests {
40484050
Config::test("inactive_partitions_cleanup")
40494051
.update_config(|mut c| {
40504052
c.partition_split_threshold = 1000000;
4053+
// Keep a single active partition: this test covers inactive-file GC, not size split.
4054+
c.partition_size_split_threshold_bytes = 1024 * 1024;
40514055
c.compaction_chunks_count_threshold = 0;
40524056
c.not_used_timeout = 0;
40534057
c.meta_store_log_upload_interval = 1;

rust/cubestore/cubestore/src/store/compaction.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -583,13 +583,20 @@ impl CompactionService for CompactionServiceImpl {
583583
) as usize)
584584
// Do not allow to much of new partitions to limit partition accuracy trade off
585585
.min(16);
586-
let new_partitions_count_by_file_size =
587-
if let Some(partition_file_size) = partition.get_row().file_size() {
586+
// Size the split by the bytes actually being written: the existing main table plus
587+
// the pending chunks merged in this pass. A partition with a small (or empty) main
588+
// table but large accumulated chunks must still split by size in a single pass,
589+
// otherwise it under-splits and re-splits on the next round.
590+
let new_partitions_count_by_file_size = {
591+
let total_file_size =
592+
partition.get_row().file_size().unwrap_or(0) + chunks_total_file_size;
593+
if total_file_size > 0 {
588594
let threshold = self.config.partition_size_split_threshold_bytes();
589-
(div_ceil(partition_file_size, threshold) as usize).min(16)
595+
(div_ceil(total_file_size, threshold) as usize).min(16)
590596
} else {
591597
1
592-
};
598+
}
599+
};
593600

594601
let new_partitions_count =
595602
new_partitions_count_by_rows.max(new_partitions_count_by_file_size);
@@ -2397,6 +2404,8 @@ mod tests {
23972404
Config::test("partition_compaction_decimal96")
23982405
.update_config(|mut c| {
23992406
c.partition_split_threshold = 20;
2407+
// Keep the split row-based: this test covers decimal handling, not size split.
2408+
c.partition_size_split_threshold_bytes = 1024 * 1024;
24002409
c
24012410
})
24022411
.start_test(async move |services| {
@@ -2497,7 +2506,9 @@ mod tests {
24972506
.get_active_partitions_by_index_id(1)
24982507
.await
24992508
.unwrap();
2500-
assert_eq!(partitions.len(), 1);
2509+
// Eager split-by-file-size: the first compaction already splits because the
2510+
// pending chunks exceed partition_size_split_threshold_bytes.
2511+
assert!(partitions.len() > 1);
25012512
let values = (0..10)
25022513
.map(|_| format!("('{}', '{}')", "a".repeat(10), "b".repeat(10)))
25032514
.collect::<Vec<_>>()

0 commit comments

Comments
 (0)