Visitar URL original
Share instance selector candidate lists between segments with equal assignments by gortiz · Pull Request #19785 · apache/pinot · GitHub
Skip to content

Share instance selector candidate lists between segments with equal assignments - #19785

Open
gortiz wants to merge 3 commits into
apache:masterfrom
gortiz:routing-a-selector-sharing
Open

gortiz wants to merge 3 commits into
apache:masterfrom
gortiz:routing-a-selector-sharing

Conversation

@gortiz

@gortiz gortiz commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

PR flow

Sharing candidate lists for old segments via canonical instance state maps

flowchart TD
  N0["Get IS and EV maps for segment #40;F1#41;"]:::stModified
  N1["Canonicalize #40;IS#44;EV#41; pair #40;F1#41;"]:::stModified
  N2["Get cached candidate list or compute and cache #40;F1#41;"]:::stModified
  N3["Set segment state with candidate list #40;F1#41;"]:::stModified
  N0 -->|"using the maps"| N1
  N1 -->|"accessing candidate list"| N2
  N2 -->|"passing candidate list"| N3
  classDef stAdded fill:#dafbe1,stroke:#1a7f37,color:#1f2328,stroke-width:2px
  classDef stModified fill:#fff8c5,stroke:#9a6700,color:#1f2328,stroke-width:2px
  classDef stRemoved fill:#ffebe9,stroke:#cf222e,color:#1f2328,stroke-width:2px
  classDef stUnchanged fill:#f6f8fa,stroke:#656d76,color:#1f2328,stroke-width:1px
Loading

AI-generated · Green: added · Yellow: modified · Red: removed · Gray: existing

Diff evidence
  • F1: pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java — before · after
  • Regenerate PR flow

Summary

Part of #19782.

Within one instance selector rebuild, segments with equal ideal state and external view instance state maps now share one list of routing candidates. In a realtime table, all segments of a stream partition usually have the same servers and states, so a large table has about as many distinct assignments as partitions. The rebuild also stops building a TreeMap and a TreeSet per segment, and stops copying candidate lists when all candidates are enabled.

The PR also adds the SLF4J marker ROUTING_NEW_SEGMENTS to the two "Got N new segments" INFO logs, so operators can drop them with a log4j2 MarkerFilter.

Changes

  • convertToSortedMap becomes sortByKeyIfNeeded. It returns the map itself when its iteration order is already sorted (a linear check), and copies it only otherwise. The Helix maps are LinkedHashMap, so the old SortedMap shortcut never applied. ZooKeeper maps are usually written in key order, so the copy is rare.
  • computeOldSegmentCandidates and computeNewSegmentCandidates build the candidates in one pass over the sorted ideal state map, without a TreeSet of online instances.
  • getEnabledCandidatesAndAddToServingInstances returns the input list itself when every candidate is enabled.
  • InstanceStateMapsCache maps each distinct pair of (ideal state map, external view map), compared by content, to one canonical InstanceStateMaps. This entry keeps the candidate list of the first segment. For ReplicaGroupInstanceSelector.updateSegmentMapsForUpsertTable, it also keeps the online instances and a flag that tells if the pair already added its unavailable instances.
  • The cache lives for one rebuild. It holds at most 4096 pairs. Past that limit, each segment gets its own list, as before.

These do not change: the candidate order, the replica ids, the rules for new and old segments, the unavailable segments, the serving instances, and the replica health metrics.

Why shared lists are safe: the lists are read-only

The broker publishes the candidate lists to queries through SegmentStates, so a shared list must never change after the rebuild. These are all the consumers:

Consumer What it does with the list
BalancedInstanceSelector.select get(index) and size()
ReplicaGroupInstanceSelector.select (also StrictReplicaGroupInstanceSelector) get(index), size(), and iteration
MultiStageReplicaGroupSelector iteration
PriorityPoolInstanceSelector.select and rank iteration and streams, copied into new collections
BaseInstanceSelector.refreshSegmentStates iteration. It returns the same list or a new filtered list.

No consumer calls add, remove, set, sort or clear. The rebuild adds elements only to lists that it creates, before it stores them. SegmentInstanceCandidate is immutable (all fields are final).

As evidence, we ran the full pinot-broker test suite with a temporary change that wraps every candidate list in Collections.unmodifiableList. All 555 tests passed. The wrapper is not part of this PR, because it adds one object per list.

Concurrency

This PR keeps the concurrency rule of #19782. InstanceStateMapsCache, InstanceStateMaps, and their online instances and flags are mutable but private to the rebuild thread. The only shared objects that queries see are the candidate lists, which are read-only (see above). Thus one list can be shared between many segments, and between the old and the new SegmentStates.

Compatibility

  • There are no config, API, metric or wire format changes. The routing results are the same as before.

  • The two INFO logs keep the same level, message and arguments. To drop them, add this filter to the log4j2 config of the broker (for example, on the logger for BaseInstanceSelector). The log4j2 configs that Pinot ships do not change.

    <MarkerFilter marker="ROUTING_NEW_SEGMENTS" onMatch="DENY" onMismatch="NEUTRAL"/>

Tests

  • InstanceSelectorTest.testSharedCandidateListsMatchPerSegmentLists, for the balanced, replica-group and strict replica-group selectors. It compares a sharing selector with a per-segment selector: candidates, unavailable segments, serving instances, replica health, and selection for 6 request ids. A third selector with a cache limit of 1 mixes shared and per-segment lists. The test then enables a disabled instance and compares again. The data has unsorted maps, an OFFLINE replica in the external view, a disabled instance, a segment missing from the external view, and new segments.
  • InstanceSelectorTest.testSortByKeyIfNeeded: a sorted LinkedHashMap and a TreeMap come back unchanged. An unsorted map comes back as a sorted copy.

Local results: org.apache.pinot.broker.routing (18 classes) runs 256 tests with 0 failures, including the 91 tests in InstanceSelectorTest. Spotless, license and checkstyle checks pass for pinot-common and pinot-broker.

Measured effect

Synthetic realtime table with 380k segments, one instance selector rebuild (times are noisy):

Selector Before After
Balanced 245 MB, 961 ms 42 MB, 760 ms
Strict replica group 260 MB, 2149 ms 68 MB, 1370 ms

We also replayed the assignment changes of a real 380k-segment realtime table. With this PR, the full rebuild of all routing components allocates 80 MB per change instead of 280 MB.

gortiz added 3 commits October 7, 2026 14:18
…ssignments

On every ideal state or external view change, BaseInstanceSelector.updateSegmentMaps
rebuilt the candidates of every segment from scratch. Per segment it copied the
ideal state instance map into a TreeMap (the Helix maps are LinkedHashMap, so the
SortedMap shortcut never applied), built a TreeSet of online instances, and
allocated a new candidate list. refreshSegmentStates then copied each list again
to filter disabled instances. In a realtime table all segments of a stream
partition sit on the same servers, so a table with hundreds of thousands of
segments has only about as many distinct assignments as partitions.

Topology-independent changes:
- convertToSortedMap becomes sortByKeyIfNeeded: it returns the map itself when
  its iteration order is already sorted (linear check) and copies only otherwise.
- The base selector builds old and new segment candidates in a single pass over
  the sorted ideal state map (computeOldSegmentCandidates and
  computeNewSegmentCandidates) instead of building a TreeSet of online instances.
- getEnabledCandidatesAndAddToServingInstances returns the input list itself when
  every candidate is enabled, instead of copying it.

Sharing: within one rebuild, InstanceStateMapsCache canonicalizes the pair of
(ideal state, external view) instance state maps of each segment, compared by
content. The canonical InstanceStateMaps caches what derives from the pair: the
old segment candidate list, and for the strict replica-group path in
ReplicaGroupInstanceSelector.updateSegmentMapsForUpsertTable also the online
instances, and whether the pair already contributed its unavailable instances.
The cache holds at most MAX_SHARED_CANDIDATE_LISTS (4096) pairs; past that,
segments get their own lists as before.

Candidate order, replica ids, new versus old segment rules, unavailable segments,
serving instances and replica health are unchanged. Sharing lists is safe because
they are never modified once built: every consumer (the balanced, replica-group,
strict replica-group and multi-stage replica-group selectors, and
PriorityPoolInstanceSelector) only reads them. The broker test suite passes with
the lists wrapped as unmodifiable.

InstanceSelectorTest adds an equivalence test that compares a sharing selector
with one that builds a list per segment, for the balanced, replica-group and
strict replica-group selectors. It uses instance state maps that are not in
sorted order, a partially offline replica, a disabled instance and new segments.
The two INFO logs that print the whole new-segment map on every rebuild
(read from ZK metadata, and derived from existing states) are noisy on
tables with many segments. Pass the public marker
BaseInstanceSelector.NEW_SEGMENTS_MARKER ("ROUTING_NEW_SEGMENTS") to
LOGGER.info so operators can drop them with a log4j2 MarkerFilter,
without code changes. Level, message and arguments are unchanged, so
existing log greps keep working. Shipped log4j2 configs are untouched.
InstanceSelectorTest.testSharedCandidateListsMatchPerSegmentLists now also runs
a selector with _maxSharedCandidateLists = 1, so one rebuild mixes shared lists
with lists that InstanceStateMapsCache does not cache once it is full. It
compares the candidates, unavailable segments, serving instances and selections
with the selector that builds one list per segment. This covers the strict
replica-group path, where uncached InstanceStateMaps compute their own online
instances and unavailable instances.

BaseInstanceSelector._maxSharedCandidateLists gets @VisibleForTesting, and the
Javadoc of getEnabledCandidatesAndAddToServingInstances is reworded.
@gortiz gortiz added the performance Related to performance optimization label Oct 7, 2026
@codecov-commenter

codecov-commenter commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.91525% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.48%. Comparing base (43ecf19) to head (5f1dc51).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...routing/instanceselector/BaseInstanceSelector.java 93.02% 2 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master   #19785   +/-   ##
=========================================
  Coverage     68.47%   68.48%           
  Complexity     1470     1470           
=========================================
  Files          3522     3522           
  Lines        229423   229478   +55     
  Branches      36431    36446   +15     
=========================================
+ Hits         157101   157159   +58     
+ Misses        60019    60016    -3     
  Partials      12303    12303           
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 68.48% <94.91%> (+<0.01%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 68.48% <94.91%> (+<0.01%) ⬆️
unittests 68.48% <94.91%> (+<0.01%) ⬆️
unittests1 58.25% <ø> (+<0.01%) ⬆️
unittests2 40.10% <94.91%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Related to performance optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants