Repository navigation
Description
Describe the bug
Filing a ticket based on a conversation in discord: https://discord.com/channels/885562378132000778/1166447479609376850/1275728622224932959
Basically, I expect that when properly configured, DataFusion would be able to sort data that doesn't fit in RAM, but instead it results in an error like
failed to optimize table: Parquet { source: General("Z-order failed while scanning data: ResourcesExhausted(\"Failed to allocate additional 2348322032 bytes for ExternalSorter[0] with 0 bytes already allocated for this reservation - 1501294808 bytes remain available for the total pool\")") }
To Reproduce
Here is a reproducer: rust_playground.tar.gz
tar xf rust_playground.tar.gz
cd rust_playground/
cargo runThe code looks like this
// how much data to sort
let row_limit = 10 * 1000;
let mem_limit = 10 * 1024 * 1024;
let print_results = false;
let pool = FairSpillPool::new(mem_limit);
let config = RuntimeConfig::new()
.with_memory_pool(Arc::new(pool))
.with_disk_manager(DiskManagerConfig::new());
let runtime_env = RuntimeEnv::new(config)?;
let builder = SessionStateBuilder::new().with_runtime_env(Arc::new(runtime_env));
let ctx = SessionContext::new_with_state(builder.build());
let generator = AccessLogGenerator::new()
.with_row_limit(row_limit)
.with_max_batch_size(100); // 100 rows per batch
// create a plan that simply sorts on the hostname
let df = ctx
.read_batches(generator)?
.sort(vec![col("host").sort(true, true)])?;
// execute the plan (it should succeed)
let results: Vec<RecordBatch> = df.collect().await?;Expected behavior
I expect the query to succeed (by spilling data to disk, etc)
Additional context
@westonpace notes #10073 may be related
Here is some of the commentary from discord:
I see four sites where an allocation failure doesn't lead to spilling:
| self.reservation.try_grow(size)? |
| self.reservation.try_resize(size)?; |
| self.reservation.try_resize(batch.get_array_memory_size())?; |
| self.reservation.try_grow(batch.get_array_memory_size())?; |
My main suspect is the fourth which is used by SortPreservingMergeStream when appending a batch, during the streaming merge of in-memory batches and spill files.
Guessed wrong. It's the second call site from the list above. Here's the relevant part of the backtrace:
0x559ea91a9cef - core::result::Result<T,E>::unwrap::h6b8b66539b551ac9
at /build/rust/tmp/build/rustc-1.80.0-src/library/core/src/result.rs:1102:23
13: 0x559ea91a9cef - datafusion_execution::memory_pool::MemoryReservation::try_grow::h6d435672ae31c4ca
at /home/claudio/Code/github.com/apache/datafusion/datafusion/execution/src/memory_pool/mod.rs:270:12
14: 0x559ea91a9cef - datafusion_execution::memory_pool::MemoryReservation::try_resize::h80744f5d7b3aa70f
at /home/claudio/Code/github.com/apache/datafusion/datafusion/execution/src/memory_pool/mod.rs:252:34
15: 0x559ea8b95ba9 - datafusion_physical_plan::sorts::sort::ExternalSorter::in_mem_sort::{{closure}}::h4686d81a505f0439
at /home/claudio/Code/github.com/apache/datafusion/datafusion/physical-plan/src/sorts/sort.rs:434:9
16: 0x559ea8b6fda0 - datafusion_physical_plan::sorts::sort::ExternalSorter::insert_batch::{{closure}}::hdd00ee915381b03a
at /home/claudio/Code/github.com/apache/datafusion/datafusion/physical-plan/src/sorts/sort.rs:289:32
17: 0x559ea8b6fda0 - <datafusion_physical_plan::sorts::sort::SortExec as datafusion_physical_plan::execution_plan::ExecutionPlan>::execute::{{closure}}::h256a01c17fe43713
at /home/claudio/Code/github.com/apache/datafusion/datafusion/physical-plan/src/sorts/sort.rs:910:52
I got this same backtrace in three worker threads at once:
thread 'tokio-runtime-worker' panicked at /home/claudio/Code/github.com/apache/datafusion/datafusion/execution/src/memory_pool/mod.rs:270:37:
called `Result::unwrap()` on an `Err` value: ResourcesExhausted("Failed to allocate additional 811511472 bytes for ExternalSorter[6] with 0 bytes already allocated for this reservation - 612881930 bytes remain available for the total pool")
stack backtrace:
thread 'tokio-runtime-worker' panicked at /home/claudio/Code/github.com/apache/datafusion/datafusion/execution/src/memory_pool/mod.rs:270:37:
called `Result::unwrap()` on an `Err` value: ResourcesExhausted("Failed to allocate additional 767287128 bytes for ExternalSorter[0] with 0 bytes already allocated for this reservation - 597992699 bytes remain available for the total pool")
thread 'tokio-runtime-worker' panicked at /home/claudio/Code/github.com/apache/datafusion/datafusion/execution/src/memory_pool/mod.rs:270:37:
called `Result::unwrap()` on an `Err` value: ResourcesExhausted("Failed to allocate additional 734693152 bytes for ExternalSorter[3] with 0 bytes already allocated for this reservation - 627555485 bytes remain available for the total pool")
Memory usage steadily grows to 1GiB until the crash but the pool size is 20 GiB
I've been able to avoid the allocation failures during sort with spill-to-disk by replacing try_grow with grow when we can't fall back to spilling. Specifically, I made that change in these places (all of which triggered allocation failures during earlier attempts):
| self.reservation.try_resize(size)?; |
| self.merge_reservation.try_resize(size)?; |
| self.reservation.try_grow(batch.get_array_memory_size())?; |
The first two happen during in-memory sorting, for the spillable and unspillable reservations, respectively. The second one happens during the merge phase.
Memory slightly overshot the 20GiB pool size but stayed close initially. Eventually it grew to 30GiB (during the merge phase, maybe?).
I couldn't get the sort to complete because the pod froze when filesystem usage grew to 1TB. That seems a bit much for sorting a 100GiB partition, I'll be looking into that next.


Activity
[-]External sorting not working for string columns[/-][+]External sorting not working for (maybe only for string columns??)[/+]cjolowicz commentedon Aug 30, 2024
This is probably unrelated, but there's something that puzzles me about the fair spill pool logic. The
try_growfunction checks that the consumer isn't allocating more than its fair share. So it needs to determine how much memory the consumer has already allocated. But it only looks at the reservation that's passed, not at all reservations of that consumer.Consider a single spillable consumer that allocates the entire pool. It shouldn't be able to grow the resulting reservation. But if it splits the reservation into two, it can now grow it - even when the other reservation remains allocated.
Here's a failing test showing this:
https://github.com/apache/datafusion/pull/12170/files
Let me know if I'm misunderstanding something here.
cjolowicz commentedon Aug 30, 2024
I could reproduce the pod freezing up at 1TB disk usage. This happened within a minute of spawning 500 threads. All threads were blocked on a mutex (
futex_wait) except one inepollwith an inode fd. Maybe deadlock at the start of the merge phase?It actually makes sense because spill files use Apache IPC format without compression, while the partition uses Parquet files with Snappy compression.
Fix allocation failures during sorting with spill-to-disk
Fix allocation failures during sorting with spill-to-disk
Fix allocation failures during sorting with spill-to-disk
Fix allocation failures during sorting with spill-to-disk
16pierre commentedon Jan 5, 2025
Also hitting this bug, is there any update on a fix ?
alamb commentedon Jan 5, 2025
I don't know of anyone working explicitly working to make external sorting better. Some recent work maybe would make it better
record_batch.get_array_memory_size()in spilling operators #13430But realistically I think there is significant additional room for improvement. Any help welcome
Omega359 commentedon Jan 6, 2025
I don't believe that adding an option for compression in DiskManager would be all that difficult - the IPCWriter and reader already has support for LZ4. It would of course incur the cost of compression/decompression but would likely result in 50+% space savings on disk. It's a nice to have though - I think of it more as a band-aid than a solution.
2010YOUY01 commentedon Jan 9, 2025
For the given reproducer, I got the error
Configure datafusion.execution.sort_spill_reservation_bytes to 1MB can let it run successfully. ( I don't know whether the parquet related error message is caused by the same issue)
Details
Reasons:
sort_spill_reservation_bytesworks: For example there is a sort query with memory limit 10MB, andsort_spill_reservation_bytesis 1M, it will accumulate batches in memory until it reaches 9M, then do in-memory sort. The sort will do in-place sort for all individual batches, and finally do a sort-preserving merge to get the final one sorted run. 1MB is reserved for internal data structures of the merging phase.Thoughts:
Note I'm not 100% sure about the implementation detail, if that's the case, I think we can get rid of this configuration option, and figure out
sort_spill_reservation_bytesaccording to the available memory budget, to prevent similar failures.westonpace commentedon Jan 9, 2025
@2010YOUY01 Your solution does not work for me. I did play around with
sort_spill_reservation_byteswhen I originally encountered this but couldn't find a way to get it to work. Here is a reproducer that uses a 100MB memory budget and fails for all values ofsort_spill_reservation_bytesbetween 1 and 100 MB. It might be that I am configuring things or using something improperly however.xuchen-plus commentedon Jan 23, 2025
We have also encountered this issue. After some debugging (by adding debug logs before every call to
try_grow), it seems that the memory counting of batches returned bySortPreservingMergeStreamis wrong.I set
sort_spill_reservation_bytesto 8MB, andmemory_limitto 256MB.After insert some batches, the pool is full and start to in-memory sort, at this moment the pool usage is:
GreedyMemoryPool { pool_size: 268435456, used: 267618380 }.And the sort and collect at
datafusion/datafusion/physical-plan/src/sorts/sort.rs
Lines 440 to 443 in 0228bee
finished without error. After sort, all memory are released.
Then the following code count the sorted batches' memory:
datafusion/datafusion/physical-plan/src/sorts/sort.rs
Lines 445 to 449 in 0228bee
And this size is: 698840964, which exceeds the memory limit and the following
self.reservation.try_resize(size)?;failed:Not sure why the sorted batches' memory is over 2.6x than the batches before sort.
xuchen-plus commentedon Feb 7, 2025
Some findings so far:
My test was reading a parquet file with mostly string columns and sort by one column. It seems that enabling string view would cause
get_record_batch_memory_sizeproduce overlarge values since multiple string view columns from multiple batches may share the same buffer.a) The memory counting at
insert_batchmay be larger than actual physical memory usage:datafusion/datafusion/physical-plan/src/sorts/sort.rs
Lines 301 to 307 in bab0f54
b) The memory counting is also too large after
in_mem_sort:datafusion/datafusion/physical-plan/src/sorts/sort.rs
Lines 446 to 450 in bab0f54
I disabled string view for parquet reader, and increase the value of
sort_spill_reservation_bytes, my test can be finished successfully, during which the disk spill works correctly. Some observationsa)
sort_spill_reservation_bytesshould be set to a big enough value to hold:RowsforSortPreservingMergeStream.b) The memory consumption in
datafusion/datafusion/physical-plan/src/sorts/sort.rs
Lines 439 to 444 in bab0f54
is not counted in the memory limit. During the
collect, the sorted batches will beinterleaved to produce merged batches byBatchBuilder. At this time both the sorted batches and the interleaved batches exist in memory and the memory consumption may be doubled.xuchen-plus commentedon Feb 8, 2025
This reproducer can also run successfully by disabling string view via
session_config.options_mut().execution.parquet.schema_force_view_types = false;and setsort_spill_reservation_bytesto 16MB.zhuqi-lucas commentedon Feb 8, 2025
Thank you @xuchen-plus , so we disable the stringView it will succeed when we increase the sort_spill_reservation_bytes. If so we need to investigate how to fix stringView case?
zhuqi-lucas commentedon Feb 8, 2025
It still failed for me for the setting, the reproduce code:
And when disable stringView, a new finding is the sorted data is smaller than the original data:
Failed with error:
xuchen-plus commentedon Feb 12, 2025
@zhuqi-lucas Sorry. I actually adjusted
sort_spill_reservation_bytesto 32MB and the above test passed. Whensort_spill_reservation_bytesis 16MB the test would failed to reserve extra memory during sort.zhuqi-lucas commentedon Feb 13, 2025
Thanks @xuchen-plus , unluckily after change to 32MB for sort_spill_reservation_bytes, it still failed for me, i am not sure which i am missing.
xuchen-plus commentedon Feb 13, 2025
@zhuqi-lucas I was using main branch at commit id
55730dcecebe5475af40b9e8a5c4805a73c31b11. I tested released version 45.0.0 and also failed with the same error like yours.2010YOUY01 commentedon Feb 13, 2025
I haven't followed the discussion yet, but to make sure the external execution results are reproducible,
datafusion.execution.target_partitionsshould also be set to a constant number. The default value is the number of CPU cores on the running machine, which can varyKontinuation commentedon Feb 13, 2025
I have also encountered the same problem with string views.
DataFusion uses
interleavefunction to produce merged batches, andinterleavetends to produce batches that has super large size due to apache/arrow-rs#6779. Although it simply references to the data buffers of interleaved arrays so it does not actually take extra memory space, but it makes the result ofget_record_batch_memory_size(batch)orbatch.get_array_memory_size()super large, increasing the chance of getting memory reservation failures.When spilling happens, these interleaved arrays will be serialized using Arrow IPC and produces very large binaries. When we read them back in spill-read phase, we have to allocate super large buffers for these arrays, which makes things much worse.
alamb commentedon Feb 13, 2025
I think the fix for apache/arrow-rs#6779 is in DataFusion 45 -- does this still happen?
Kontinuation commentedon Feb 13, 2025
I am using the latest main branch of DataFusion (commit 28856e1) and the problem still happens.
Edit: this can be easily reproduced using sort-tpch Q3. The following error message is obtained from a
dfbench sort-tpchrun with 200MB memory limit and 100MBsort_spill_reservation_bytes:We see 100MB in-memory batches got sorted and became 2.1GB.
Backtrace:
rluvaton commentedon Sep 13, 2025
Hey, could you please check if this still happening on latest main? I've added the following pr which should fix this problem
alamb commentedon Sep 16, 2025
I just retried with DataFusion 50 and unfortuately the repdoducer it still fails the same way:
However, when I cranked the target partitions down to 2 and the memory limit to 50MB it did succeed and thus I believe this issue is now closed (there is some fixed amount of memory required per target partition but once that is satisfied, then the repo will work
Updated
alamb commentedon Sep 16, 2025
Thank you (again) @rluvaton