[Subscription] Reduce consensus WAL replay and ACK contention - #18402
[Subscription] Reduce consensus WAL replay and ACK contention#18402Caideyipi wants to merge 2 commits into
Conversation
| if (advanceLocalCursorIfPresent(request)) { | ||
| // The pending path advances independently of the WAL iterator. Defer realignment until the | ||
| // next round so the current pending batch can finish without repeatedly reopening the WAL. | ||
| requestSubscriptionWalReset(nextExpectedSearchIndex.get(), expectedSeekGeneration); |
There was a problem hiding this comment.
Could we clear or update this deferred reset when fillGapFromWAL realigns the iterator? For example, with pending requests [1, 5], processing 1 records a reset target of 2. Processing 5 then fills the WAL gap from 2 and may advance both the iterator and nextExpectedSearchIndex to 6. Since request 5 is now before the local cursor, this method is not called again, so the pending target remains 2. The next prefetch round applies that stale target and rereads/skips/decompresses WAL entries 2 through 5, recreating the replay cost this PR is intended to remove. More generally, applying this reset at the start of every following round also reconstructs ProgressWALIterator (including listing and sorting retained WAL files) once per steady-state pending batch even when that round never needs WAL. Please consider consuming the marker only immediately before entering the WAL path, and clearing/updating it when fillGapFromWAL has already aligned the iterator. A regression test with pending [1, 5] and WAL [2..5] would cover the stale-target case.
Description
This PR removes repeated DataNode-side WAL replay work and reduces the ACK lock contention amplified by that work. It only changes IoTDB/DataNode code; no consumer code is changed.
Bottleneck shown by the flame graphs
The four CPU/wall profiles show one server-side root cause and its client-side amplification.
On the DataNode CPU profile:
DataInputStream.readFullyaccounts for about 60.4%.LZ4_decompress_safeself time is about 32.3%.openReaderAtIndexis about 33.5% cumulative andskipEntriesis about 16.2%.The bottleneck is therefore repeated WAL replay work, rather than raw disk throughput.
tryCatchUpFromWAL()previously reset the iterator before every batch. A precedinghasNext()could already have parsed and cachednextReady, but the next round closed that iterator, discarded the cached request, reopened the WAL atnextExpectedSearchIndex, and repeated reader lookup, skipping, reads, and LZ4 decompression.Even after keeping the iterator alive, every round still listed, parsed, filtered, and sorted all retained WAL files. Live-WAL reopen also copied the same metadata twice: once to check whether new entries existed and again while opening the reader.
On the DataNode wall profile, the expensive prefetch round holds the queue read lock. A late/missing ACK then waits for the queue write lock for roughly 86% of its RPC wall time.
On the consumer wall profile, the auto-commit RPC waits for that ACK response while holding the consumer
SynchronizedHandlermonitor. A concurrent business thread callingcommitSyncwaits for the same monitor for roughly 48% of consumer wall time. Enabling auto-commit together with manualcommitSyncamplifies the server stall, but the server-side WAL replay and ACK lock contention are the root causes addressed here.The consumer CPU profile also shows
Tablet.deserializeat about 49% andTablet.readValuesFromBufferat about 40%. Those are separate consumer-side costs and are intentionally outside this DataNode-only PR.Implemented low-risk optimizations
ProgressWALIteratorand itshasNext()cache alive across normal WAL catch-up rounds instead of reopening it at the current search index for every batch.WALMetaDatasnapshot when reopening the reader, avoiding a second full metadata copy for the same reopen attempt.hasNext()on the stale iterator. The next prefetch round applies the reset under the queue lock.Performance test
ProgressWALIteratorTest#testIteratorReusePerformanceis disabled by default and can be enabled manually:mvn test -pl iotdb-core/datanode \ -Dtest=ProgressWALIteratorTest#testIteratorReusePerformance \ -Diotdb.test.subscription.performance=trueOptional parameters:
-Diotdb.test.subscription.performance.entries=<count>-Diotdb.test.subscription.performance.batch-size=<count>With 4,096 entries and batches of 64 on the same generated WAL:
reopenmodels the original per-batch reopen/skip/decompress behavior.refreshEachBatchkeeps the iterator but still rescans retained WAL files on every batch.reuseis the final path in this PR. The benchmark isolates these WAL iterator costs from consumer RPC and tablet deserialization.High-risk directions intentionally not implemented
skipToEntryIndex(0): bypassing the current call changes when the first WAL segment is read and validated, which can alter corrupted-WAL and near-live retry behavior.IoTConsensusRequestcan retain buffers beyond the reader call, so safe reuse requires a new ownership/lifetime contract.These directions may provide additional gains, but they carry correctness or memory-lifecycle risk disproportionate to this focused optimization and should be handled separately with dedicated stress/compatibility testing.
Tests
ConsensusPrefetchingQueueTest#testLateAckDoesNotWaitForPrefetchReadLockConsensusPrefetchingQueueTest#testWalCatchUpReusesIteratorAcrossRoundsConsensusPrefetchingQueueTest#testReadableWalIteratorSkipsFileListRefreshConsensusPrefetchingQueueTest#testWalRollDoesNotRefreshNewIteratorTwiceConsensusPrefetchingQueueTest#testPendingCursorAdvanceDefersWalIteratorRealignmentProgressWALIteratorTest#testLiveWalReopenReusesMetadataSnapshotConsensusPrefetchingQueueTest: 25 passedProgressWALIteratorTest: 9 discovered, 0 failures, 0 errors, 1 performance test skipped by defaultThis PR has:
Key changed/added classes
ConsensusPrefetchingQueueProgressWALIteratorConsensusPrefetchingQueueTestProgressWALIteratorTest