-
Notifications
You must be signed in to change notification settings - Fork 436
[CELEBORN-2299] Fix memory leak and severe FGC in CelebornSortBasedPusher for hi… #3646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,18 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.celeborn.common.unsafe.Platform; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.celeborn.common.util.Utils; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Sort-based pusher for MapReduce shuffle data to Celeborn. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This implementation uses primitive int arrays to store record metadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (offsets, key lengths, value lengths) during data collection to minimize | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * object allocation. During flush, temporary Record objects are created | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * for sorting and immediately garbage collected in Young Gen. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * To prevent memory pressure during sorting, the implementation triggers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * early spill when record count exceeds a threshold (5M records by default), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ensuring temporary objects fit within Young Gen capacity. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class CelebornSortBasedPusher<K, V> extends OutputStream { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Logger logger = LoggerFactory.getLogger(CelebornSortBasedPusher.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final int mapId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,7 +60,7 @@ public class CelebornSortBasedPusher<K, V> extends OutputStream { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final AtomicReference<Exception> exception = new AtomicReference<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Counters.Counter mapOutputByteCounter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Counters.Counter mapOutputRecordCounter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Map<Integer, List<SerializedKV>> partitionedKVs; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final Map<Integer, KVBufferInfo> partitionedKVBuffers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private int writePos; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private byte[] serializedKV; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final int maxPushDataSize; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,7 +91,7 @@ public CelebornSortBasedPusher( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.mapOutputRecordCounter = mapOutputRecordCounter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.comparator = comparator; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.shuffleClient = shuffleClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partitionedKVs = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partitionedKVBuffers = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| serializedKV = new byte[maxIOBufferSize]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxPushDataSize = (int) celebornConf.clientMrMaxPushData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -102,6 +114,7 @@ public CelebornSortBasedPusher( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void insert(K key, V value, int partition) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if we should spill based on buffer size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (writePos >= spillIOBufferSize) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // needs to sort and flush data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (logger.isDebugEnabled()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -114,6 +127,22 @@ public void insert(K key, V value, int partition) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sortKVs(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendKVAndUpdateWritePos(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Additional check: limit total record count to avoid memory pressure during sort | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If total records exceed safe threshold, force an early spill | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int totalRecords = getTotalRecordCount(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final int MAX_RECORDS_BEFORE_SPILL = 5_000_000; // 5M records = ~120MB temporary objects | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (totalRecords >= MAX_RECORDS_BEFORE_SPILL && writePos > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (logger.isDebugEnabled()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Record count {} exceeds safe threshold {}, forcing early spill", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalRecords, MAX_RECORDS_BEFORE_SPILL); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final int MAX_RECORDS_BEFORE_SPILL = 5_000_000; // 5M records = ~120MB temporary objects | |
| if (totalRecords >= MAX_RECORDS_BEFORE_SPILL && writePos > 0) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug( | |
| "Record count {} exceeds safe threshold {}, forcing early spill", | |
| totalRecords, MAX_RECORDS_BEFORE_SPILL); | |
| if (totalRecords >= MAX_SORT_RECORDS && writePos > 0) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug( | |
| "Record count {} exceeds safe threshold {}, forcing early spill", | |
| totalRecords, MAX_SORT_RECORDS); |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling getTotalRecordCount() on every insert() makes each record insertion O(#partitions) due to iterating partitionedKVBuffers.values(). This can add significant overhead at tens of millions of records. Consider maintaining a totalRecordCount field that increments in insertRecordInternal(...) (after bufferInfo.add(...)) and resets when spilling/clearing (e.g., in sendKVAndUpdateWritePos() and close()).
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This batching logic sends a batch after partitionKVTotalLen has already exceeded maxPushDataSize, which can produce push payloads larger than the configured limit (contradicting the comment about limiting max size). To actually enforce the limit, structure the loop so that if adding the next record would exceed the threshold, you send the previous batch first, then start a new batch with the current record. Also handle the edge case where a single record is larger than maxPushDataSize (so you avoid an empty-batch send or an infinite loop).
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This batching logic sends a batch after partitionKVTotalLen has already exceeded maxPushDataSize, which can produce push payloads larger than the configured limit (contradicting the comment about limiting max size). To actually enforce the limit, structure the loop so that if adding the next record would exceed the threshold, you send the previous batch first, then start a new batch with the current record. Also handle the edge case where a single record is larger than maxPushDataSize (so you avoid an empty-batch send or an infinite loop).
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 5M threshold is duplicated (MAX_RECORDS_BEFORE_SPILL and MAX_SORT_RECORDS) and could drift over time. Consider defining a single class-level constant (or deriving it from configuration) and using it consistently for both early-spill and sorting limits. This also makes it easier to document/adjust the policy in one place.
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sortKVs() calls sendPartialBuffer(...) when bufferInfo.count > MAX_SORT_RECORDS, but sendPartialBuffer currently always throws UnsupportedOperationException. That makes large buffers fail at runtime rather than spilling safely. If the intent is to rely on early-spill to guarantee count <= MAX_SORT_RECORDS, consider removing this branch and enforcing the invariant (e.g., via a clear exception that should never happen). Otherwise, implement partial sending by integrating batch boundaries into sendKVAndUpdateWritePos() (so the batching logic that exists there is reused rather than throwing).
| // If too many records, split into batches | |
| if (bufferInfo.count > MAX_SORT_RECORDS) { | |
| // Sort and flush each batch immediately | |
| int remaining = bufferInfo.count; | |
| int start = 0; | |
| while (remaining > 0) { | |
| int batchSize = Math.min(remaining, MAX_SORT_RECORDS); | |
| int end = start + batchSize; | |
| // Sort this batch | |
| sortBatch(bufferInfo, start, end); | |
| // Send this batch immediately to free memory | |
| sendPartialBuffer(bufferInfo, start, batchSize); | |
| start = end; | |
| remaining -= batchSize; | |
| } | |
| // After all batches sent, clear the buffer | |
| bufferInfo.clear(); | |
| } else { | |
| // Full sort (single batch) | |
| sortBatch(bufferInfo, 0, bufferInfo.count); | |
| } | |
| // Enforce invariant that early spilling keeps record count within bounds. | |
| if (bufferInfo.count > MAX_SORT_RECORDS) { | |
| throw new IllegalStateException( | |
| "KVBufferInfo.count (" + bufferInfo.count | |
| + ") exceeds MAX_SORT_RECORDS (" + MAX_SORT_RECORDS | |
| + "). Early spill should prevent this; partial buffer sending " | |
| + "is not supported in sortKVs()."); | |
| } | |
| // Full sort (single batch) | |
| sortBatch(bufferInfo, 0, bufferInfo.count); |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description states the fix is fully backward compatible and requires no configuration changes, but this code path explicitly instructs users to change mapreduce.task.io.sort.mb or heap settings and fails the task. Either remove/avoid this user-facing failure mode (preferred, consistent with the PR description) or update the PR description to reflect the new behavior and conditions under which it can occur.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling
getTotalRecordCount()on everyinsert()makes each record insertion O(#partitions) due to iteratingpartitionedKVBuffers.values(). This can add significant overhead at tens of millions of records. Consider maintaining atotalRecordCountfield that increments ininsertRecordInternal(...)(afterbufferInfo.add(...)) and resets when spilling/clearing (e.g., insendKVAndUpdateWritePos()andclose()).