optimize: park merge send thread when idle to avoid 1ms polling CPU spin - #8205
optimize: park merge send thread when idle to avoid 1ms polling CPU spin#8205CuriousLinYu wants to merge 5 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 2.x #8205 +/- ##
============================================
+ Coverage 73.26% 73.28% +0.01%
Complexity 1146 1146
============================================
Files 1153 1153
Lines 42348 42356 +8
Branches 5061 5064 +3
============================================
+ Hits 31028 31040 +12
+ Misses 8832 8830 -2
+ Partials 2488 2486 -2
🚀 New features to boost your workflow:
|
| // signalling (see sendSyncRequest), so no wake-up can be lost. | ||
| while (isBasketEmpty()) { | ||
| isSending = false; | ||
| mergeCondition.await(); |
There was a problem hiding this comment.
With this approach, whenever a message arrives, the processing thread waits for up to another 1 ms. Why is this additional delay necessary?
With the implementation you’re currently using, consider the case where only a single message arrives: it wakes up the thread, but the thread then waits for another 1 ms before sending the message. This effectively adds about 1 ms to the request latency.
I don’t think an await of 1 ms alone should cause such high CPU usage. Could the high CPU utilization be related to Arthas being enabled? Given the performance of modern CPUs, 1,000 wake-ups per second that do essentially no work should not normally be enough to consume around 30% CPU.
There was a problem hiding this comment.
Thanks for the review — both points were spot on. I've removed the post-wakeup await(1ms) entirely: the thread now drains immediately after being signalled. Measured locally: first-message drain latency drops from ~1.4-15.3ms (with the 1ms await) to ~0.09-0.21ms, and batching does not regress — avg batch size is 50 vs 21.3 on the original under a 4x25 concurrent burst. The full measurement table is in the PR description.
On the 30% CPU number — fair point that Arthas sampling and container CPU quota matter. What the issue confirms is the thread dump: the rpcMergeMessageSend_*_1 threads sit in TIMED_WAITING while burning CPU. Locally (ThreadMXBean, 10s window) the original implementation consumes a steady 31ms of thread CPU time per 10s at idle; this PR takes it to 0. The absolute ratio is environment-dependent, but the idle wake-up cost is real and now gone.
…ms await The post-wakeup 1ms await added a >=1ms latency floor to the first message after idle (measured 1.4-15.3ms) without improving batching (avg batch 50 vs 21 on the original). Idle CPU stays 0.000% (parked). See PR apache#8205 discussion.
|
Hi @funky-eyes, thanks for the review — both points were addressed in Two process-side items are still blocking the merge:
Happy to make any further adjustments. |
|
Pushed a merge with the latest On the previous spotless failure: I could not reproduce it locally. Could someone approve the run so we can see a real result? If it comes back with a specific file, I'll fix it immediately. |
Fixes #6041
Problem
MergedSendRunnablepolls every 1ms (MAX_MERGE_SEND_MILLS = 1) even when all baskets are empty. With no traffic, the thread wakes up 1000 times per second, each cycle acquiringmergeLock, iteratingbasketMapand going back to sleep. Issue #6041 reported this at ~30% CPU per thread (thread dump shows therpcMergeMessageSend_*_1threads inTIMED_WAITING), and it still reproduces on 2.x (RM client enables batch send by default:DEFAULT_ENABLE_RM_CLIENT_BATCH_SEND_REQUEST = true).Solution
When
basketMapis empty, the merge thread parks onmergeCondition.await()(no timeout) instead of a 1ms timed wait. Producers already offer to the basket and then signal (sendSyncRequest), so the parked thread is woken up as soon as a message arrives and drains the basket immediately.About the removed 1ms timed wait:
AbstractRpcRemotingClient; see the history ofAbstractNettyRemotingClient.java, e.g. PR refactor TmRpcClient & RmClient for common use. #1105 and PR feature: add single send request for client. #1966) —sendSyncRequestalways signals when the merge thread is not sending.sendSyncRequestis enough to wake the thread, so a timed wait is not required for correctness.Race safety
isBasketEmpty()is evaluated andawait()is entered while holdingmergeLock.mergeLockand signalling (unchanged code), so a message can never sit in the basket with the merge thread parked indefinitely: either the producer's signal wakes the parked thread, or the producer seesisSending == trueand the thread is already in the send loop which drains the basket on the next iteration.Condition.awaitreleases the lock atomically), and the empty-check is re-evaluated inside thewhileloop.Measurements
Measured locally with an instrumented test client (basket-level offer/poll instrumentation, no network needed) across three variants. Thread CPU time via
ThreadMXBean.getThreadCpuTimeover a 10s idle window; batch sizes over 10 rounds of a 4x25 concurrent burst.The absolute idle-CPU ratio depends on the machine and CPU quota (issue #6041 measured ~30% in a constrained container). The relevant comparison is original > 0 vs this PR = 0. The "park + 1ms await" variant shows the >=1ms latency floor that this change removes, while batching does not regress vs the original.
Tests
testMergedSendRunnableIdleWaitState: asserts therpcMergeMessageSendthread is NOT inTIMED_WAITINGafter 300ms idle, then submits a request and verifies the basket is drained.NettyRemotingClientBehaviorTest: 69 tests, 0 failures.