This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new afbf3049019 Cache segment to tier map at rebalance start (#19055)
afbf3049019 is described below

commit afbf3049019b7cc337287d6647a7f32902b5e250
Author: Jhow <[email protected]>
AuthorDate: Thu Jul 23 13:49:02 2026 -0700

    Cache segment to tier map at rebalance start (#19055)
---
 .../assignment/segment/BaseSegmentAssignment.java  | 25 ++++++-
 .../assignment/segment/SegmentAssignmentUtils.java | 82 +++++++++++++++-------
 ...NonReplicaGroupTieredSegmentAssignmentTest.java | 64 +++++++++++++++++
 ...NonReplicaGroupTieredSegmentAssignmentTest.java |  8 +++
 .../StrictRealtimeSegmentAssignmentTest.java       | 14 +++-
 5 files changed, 164 insertions(+), 29 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java
index 7a6ad7511d6..16ed00f9dc8 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java
@@ -71,6 +71,20 @@ public abstract class BaseSegmentAssignment implements 
SegmentAssignment {
   protected TableConfig _tableConfig;
   protected ControllerMetrics _controllerMetrics;
 
+  // Cache of segment name to the name of the tier it is eligible for (null 
value means not eligible for any tier), to
+  // avoid reading each segment's ZK metadata on every rebalanceTiers() 
invocation.
+  // NOTE:
+  // 1. This cache is used for table rebalance only. During rebalance, 
rebalanceTable() (and thus rebalanceTiers()) can
+  //    be invoked multiple times when the ideal state changes during the 
rebalance process. The eligible tier of a
+  //    segment is derived from its ZK metadata, which is not expected to 
change during a rebalance, so it is resolved
+  //    once (with a single bulk ZK read) and reused across invocations. A 
fresh SegmentAssignment instance is created
+  //    per rebalance (see SegmentAssignmentFactory), so the cache is scoped 
to a single rebalance.
+  // 2. Segments that are added after the initial bulk read (e.g. realtime 
segments that commit during a long
+  //    rebalance) will be absent from the cache; they are resolved on demand 
and merged in, so they are still routed
+  //    to the correct tier.
+  @Nullable
+  private Map<String, String> _segmentToTierName;
+
   @Override
   public void init(HelixManager helixManager, TableConfig tableConfig, 
@Nullable ControllerMetrics controllerMetrics) {
     _helixManager = helixManager;
@@ -102,10 +116,17 @@ public abstract class BaseSegmentAssignment implements 
SegmentAssignment {
     _logger.info("Rebalancing tiers: {} for table: {} with bootstrap: {}", 
tierInstancePartitionsMap.keySet(),
         _tableNameWithType, bootstrap);
 
+    // Resolve the eligible tier of each segment once (single bulk ZK read) 
and reuse it across the multiple
+    // rebalanceTiers() invocations of this rebalance, instead of reading each 
segment's ZK metadata every time.
+    // Any new segments come later would not be in _segmentToTierName map, and 
categorized in the null (default) tier.
+    if (_segmentToTierName == null) {
+      _segmentToTierName =
+          SegmentAssignmentUtils.getSegmentToTierNameMap(_helixManager, 
_tableNameWithType, sortedTiers);
+    }
+
     // Get tier to segment assignment map i.e. current assignments split by 
tiers they are eligible for
     SegmentAssignmentUtils.TierSegmentAssignment tierSegmentAssignment =
-        new SegmentAssignmentUtils.TierSegmentAssignment(_helixManager, 
_tableNameWithType, sortedTiers,
-            currentAssignment);
+        new SegmentAssignmentUtils.TierSegmentAssignment(sortedTiers, 
currentAssignment, _segmentToTierName);
     Map<String, Map<String, Map<String, String>>> 
tierNameToSegmentAssignmentMap =
         tierSegmentAssignment.getTierNameToSegmentAssignmentMap();
 
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/SegmentAssignmentUtils.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/SegmentAssignmentUtils.java
index dfc9eadb11b..500af00d0d4 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/SegmentAssignmentUtils.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/SegmentAssignmentUtils.java
@@ -452,6 +452,48 @@ public class SegmentAssignmentUtils {
     }
   }
 
+  /**
+   * Resolves, for each segment of the table, the name of the first tier (in 
{@code sortedTiers} order) that the segment
+   * is eligible for. The value is the tier name, or {@code null} if the 
segment (including COMMITTING segments) is not
+   * eligible for any tier. This performs a single bulk ZK read for all 
segment ZK metadata, so it is meant to be
+   * computed once per rebalance and reused across the multiple {@code 
rebalanceTable()} invocations of that rebalance
+   * rather than reading each segment's ZK metadata every time. Segments that 
appear only after this map is computed
+   * (e.g. segments that commit during a long rebalance) are absent from the 
map and treated as not eligible for any
+   * tier.
+   */
+  static Map<String, String> getSegmentToTierNameMap(HelixManager 
helixManager, String tableNameWithType,
+      List<Tier> sortedTiers) {
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
helixManager.getHelixPropertyStore();
+    List<SegmentZKMetadata> segmentsZKMetadata = 
ZKMetadataProvider.getSegmentsZKMetadata(propertyStore,
+        tableNameWithType);
+    Map<String, String> segmentToTierName = new HashMap<>();
+    for (SegmentZKMetadata segmentZKMetadata : segmentsZKMetadata) {
+      segmentToTierName.put(segmentZKMetadata.getSegmentName(), 
getTierName(tableNameWithType, sortedTiers,
+          segmentZKMetadata));
+    }
+    return segmentToTierName;
+  }
+
+  /**
+   * Returns the name of the first tier (in {@code sortedTiers} order) the 
given segment is eligible for, or
+   * {@code null} if the segment is COMMITTING or not eligible for any tier.
+   */
+  @Nullable
+  private static String getTierName(String tableNameWithType, List<Tier> 
sortedTiers,
+      SegmentZKMetadata segmentZKMetadata) {
+    // Skip COMMITTING segments
+    if (segmentZKMetadata.getStatus() == Status.COMMITTING) {
+      return null;
+    }
+    // Find an eligible tier for the segment, from the ordered list of tiers
+    for (Tier tier : sortedTiers) {
+      if (tier.getSegmentSelector().selectSegment(tableNameWithType, 
segmentZKMetadata)) {
+        return tier.getName();
+      }
+    }
+    return null;
+  }
+
   /**
    * Takes a segment assignment and splits them up based on which tiers the 
segments are eligible for. Only considers
    * ONLINE segments.
@@ -463,44 +505,32 @@ public class SegmentAssignmentUtils {
     private final Map<String, Map<String, String>> _nonTierSegmentAssignment = 
new TreeMap<>();
 
     /**
-     * Creates a TierSegmentAssignment from the given segmentAssignment
-     * @param tableNameWithType table to which the segment assignment belongs
+     * Creates a TierSegmentAssignment from the given segmentAssignment.
      * @param sortedTiers list of tiers, pre-sorted as per desired order by 
caller
      * @param segmentAssignment segment assignment of the table
+     * @param segmentToTierName map from segment name to the name of the tier 
it is eligible for (see
+     *                          {@link #getSegmentToTierNameMap}); segments 
absent from this map are not eligible for
+     *                          any tier
      */
-    TierSegmentAssignment(HelixManager helixManager, String tableNameWithType, 
List<Tier> sortedTiers,
-        Map<String, Map<String, String>> segmentAssignment) {
+    TierSegmentAssignment(List<Tier> sortedTiers, Map<String, Map<String, 
String>> segmentAssignment,
+        Map<String, String> segmentToTierName) {
 
       // initialize tier to segmentAssignment map
       sortedTiers.forEach(t -> 
_tierNameToSegmentAssignmentMap.put(t.getName(), new TreeMap<>()));
 
       // iterate over all segments
-      // TODO: Reduce ZK access
-      ZkHelixPropertyStore<ZNRecord> propertyStore = 
helixManager.getHelixPropertyStore();
       for (Map.Entry<String, Map<String, String>> entry : 
segmentAssignment.entrySet()) {
         String segmentName = entry.getKey();
         Map<String, String> instanceStateMap = entry.getValue();
-        boolean selected = false;
-
-        // only consider ONLINE segments for tiers
-        if (instanceStateMap.containsValue(SegmentStateModel.ONLINE)) {
-          // find an eligible tier for the segment, from the ordered list of 
tiers
-          SegmentZKMetadata segmentZKMetadata =
-              ZKMetadataProvider.getSegmentZKMetadata(propertyStore, 
tableNameWithType, segmentName);
-          // Skip COMMITTING segments
-          if (segmentZKMetadata != null && segmentZKMetadata.getStatus() != 
Status.COMMITTING) {
-            for (Tier tier : sortedTiers) {
-              if (tier.getSegmentSelector().selectSegment(tableNameWithType, 
segmentZKMetadata)) {
-                
_tierNameToSegmentAssignmentMap.get(tier.getName()).put(segmentName, 
instanceStateMap);
-                selected = true;
-                break;
-              }
-            }
-          }
-        }
 
-        // if segment not eligible for any tier, put in ordinary segments map
-        if (!selected) {
+        // Only consider ONLINE segments for tiers. The eligible tier for each 
segment is resolved once via
+        // segmentToTierName, avoiding a per-segment ZK read on every 
rebalanceTable() invocation.
+        String tierName = 
instanceStateMap.containsValue(SegmentStateModel.ONLINE)
+            ? segmentToTierName.get(segmentName) : null;
+        if (tierName != null) {
+          _tierNameToSegmentAssignmentMap.get(tierName).put(segmentName, 
instanceStateMap);
+        } else {
+          // if segment not eligible for any tier, put in ordinary segments map
           _nonTierSegmentAssignment.put(segmentName, instanceStateMap);
         }
       }
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/OfflineNonReplicaGroupTieredSegmentAssignmentTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/OfflineNonReplicaGroupTieredSegmentAssignmentTest.java
index 83ef265676f..18d8f1939b6 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/OfflineNonReplicaGroupTieredSegmentAssignmentTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/OfflineNonReplicaGroupTieredSegmentAssignmentTest.java
@@ -19,6 +19,7 @@
 package org.apache.pinot.controller.helix.core.assignment.segment;
 
 import com.google.common.collect.Lists;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -47,9 +48,12 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
@@ -116,6 +120,13 @@ public class 
OfflineNonReplicaGroupTieredSegmentAssignmentTest {
       String segmentName = path.substring(path.lastIndexOf('/') + 1);
       return new ZNRecord(segmentName);
     });
+    // Bulk read of all segment ZK metadata, used to resolve the eligible tier 
of each segment once per rebalance
+    List<ZNRecord> segmentZNRecords = new ArrayList<>(NUM_SEGMENTS);
+    for (String segmentName : SEGMENTS) {
+      segmentZNRecords.add(new ZNRecord(segmentName));
+    }
+    when(propertyStore.getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt())).thenReturn(
+        segmentZNRecords);
     //noinspection unchecked
     when(helixManager.getHelixPropertyStore()).thenReturn(propertyStore);
 
@@ -269,6 +280,59 @@ public class 
OfflineNonReplicaGroupTieredSegmentAssignmentTest {
     }
   }
 
+  /**
+   * The segment to tier resolution is computed once (single bulk ZK read) at 
the start of the rebalance and reused
+   * across the multiple rebalanceTable() invocations of that rebalance. A 
segment that appears in the current
+   * assignment only in a later invocation (e.g. after the ideal state changes 
during a long rebalance) is absent from
+   * the resolution map, so it is kept on the default instances rather than 
triggering another ZK read.
+   */
+  @Test
+  public void testSegmentAddedAfterInitialBulkReadStaysOnDefaultTier() {
+    HelixManager helixManager = mock(HelixManager.class);
+    //noinspection rawtypes
+    ZkHelixPropertyStore propertyStore = mock(ZkHelixPropertyStore.class);
+    // The initial bulk read only returns the segments that existed when the 
rebalance started (segment_0 .. 99)
+    List<ZNRecord> segmentZNRecords = new ArrayList<>(NUM_SEGMENTS);
+    for (String segmentName : SEGMENTS) {
+      segmentZNRecords.add(new ZNRecord(segmentName));
+    }
+    //noinspection unchecked
+    when(propertyStore.getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt())).thenReturn(
+        segmentZNRecords);
+    //noinspection unchecked
+    when(helixManager.getHelixPropertyStore()).thenReturn(propertyStore);
+    SegmentAssignment segmentAssignment = 
SegmentAssignmentFactory.getSegmentAssignment(helixManager,
+        new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME).setNumReplicas(NUM_REPLICAS).build(),
+        null);
+
+    Map<String, Map<String, String>> currentAssignment = new TreeMap<>();
+    for (String segmentName : SEGMENTS) {
+      currentAssignment.put(segmentName,
+          SegmentAssignmentUtils.getInstanceStateMap(INSTANCES.subList(0, 
NUM_REPLICAS), SegmentStateModel.ONLINE));
+    }
+
+    // First rebalance resolves the segment to tier map from the single bulk 
read
+    segmentAssignment.rebalanceTable(currentAssignment, 
_instancePartitionsMap, _sortedTiers,
+        _tierInstancePartitionsMap, new RebalanceConfig());
+    //noinspection unchecked
+    verify(propertyStore, times(1)).getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt());
+
+    // A new segment that would be eligible for tierC (segId >= 120) appears 
in the assignment after the initial bulk
+    // read
+    String newSegment = SEGMENT_NAME_PREFIX + 200;
+    currentAssignment.put(newSegment,
+        SegmentAssignmentUtils.getInstanceStateMap(INSTANCES.subList(0, 
NUM_REPLICAS), SegmentStateModel.ONLINE));
+    Map<String, Map<String, String>> newAssignment =
+        segmentAssignment.rebalanceTable(currentAssignment, 
_instancePartitionsMap, _sortedTiers,
+            _tierInstancePartitionsMap, new RebalanceConfig());
+
+    // No second bulk read happens, and the new segment is kept on the default 
instances (not relocated to tierC)
+    //noinspection unchecked
+    verify(propertyStore, times(1)).getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt());
+    Assert.assertEquals(newAssignment.get(newSegment).size(), NUM_REPLICAS);
+    
Assert.assertTrue(INSTANCES.containsAll(newAssignment.get(newSegment).keySet()));
+  }
+
   /**
    * Selects segment_50 to segment_69 i.e. 20 segments
    */
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/RealtimeNonReplicaGroupTieredSegmentAssignmentTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/RealtimeNonReplicaGroupTieredSegmentAssignmentTest.java
index b2316ba1547..2f5cbf29a15 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/RealtimeNonReplicaGroupTieredSegmentAssignmentTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/RealtimeNonReplicaGroupTieredSegmentAssignmentTest.java
@@ -47,6 +47,7 @@ import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -126,6 +127,13 @@ public class 
RealtimeNonReplicaGroupTieredSegmentAssignmentTest {
       String segmentName = path.substring(path.lastIndexOf('/') + 1);
       return new ZNRecord(segmentName);
     });
+    // Bulk read of all segment ZK metadata, used to resolve the eligible tier 
of each segment once per rebalance
+    List<ZNRecord> segmentZNRecords = new ArrayList<>(NUM_SEGMENTS);
+    for (String segmentName : _segments) {
+      segmentZNRecords.add(new ZNRecord(segmentName));
+    }
+    when(propertyStore.getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt())).thenReturn(
+        segmentZNRecords);
     //noinspection unchecked
     when(helixManager.getHelixPropertyStore()).thenReturn(propertyStore);
 
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/StrictRealtimeSegmentAssignmentTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/StrictRealtimeSegmentAssignmentTest.java
index e308b43bb8d..80968cf5a96 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/StrictRealtimeSegmentAssignmentTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/assignment/segment/StrictRealtimeSegmentAssignmentTest.java
@@ -54,6 +54,7 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -78,7 +79,7 @@ public class StrictRealtimeSegmentAssignmentTest {
   private static final String CONSUMING_INSTANCE_PARTITIONS_NAME =
       
InstancePartitionsType.CONSUMING.getInstancePartitionsName(RAW_TABLE_NAME);
 
-  private List<String> _segments;
+  private static List<String> _segments;
   private Map<InstancePartitionsType, InstancePartitions> 
_instancePartitionsMap;
   private InstancePartitions _newConsumingInstancePartitions;
 
@@ -493,6 +494,17 @@ public class StrictRealtimeSegmentAssignmentTest {
       }
       return record;
     });
+    // Bulk read of all segment ZK metadata, used to resolve the eligible tier 
of each segment once per rebalance
+    List<ZNRecord> segmentZNRecords = new ArrayList<>(_segments.size());
+    for (String segmentName : _segments) {
+      ZNRecord record = new ZNRecord(segmentName);
+      if (tieredSegments.contains(segmentName)) {
+        record.setSimpleField(Segment.TIER, "coldTier");
+      }
+      segmentZNRecords.add(record);
+    }
+    when(propertyStore.getChildren(anyString(), eq(null), 
eq(AccessOption.PERSISTENT), anyInt(), anyInt())).thenReturn(
+        segmentZNRecords);
     when(helixManager.getHelixPropertyStore()).thenReturn(propertyStore);
     return helixManager;
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to