Batch AdminClient config RPCs in ReplicationThrottleHelper - #2367
Conversation
setThrottles() and clearThrottles() previously made sequential, blocking AdminClient RPCs — one describeConfigs + one incrementalAlterConfigs per broker, then per topic. For a 250-broker cluster with 69 topics, this produced 600+ synchronous RPCs taking 30+ minutes before any data movement began. Batch all describeConfigs and incrementalAlterConfigs calls to use multi-resource maps. The AdminClient API already supports this — each call now passes the full set of resources instead of Collections.singletonMap/singletonList with one resource. Design decisions: - Broker configs use .all() (atomic) since brokers always exist - Topic configs use .values() (per-resource futures) since topics can be deleted mid-operation - waitForBatchConfigs uses .values() for verification with per-resource error handling - Added null guard in setThrottles matching clearThrottles - Added WARN logging for transient verification failures Reduces throttle setup from ~30 minutes to seconds for large clusters.
- Move TimeoutException into inner per-resource catch in waitForBatchConfigs so one timeout only skips that resource, not the entire batch - Re-interrupt thread on InterruptedException - Add Javadoc explaining .all() vs .values() asymmetry - Add testWaitForBatchConfigs cases for ExecutionException and TimeoutException during verification - Add testSetThrottleOnMixedTopicExistence: exercises batch with both existing and non-existing topics in one call
|
It's interesting that we'd also seen this ~30min waiting before movements begin, but we didn't know the reason. This should be a very nice and practical fix. (I'm not contributor but I might test this in our environment and give feedback for it. Because these open PRs looks really practical to our org, I've started testing them one by one and pick the good ones to use) |
Thanks @imans777 for the comment and working on testing. appreciated |
…hunked requests Address review findings on the batching change: - waitForBatchConfigs no longer skips verification on arbitrary per-resource failures. A topic is dropped from verification only after confirming (via listTopics) that it was deleted; any other failure (transient error, broker read failure) keeps the resource pending and retries, so an unverified throttle can never pass silently. The failure message now names the unverified resources and their expected configs. - clearThrottles reads broker configs via per-broker futures (values()) instead of all(), so a broker that became unreachable mid-execution only skips throttle removal for itself instead of leaving throttles behind on every other broker. - Resolve non-existent topics with a single lazily-fetched listTopics per batch instead of one listTopics RPC per failed topic. - Chunk describeConfigs/incrementalAlterConfigs requests at 500 resources per call. Topic config requests are routed to a single broker, so unbounded batches could produce oversized requests on clusters with many topics. - Remove the now-unused single-resource methods (changeTopicConfigs, changeBrokerConfigs, waitForConfigs, getEntityConfigs); tests apply fixture configs through the admin client directly and reuse waitForBatchConfigs. Test updates: verification-failure cases now cover deleted-topic skip, transient-failure retry, and persistent-broker-failure exhaustion; clear-path mocks use per-broker futures.
|
Thanks for pulling these together — I hadn't seen all of them, and it's encouraging that several people independently hit the same bottleneck. I went through all three carefully. #2214 and #2304 take a smart approach at a different layer: they optimize when throttles are set and cleared in the Executor (once per broker / once per execution), which cuts down how often the helper is invoked. This PR works at the layer below — batching the RPCs inside #2305 is very close in spirit to this PR — same goal, same file — and it got several important things right, including bulk verification and removing the old single-resource methods. The main differences in this PR are around failure handling:
I also took your "best of 4" suggestion seriously and pushed 5195d1d to close the remaining gaps, borrowing what the other PRs did well:
All 11 tests (unit + embedded-Kafka integration) pass and checkstyle is clean. And to be clear — if the maintainers prefer #2305 as the base, I'd be glad to help review it or port the per-resource verification handling over there instead; the goal is getting the fix merged, whichever vehicle it takes. Since you mentioned you're testing these branches in your environment, feedback from a real cluster run would be hugely appreciated — happy to fix anything you hit. |
|
Real-cluster test results Following up with results from running this branch (through 5195d1d) on a real production cluster (hundreds of brokers, ~100K partitions), deployed via Strimzi with the patched cruise-control jar. Triggered a full rebalance with tens of thousands of replica movements (tens of TB of data to move). Timeline from the trigger:
Throttle setup for the whole cluster took ~23 seconds, and nearly all of that is the two fixed 10-second verification backoff sleeps (one for broker configs, one for topic configs — the first The skip logic and throttle clearing also behave as intended across execution batches — subsequent batches only alter brokers whose throttle rate isn't already set, and no verification failures or errors appeared during execution. Happy to share more details if useful. |
|
Updated the description to follow the PR template format. |
There was a problem hiding this comment.
Pull request overview
This PR refactors ReplicationThrottleHelper to batch Kafka AdminClient config RPCs (describe/alter/verify) across brokers and topics, reducing the number of synchronous requests and improving throttle set/clear latency on large clusters.
Changes:
- Batch broker and topic throttle config reads/writes using multi-resource AdminClient calls (with chunking support for large request sets).
- Replace per-resource verification loops with a single batched polling loop (
waitForBatchConfigs) that handles per-resource failures. - Update and expand
ReplicationThrottleHelperTestto mock/validate batched behaviors and additional verification edge cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/executor/ReplicationThrottleHelper.java | Implements batched describe/alter/verify flows for broker/topic throttles and introduces chunking + batched verification helper logic. |
| cruise-control/src/test/java/com/linkedin/kafka/cruisecontrol/executor/ReplicationThrottleHelperTest.java | Updates mocks and adds coverage for new batched verification and mixed topic existence scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Map<ConfigResource, Config> brokerConfigs = _adminClient.describeConfigs(brokerResources) | ||
| .all().get(CLIENT_REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
There was a problem hiding this comment.
Good catch. Fixed in 58fbba7 — the broker read in setThrottles() now goes through describeConfigsInChunks() like every other AdminClient call in this class. To preserve the fail-fast semantics that .all() gave us for brokers, the per-broker futures are still resolved eagerly and any failure is rethrown, so behavior is unchanged apart from respecting the 500-resource cap.
| for (String replicaThrottleRateConfigKey : Arrays.asList(LEADER_REPLICATION_THROTTLED_RATE_CONFIG, | ||
| FOLLOWER_REPLICATION_THROTTLED_RATE_CONFIG)) { | ||
| ConfigEntry currThrottleRate = brokerConfigs.get(replicaThrottleRateConfigKey); | ||
| if (currThrottleRate == null || !currThrottleRate.value().equals(String.valueOf(_throttleRate))) { | ||
| LOG.debug("Setting {} to {} bytes/second for broker {}", replicaThrottleRateConfigKey, _throttleRate, brokerId); |
There was a problem hiding this comment.
With the chunking fix in 58fbba7, the broker config in setThrottles() now comes from resolving each KafkaFuture<Config> directly, which either returns a non-null Config or throws — there is no longer a map lookup that could return null, so a failure surfaces as the underlying AdminClient exception rather than an NPE.
| Map<ConfigResource, KafkaFuture<Config>> futures = describeConfigsInChunks(new ArrayList<>(pendingByResource.keySet())); | ||
| Iterator<Map.Entry<ConfigResource, Map<String, String>>> pendingIter = pendingByResource.entrySet().iterator(); | ||
| while (pendingIter.hasNext()) { | ||
| Map.Entry<ConfigResource, Map<String, String>> entry = pendingIter.next(); | ||
| ConfigResource cf = entry.getKey(); | ||
| try { | ||
| Config config = futures.get(cf).get(CLIENT_REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); | ||
| if (configsEqual(config, entry.getValue())) { | ||
| pendingIter.remove(); | ||
| } | ||
| } catch (ExecutionException | TimeoutException e) { | ||
| if (cf.type() == ConfigResource.Type.TOPIC && topicNoLongerExists(cf.name())) { | ||
| LOG.debug("Skipping config verification for topic {} since it no longer exists", cf.name()); | ||
| pendingIter.remove(); | ||
| } else { | ||
| // Keep the resource pending: a transient failure must not let an unverified | ||
| // (potentially unthrottled) config pass silently. | ||
| LOG.warn("Failed to verify config for {}; will retry", cf, e); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Good catch — the intent was indeed a single lazy listTopics() per attempt (matching batchGetTopicConfigs / batchAlterTopicConfigs), but calling the per-topic helper inside the failure path defeated that. Fixed in 58fbba7 by fetching the topic-name set lazily at most once per verification attempt and reusing it for all failing topics in that attempt. A failed listTopics() still conservatively keeps the topics pending, so verification never passes unverified.
…ng in verification - setThrottles() broker describeConfigs now goes through describeConfigsInChunks() like every other AdminClient call, resolving each future eagerly and rethrowing on failure to preserve the previous fail-fast .all() semantics. This also removes the map lookup that could theoretically return null. - waitForBatchConfigs() now fetches the topic-name set lazily at most once per verification attempt and reuses it for all failing topics in that attempt, instead of issuing one listTopics per failing topic. A failed listing still conservatively keeps topics pending. - Remove now-unused topicExists() and the all()-based broker mock helpers.
Summary
setThrottles()andclearThrottles()inReplicationThrottleHelpermake sequential, blocking AdminClient RPCs — onedescribeConfigs+ oneincrementalAlterConfigsper broker, then per topic. For a 250-broker cluster with ~100 topics and 1250-movement batches, this produces 600+ synchronous RPCs taking 30+ minutes before any data movement begins. Every call passesCollections.singletonMap()/Collections.singletonList()with exactly oneConfigResource, despite the AdminClient API supporting multi-resource operations.describeConfigsandincrementalAlterConfigscalls across brokers and topics into single multi-resource AdminClient calls (chunked at 500 resources per request), and verify all configs in one batched polling loop:setThrottles()broker ratessetThrottles()topic replicasclearThrottles()waitForConfigs()verificationDesign decisions:
.all()(brokers always exist, atomic batch); topic configs and the per-broker clear path use.values()so a topic deleted mid-operation or a broker that became unreachable only skips itself instead of failing the whole batch.waitForBatchConfigsdrops a topic from verification only after confirming via a single lazily-fetchedlistTopicsthat it was really deleted; any other failure keeps the resource pending and retries, so an unverified throttle can never pass silently.CLIENT_REQUEST_TIMEOUT_MSper call; the per-call timeout does not scale linearly with resource count.changeTopicConfigs,changeBrokerConfigs,waitForConfigs,getEntityConfigs) are removed.Expected Behavior
Throttle setup and removal for an execution completes in seconds regardless of cluster size, and replica movements begin promptly after the proposal is computed.
Actual Behavior
On large clusters, the executor spends 30+ minutes issuing sequential throttle config RPCs before the first replica movement starts, and again when clearing throttles between batches.
Steps to Reproduce
rebalance?dryrun=false).describeConfigs+ oneincrementalAlterConfigsround-trip per broker and per topic.Known Workarounds
None within Cruise Control. Related PRs (#2214, #2304) reduce how often the helper is invoked at the Executor layer, which is complementary but does not remove the per-resource RPC pattern inside the helper.
Additional evidence
ReplicationThrottleHelperTesttests pass (unit + embedded-Kafka integration), including newtestWaitForBatchConfigscases (retry exhaustion, immediate match, partial match with retries, ExecutionException/TimeoutException during verification, deleted-topic skip, transient-failure retry, persistent-broker-failure exhaustion) andtestSetThrottleOnMixedTopicExistence; checkstyle clean.Categorization