Skip to content

Commit a5cc347

Browse files
authored
Further optimizations for ingestion flow (#1668)
* Investigations for ingestion optimization Try to find more areas for optimization. - compress arrow files while writing (lz4_frame or zstd) - create new .part file based on size - one parquet per arrow file (converted in parallel) - separate runtimes to run ingestion tasks, sync and conversion tasks * add env vars for sort pruning and parquet creation * revert pruning env var
1 parent 551f792 commit a5cc347

4 files changed

Lines changed: 184 additions & 83 deletions

File tree

src/parseable/staging/writer.rs

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ use std::{
2626
};
2727

2828
use arrow_array::RecordBatch;
29-
use arrow_ipc::writer::StreamWriter;
29+
use arrow_ipc::{
30+
CompressionType,
31+
writer::{IpcWriteOptions, StreamWriter},
32+
};
3033
use arrow_schema::Schema;
3134
use arrow_select::concat::concat_batches;
3235
use chrono::{TimeDelta, Utc};
36+
use datafusion::physical_plan::buffer::SizedMessage;
3337
use itertools::Itertools;
3438
use once_cell::sync::Lazy;
3539
use rand::distributions::{Alphanumeric, DistString};
3640
use tracing::error;
41+
use ulid::Ulid;
3742

3843
use crate::{
3944
parseable::{ARROW_FILE_EXTENSION, PART_FILE_EXTENSION},
@@ -53,9 +58,31 @@ static DISK_WRITE_BATCH_MAX_AGE_SECS: Lazy<i64> = Lazy::new(|| {
5358
}
5459
});
5560

61+
const ARROW_FLUSH_SIZE_LIMIT_VAR: &str = "ARROW_FLUSH_SIZE_LIMIT";
62+
static ARROW_FLUSH_SIZE_LIMIT: Lazy<usize> = Lazy::new(|| {
63+
if let Ok(var) = std::env::var(ARROW_FLUSH_SIZE_LIMIT_VAR)
64+
&& let Ok(var) = var.parse::<usize>()
65+
{
66+
var
67+
} else {
68+
1024 * 1024 * 1024 * 10
69+
}
70+
});
71+
72+
const ONE_PARQUET_PER_ARROW_VAR: &str = "ONE_PARQUET_PER_ARROW";
73+
static ONE_PARQUET_PER_ARROW: Lazy<bool> = Lazy::new(|| {
74+
if let Ok(var) = std::env::var(ONE_PARQUET_PER_ARROW_VAR)
75+
&& let Ok(var) = var.parse::<bool>()
76+
{
77+
var
78+
} else {
79+
false
80+
}
81+
});
82+
5683
#[derive(Default)]
5784
pub struct Writer {
58-
pub mem: MemWriter<16384>,
85+
pub mem: MemWriter<4096>,
5986
pub disk: HashMap<String, DiskWriter>,
6087
disk_pending: HashMap<String, PendingDiskBatch>,
6188
}
@@ -162,17 +189,21 @@ fn write_pending_disk_batch(
162189

163190
let schema = pending.batches[0].schema();
164191
let batch = concat_batches(&schema, pending.batches.iter())?;
165-
match disk.get_mut(&filename) {
192+
let s = match disk.get_mut(&filename) {
166193
Some(writer) => writer.write(&batch)?,
167194
None => {
168195
let range = pending.range.expect("pending disk batch must have range");
169196
if let Some(parent) = file_path.parent() {
170197
fs::create_dir_all(parent)?;
171198
}
172199
let mut writer = DiskWriter::try_new(file_path, &schema, range)?;
173-
writer.write(&batch)?;
174-
disk.insert(filename, writer);
200+
let s = writer.write(&batch)?;
201+
disk.insert(filename.clone(), writer);
202+
s
175203
}
204+
};
205+
if s >= *ARROW_FLUSH_SIZE_LIMIT {
206+
disk.remove(&filename);
176207
}
177208

178209
Ok(())
@@ -203,6 +234,7 @@ pub struct DiskWriter {
203234
inner: StreamWriter<BufWriter<File>>,
204235
path: PathBuf,
205236
range: TimeRange,
237+
size: usize,
206238
}
207239

208240
impl DiskWriter {
@@ -219,9 +251,22 @@ impl DiskWriter {
219251
.truncate(true)
220252
.create(true)
221253
.open(&path)?;
222-
let inner = StreamWriter::try_new_buffered(file, schema)?;
223-
224-
Ok(Self { inner, path, range })
254+
let inner = StreamWriter::try_new_with_options(
255+
BufWriter::new(file),
256+
schema,
257+
IpcWriteOptions::default()
258+
.try_with_compression(Some(CompressionType::LZ4_FRAME))
259+
.unwrap(),
260+
)?;
261+
262+
let size = 0;
263+
264+
Ok(Self {
265+
inner,
266+
path,
267+
range,
268+
size,
269+
})
225270
}
226271

227272
pub fn is_current(&self) -> bool {
@@ -230,8 +275,10 @@ impl DiskWriter {
230275

231276
/// Write a single recordbatch into file
232277
#[cfg_attr(feature = "hotpath", hotpath::measure)]
233-
pub fn write(&mut self, rb: &RecordBatch) -> Result<(), StagingError> {
234-
self.inner.write(rb).map_err(StagingError::Arrow)
278+
pub fn write(&mut self, rb: &RecordBatch) -> Result<usize, StagingError> {
279+
self.size += rb.size();
280+
self.inner.write(rb).map_err(StagingError::Arrow)?;
281+
Ok(self.size)
235282
}
236283
}
237284

@@ -244,7 +291,15 @@ impl Drop for DiskWriter {
244291
}
245292

246293
let mut arrow_path = self.path.to_owned();
247-
arrow_path.set_extension(ARROW_FILE_EXTENSION);
294+
295+
// a rudimentary way to ensure one parquet per arrow file
296+
if *ONE_PARQUET_PER_ARROW {
297+
arrow_path.set_extension(Ulid::new().to_string());
298+
#[allow(clippy::incompatible_msrv)]
299+
arrow_path.add_extension(ARROW_FILE_EXTENSION);
300+
} else {
301+
arrow_path.set_extension(ARROW_FILE_EXTENSION);
302+
}
248303

249304
// If file exists, append a random string before .date to avoid overwriting
250305
if arrow_path.exists() {
@@ -260,6 +315,11 @@ impl Drop for DiskWriter {
260315
if let Err(err) = std::fs::rename(&self.path, &arrow_path) {
261316
error!("Couldn't rename file {:?}, error = {err}", self.path);
262317
}
318+
tracing::info!(
319+
"flushing {:?} due to drop with size {}\n",
320+
self.path,
321+
self.size
322+
);
263323
}
264324
}
265325

src/parseable/streams.rs

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use arrow_schema::{Field, Fields, Schema};
2323
use chrono::{NaiveDateTime, Timelike, Utc};
2424
use derive_more::derive::{Deref, DerefMut};
2525
use itertools::Itertools;
26-
use once_cell::sync::Lazy;
26+
use once_cell::sync::{Lazy, OnceCell};
2727
use parquet::{
2828
arrow::ArrowWriter,
2929
basic::Encoding,
@@ -33,6 +33,7 @@ use parquet::{
3333
},
3434
schema::types::ColumnPath,
3535
};
36+
use rayon::iter::{IntoParallelIterator, ParallelIterator};
3637
use relative_path::RelativePathBuf;
3738
use std::sync::PoisonError;
3839
use std::{
@@ -62,6 +63,7 @@ use crate::{
6263
option::Mode,
6364
parseable::{DEFAULT_TENANT, PARSEABLE},
6465
storage::{StreamType, object_storage::to_bytes, retention::Retention},
66+
sync::FLUSH_AND_CONVERT_RUNTIME,
6567
utils::time::{Minute, TimeRange},
6668
};
6769

@@ -70,6 +72,8 @@ use super::{
7072
staging::{StagingError, reader::MergedReverseRecordReader, writer::Writer},
7173
};
7274

75+
static HOSTNAME: OnceCell<String> = OnceCell::new();
76+
7377
const DISK_WRITE_BATCH_ROWS_VAR: &str = "DISK_WRITE_BATCH_ROWS";
7478
static DISK_WRITE_BATCH_ROWS: Lazy<usize> = Lazy::new(|| {
7579
if let Ok(var) = std::env::var(DISK_WRITE_BATCH_ROWS_VAR)
@@ -209,12 +213,16 @@ impl Stream {
209213
parsed_timestamp: NaiveDateTime,
210214
custom_partition_values: &HashMap<String, String>,
211215
) -> String {
212-
let mut hostname = hostname::get()
213-
.unwrap_or_else(|_| std::ffi::OsString::from(&Ulid::new().to_string()))
214-
.into_string()
215-
.unwrap_or_else(|_| Ulid::new().to_string())
216-
.matches(|c: char| c.is_alphanumeric() || c == '-' || c == '_')
217-
.collect::<String>();
216+
let mut hostname = HOSTNAME
217+
.get_or_init(|| {
218+
hostname::get()
219+
.unwrap_or_else(|_| std::ffi::OsString::from(&Ulid::new().to_string()))
220+
.into_string()
221+
.unwrap_or_else(|_| Ulid::new().to_string())
222+
.matches(|c: char| c.is_alphanumeric() || c == '-' || c == '_')
223+
.collect::<String>()
224+
})
225+
.clone();
218226

219227
if let Some(id) = &self.ingestor_id {
220228
hostname.push_str(id);
@@ -257,7 +265,7 @@ impl Stream {
257265
return vec![];
258266
};
259267

260-
//iterate through all the inprocess_ directories and collect all arrow files
268+
// iterate through all the inprocess_ directories and collect all arrow files
261269
dir.filter_map(|entry| {
262270
let path = entry.ok()?.path();
263271
if path.is_dir()
@@ -587,6 +595,7 @@ impl Stream {
587595
self.stream_name, poisoned
588596
)))
589597
})?;
598+
// why clean Writer.MemWriter?
590599
writer.mem.clear();
591600
writer.take_flushable_disk(forced)
592601
};
@@ -825,32 +834,52 @@ impl Stream {
825834
}
826835

827836
self.update_staging_metrics(&staging_files, tenant_id);
828-
for (parquet_path, arrow_files) in staging_files {
829-
let record_reader = MergedReverseRecordReader::try_new(&arrow_files);
830-
if record_reader.readers.is_empty() {
831-
continue;
832-
}
833-
let merged_schema = record_reader.merged_schema();
834-
let props = self.parquet_writer_props(&merged_schema, time_partition, custom_partition);
835-
schemas.push(merged_schema.clone());
836-
let schema = Arc::new(merged_schema);
837-
838-
let part_path = parquet_path.with_extension("part");
839-
840-
if !self.write_parquet_part_file(
841-
&part_path,
842-
record_reader,
843-
&schema,
844-
&props,
845-
time_partition,
846-
)? {
847-
continue;
848-
}
849837

850-
if let Err(e) = std::fs::rename(&part_path, &parquet_path) {
851-
error!("Couldn't rename part file: {part_path:?} -> {parquet_path:?}, error = {e}");
852-
} else {
853-
self.cleanup_arrow_files_and_dir(&arrow_files, tenant_id);
838+
let _schemas: Vec<Result<Option<Schema>, StagingError>> = staging_files.into_par_iter().map(
839+
|(parquet_path, arrow_files)| -> Result<Option<Schema>, StagingError> {
840+
let record_reader = MergedReverseRecordReader::try_new(&arrow_files);
841+
if record_reader.readers.is_empty() {
842+
Ok(None)
843+
} else {
844+
let merged_schema = record_reader.merged_schema();
845+
let props =
846+
self.parquet_writer_props(&merged_schema, time_partition, custom_partition);
847+
// schemas.push(merged_schema.clone());
848+
let schema = Arc::new(merged_schema.clone());
849+
850+
let part_path = parquet_path.with_extension("part");
851+
852+
if !self.write_parquet_part_file(
853+
&part_path,
854+
record_reader,
855+
&schema,
856+
&props,
857+
time_partition,
858+
)? {
859+
return Ok(None)
860+
}
861+
862+
if let Err(e) = std::fs::rename(&part_path, &parquet_path) {
863+
error!(
864+
"Couldn't rename part file: {part_path:?} -> {parquet_path:?}, error = {e}"
865+
);
866+
} else {
867+
self.cleanup_arrow_files_and_dir(&arrow_files, tenant_id);
868+
}
869+
Ok(Some(merged_schema))
870+
}
871+
},
872+
)
873+
.collect();
874+
875+
for res in _schemas {
876+
match res {
877+
Ok(s) => {
878+
if let Some(s) = s {
879+
schemas.push(s)
880+
}
881+
}
882+
Err(e) => return Err(e),
854883
}
855884
}
856885
if schemas.is_empty() {
@@ -880,6 +909,8 @@ impl Stream {
880909
.open(part_path)
881910
.map_err(|_| StagingError::Create)?;
882911
let mut writer = ArrowWriter::try_new(&mut part_file, schema.clone(), Some(props.clone()))?;
912+
913+
// does pruning help with query?
883914
let sort_for_metric_pruning = self.is_otel_metrics();
884915
let time_partition_field = time_partition.map_or_else(
885916
|| DEFAULT_TIMESTAMP_KEY.to_string(),
@@ -1409,20 +1440,24 @@ impl Stream {
14091440
// For regular cycles, use false to only flush non-current writers
14101441
let forced = init_signal || shutdown_signal;
14111442
self.flush(forced)?;
1412-
info!(
1413-
"Flushing stream ({}) took: {}s",
1414-
self.stream_name,
1415-
start_flush.elapsed().as_secs_f64()
1416-
);
1443+
if self.get_stream_type().eq(&StreamType::UserDefined) {
1444+
info!(
1445+
"Flushing stream ({}) took: {}s",
1446+
self.stream_name,
1447+
start_flush.elapsed().as_secs_f64()
1448+
);
1449+
}
14171450

14181451
let start_convert = Instant::now();
14191452

14201453
self.prepare_parquet(init_signal, shutdown_signal, tenant_id)?;
1421-
info!(
1422-
"Converting arrows to parquet on stream ({}) took: {}s",
1423-
self.stream_name,
1424-
start_convert.elapsed().as_secs_f64()
1425-
);
1454+
if self.get_stream_type().eq(&StreamType::UserDefined) {
1455+
info!(
1456+
"Converting arrows to parquet on stream ({}) took: {}s",
1457+
self.stream_name,
1458+
start_convert.elapsed().as_secs_f64()
1459+
);
1460+
}
14261461

14271462
Ok(())
14281463
}
@@ -1512,15 +1547,6 @@ impl Streams {
15121547
} else {
15131548
vec![]
15141549
}
1515-
1516-
// self.read()
1517-
// .expect(LOCK_EXPECT)
1518-
// .get(&tenant_id)
1519-
// .and_then(|v|v.keys())
1520-
// .map(f)
1521-
// .keys()
1522-
// .map(String::clone)
1523-
// .collect()
15241550
}
15251551

15261552
pub fn list_internal_streams(&self, tenant_id: &Option<String>) -> Vec<String> {
@@ -1553,6 +1579,7 @@ impl Streams {
15531579
vec![DEFAULT_TENANT.to_owned()]
15541580
};
15551581

1582+
let handle = FLUSH_AND_CONVERT_RUNTIME.handle();
15561583
for tenant_id in tenants {
15571584
let guard = self.read().expect(LOCK_EXPECT);
15581585
let streams: Vec<Arc<Stream>> = if let Some(tenant_streams) = guard.get(&tenant_id) {
@@ -1563,10 +1590,13 @@ impl Streams {
15631590
for stream in streams {
15641591
let tenant = tenant_id.clone();
15651592
let span = info_span!("stream_sync", stream_name = %stream.stream_name);
1566-
joinset.spawn_blocking(move || {
1567-
let _guard = span.enter();
1568-
stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant))
1569-
});
1593+
joinset.spawn_blocking_on(
1594+
move || {
1595+
let _guard = span.enter();
1596+
stream.flush_and_convert(init_signal, shutdown_signal, &Some(tenant))
1597+
},
1598+
handle,
1599+
);
15701600
}
15711601
}
15721602
}

0 commit comments

Comments
 (0)