perf: reduce processing pool queue-lock contention with a sharded task queue (druid.processing.numThreadPools) - #19938
Draft
rbankar7 wants to merge 3 commits into
Draft
perf: reduce processing pool queue-lock contention with a sharded task queue (druid.processing.numThreadPools)#19938rbankar7 wants to merge 3 commits into
rbankar7 wants to merge 3 commits into
Conversation
….processing.numThreadPools)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #19937.
Description
Under very high task rates, the processing pool's single task queue becomes a
lock-contention bottleneck. Every per-segment scan/merge task is submitted to a
single
PrioritizedExecutorServicebacked by onePriorityBlockingQueue, which—being a binary heap— is guarded by a single
ReentrantLockfor bothputand
take(unlikeLinkedBlockingQueue's two-lock design). So every task paysthe lock twice, and all
numThreadsworkers plus all producers serialize on onelock. On many-core nodes serving many small segments at high QPS, contention on
this one lock (AQS park/unpark + cache-line bouncing on the AQS
stateword)dominates, showing up as high query wait time while CPU stays low — and it gets
worse as the node's thread count grows.
This PR adds an option to split the processing pool into N independent pools
("shards"), each with its own
PriorityBlockingQueueand its own lock, so eachqueue lock only sees ~1/N of the submit/take traffic and ~1/N of the contending
threads. It is fully opt-in and defaults to the existing single-pool behavior.
See the issue (#19937) for the motivation and profiling background.
Added
druid.processing.numThreadPools(default1)druid.processing.numThreadsis split as evenly as possible across the pools(remainder to the first pools), so total thread count and processing-buffer
sizing are unchanged (direct-memory accounting is unaffected). The value is
clamped to
[1, numThreads](you can't have more pools than threads).numThreadPools = 1preserves the current behavior exactly.Added
ShardedPrioritizedExecutorServiceA composite
ListeningExecutorServiceholding N ordinaryPrioritizedExecutorServiceinstances. Per-task work (execute/submit) isrouted to a shard chosen with
ThreadLocalRandom— no shared counter, so therouter adds no contention of its own — and the chosen shard does all the
priority wrapping and ordering. Lifecycle calls fan out to every shard;
getQueueSize()/getActiveTasks()are summed across shards.Design choice — routing: random routing was chosen over round-robin (which
needs a shared atomic counter, reintroducing a contended cache line) .
For a homogeneous, high-rate
task stream, random spreads load evenly enough that per-shard queue depths stay
balanced.
Trade-off: priority ordering becomes per-shard rather than global — a
high-priority task in one shard does not preempt work queued in another. For the
high-throughput, effectively-single-priority per-segment workload this pool
serves, that is an acceptable exchange; deployments needing strict global
priority ordering should keep the default (
numThreadPools = 1).Added
ProcessingPoolStatsinterfaceExtracted a small interface (
getQueueSize()/getActiveTasks()) implementedby both
PrioritizedExecutorServiceandShardedPrioritizedExecutorService.MetricsEmittingQueryProcessingPoolnow emitssegment/scan/pendingandsegment/scan/activeviainstanceof ProcessingPoolStatsinstead of a concreteclass, so those metrics keep working for both pool types (summed across shards
for the sharded case) — no metric names change.
Wiring
DruidProcessingModule.createProcessingExecutorPoolselects the shardedimplementation only when
numThreadPools > 1; otherwise it uses the existingPrioritizedExecutorServiceunchanged.Release note
Added
druid.processing.numThreads Pools(default1), which splits theprocessing pool into that many independent thread pools/queues, each with its own
lock. Increasing it relieves contention on the single processing-queue lock under
very high task rates (e.g. Historicals scanning many small segments at high QPS),
at the cost of per-pool rather than global task-priority ordering. The default
(
1) is identical to previous behavior.Key changed/added classes in this PR
ProcessingPoolStats(new) — capability interface for pool queue/active-task stats.ShardedPrioritizedExecutorService(new) — composite of NPrioritizedExecutorServiceshards with random routing.PrioritizedExecutorService— implementsProcessingPoolStats; extracted a sharedmakeThreadPoolExecutorhelper; added a startup log line.DruidProcessingConfig— newnumThreadPoolsproperty (clamped to[1, numThreads]) + getter; backward-compatible constructor retained.MetricsEmittingQueryProcessingPool— emits scan metrics viaProcessingPoolStats.DruidProcessingModule— selects the sharded pool whennumThreadPools > 1.This PR has: