Jackie-Jiang commented on code in PR #19386:
URL: https://github.com/apache/pinot/pull/19386#discussion_r4252685422
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -2080,6 +2367,143 @@ private static void
updateNumSegmentsToOffloadMap(Map<String, Integer> numSegmen
}
}
+ /// Returns the minimum available replicas the rebalance has to keep up,
derived from
+ /// `minReplicasToKeepUpForNoDowntime` and the replication of the segments
to be moved. Shared by the rebalance and
+ /// by the pre-checks so that the two cannot drift apart.
+ ///
+ /// NOTE:
+ /// 1. The calculation is based on the number of replicas of the target
assignment. In case of increasing the number
+ /// of replicas for the current assignment, the current instance state
map might not have enough replicas to reach
+ /// the minimum available replicas requirement. In this scenario we don't
want to fail the check, but keep all the
+ /// current instances as this is the best we can do, and can help the
table get out of this state.
+ /// 2. Only check the segments to be moved because we don't need to maintain
available replicas for segments not
+ /// being moved, including segments with all replicas OFFLINE (error
segments during consumption).
+ ///
+ /// @return the minimum available replicas, or
[#ILLEGAL_MIN_AVAILABLE_REPLICAS] when
+ /// `minReplicasToKeepUpForNoDowntime` is not less than the
replication of the segments to be moved
+ @VisibleForTesting
+ static int getMinAvailableReplicas(Map<String, Map<String, String>>
currentAssignment,
+ Map<String, Map<String, String>> targetAssignment, List<String>
segmentsToMove,
+ int minReplicasToKeepUpForNoDowntime, Logger tableRebalanceLogger) {
+ int numReplicas = getMinNumReplicas(targetAssignment, segmentsToMove);
+ int minAvailableReplicas;
+ if (minReplicasToKeepUpForNoDowntime >= 0) {
+ // For non-negative value, use it as min available replicas
+ if (minReplicasToKeepUpForNoDowntime >= numReplicas) {
+ return ILLEGAL_MIN_AVAILABLE_REPLICAS;
+ }
+ minAvailableReplicas = minReplicasToKeepUpForNoDowntime;
+ } else {
+ // For negative value, use it as max unavailable replicas
+ minAvailableReplicas = Math.max(numReplicas +
minReplicasToKeepUpForNoDowntime, 0);
+ }
+
+ int numCurrentAssignmentReplicas = getMinNumReplicas(currentAssignment,
segmentsToMove);
+ if (minAvailableReplicas > numCurrentAssignmentReplicas) {
+ tableRebalanceLogger.warn("minAvailableReplicas: {} larger than existing
number of replicas: {}, "
+ + "resetting minAvailableReplicas to {}", minAvailableReplicas,
numCurrentAssignmentReplicas,
+ numCurrentAssignmentReplicas);
+ minAvailableReplicas = numCurrentAssignmentReplicas;
+ }
+ return minAvailableReplicas;
+ }
+
+ private static int getMinNumReplicas(Map<String, Map<String, String>>
assignment, List<String> segments) {
+ int numReplicas = Integer.MAX_VALUE;
+ for (String segment : segments) {
+ numReplicas = Math.min(assignment.get(segment).size(), numReplicas);
+ }
+ return numReplicas;
+ }
+
+ /// Replays a low disk mode rebalance to find the servers it cannot keep
within the disk they start with.
+ ///
+ /// Low disk mode bounds the disk each server may take on to the larger of
what it hosts when the rebalance starts
+ /// and what the target assignment places on it. When no progress at all is
possible within those bounds - servers
+ /// circularly waiting for one another to drop first - the rebalance relaxes
them for a step rather than stalling,
+ /// which is the only way a server can end up over its bound. This runs
[#getNextAssignment] over the whole rebalance
+ /// up front to find out whether that happens, and for which servers, before
a single segment is moved.
+ ///
+ /// The replay is exact for the assignment it is given, with one caveat: it
does not read the segment partition ids,
+ /// so with `batchSizePerServer` enabled and strict replica group routing
the segments are grouped more coarsely than
+ /// the rebalance groups them, which changes the pacing of the moves but not
the bounds themselves.
+ ///
+ /// @return `null` when the replay could not be finished, which establishes
nothing and must not be read as safe;
+ /// an empty map when the rebalance was replayed to the target
assignment inside every server's bound; and
+ /// otherwise the servers that would be pushed over their bound,
mapped to the bytes they would go over by
+ @Nullable
+ public static Map<String, Long> getServersForcedOverDiskBudget(
+ Map<String, Map<String, String>> currentAssignment, Map<String,
Map<String, String>> targetAssignment,
+ int minAvailableReplicas, boolean enableStrictReplicaGroup, int
batchSizePerServer,
+ @Nullable TableSizeReader.TableSubTypeSizeDetails tableSizeDetails,
Logger tableRebalanceLogger) {
+ return getServersForcedOverDiskBudget(currentAssignment, targetAssignment,
minAvailableReplicas,
+ enableStrictReplicaGroup, batchSizePerServer, tableSizeDetails,
tableRebalanceLogger,
+ MAX_DISK_BUDGET_REPLAY_STEPS);
+ }
+
+ /// @param maxSteps how many steps to replay before giving up, so that the
giving-up path can be tested without
+ /// building an assignment that genuinely needs tens of
thousands of steps
+ @Nullable
+ @VisibleForTesting
+ static Map<String, Long> getServersForcedOverDiskBudget(Map<String,
Map<String, String>> currentAssignment,
+ Map<String, Map<String, String>> targetAssignment, int
minAvailableReplicas, boolean enableStrictReplicaGroup,
+ int batchSizePerServer, @Nullable
TableSizeReader.TableSubTypeSizeDetails tableSizeDetails,
+ Logger tableRebalanceLogger, int maxSteps) {
+ DiskUsageBudget diskUsageBudget =
DiskUsageBudget.create(currentAssignment, tableSizeDetails);
+ Object2IntOpenHashMap<String> segmentPartitionIdMap = new
Object2IntOpenHashMap<>();
+ Map<String, Long> serverToBytesOverBudget = new TreeMap<>();
+ Map<String, Map<String, String>> assignment = currentAssignment;
+ for (int step = 1; step <= maxSteps; step++) {
+ if (assignment.equals(targetAssignment)) {
+ return serverToBytesOverBudget;
+ }
+ Map<String, Long> remainingBytes = diskUsageBudget.forStep(assignment,
targetAssignment).getRemainingBytes();
+ Map<String, Map<String, String>> nextAssignment;
+ try {
+ nextAssignment =
+ getNextAssignment(assignment, targetAssignment,
minAvailableReplicas, enableStrictReplicaGroup, true,
+ batchSizePerServer, segmentPartitionIdMap,
DEFAULT_PARTITION_ID_FETCHER, new NoOpRiskAssessor(),
+ tableRebalanceLogger, diskUsageBudget);
Review Comment:
**[P1] Replay with the same partition IDs used by execution**
`DEFAULT_PARTITION_ID_FETCHER` assigns every segment partition ID 0. With
strict routing and batching, collapsing partition boundaries changes allocation
decisions and whether the unrestricted fallback is reached. It can therefore
return an empty over-budget map while the actual rebalance exceeds the budget.
Using the exact planner methods with `enableStrictReplicaGroup=true`,
`minAvailableReplicas=2`, `batchSizePerServer=1`, and all replicas ONLINE, this
fixture returns `{}` from the public replay. Running the same assignment with
the listed partition IDs overruns h3 by 12 MiB and h6 by 30 MiB. Partitions are
cohesive: each partition has a single current/target placement pair.
| Segment | Size (MiB) | Partition | Current hosts | Target hosts |
|---|---:|---:|---|---|
| s007 | 22 | 2 | h5,h7 | h0,h4,h6 |
| s008 | 28 | 3 | h5,h7 | h0,h4,h6 |
| s013 | 37 | 4 | h0,h2,h3 | h2,h4,h6 |
| s016 | 19 | 8 | h0,h4,h6 | h2,h5,h7 |
| s017 | 15 | 10 | h0,h4,h6 | h2,h5,h7 |
| s018 | 24 | 10 | h0,h4,h6 | h2,h5,h7 |
| s019 | 49 | 9 | h0,h4,h6 | h2,h5,h7 |
| s020 | 30 | 9 | h0,h4,h6 | h2,h5,h7 |
| s022 | 20 | 13 | h0,h1,h2 | h0,h3,h6 |
| s023 | 25 | 15 | h0,h1,h2 | h0,h3,h6 |
| s024 | 12 | 13 | h0,h1,h2 | h0,h3,h6 |
h6 starts at 137 MiB, targets 144 MiB, and reaches 174 MiB during execution.
For a concrete disk-threshold example, give h6 970 MiB used out of 2,000 MiB at
a 50% threshold, and give the other servers ample capacity. With the table's
reported per-replica size set to 281 MiB, the pre-check estimates h6 below
threshold after rebalance and invokes replay for the transient risk. Replay's
empty result produces PASS, but execution reaches 1,007 MiB used, above the
1,000 MiB threshold.
Please supply the real partition IDs to replay, or report this configuration
as inconclusive. A regression should compare replay with partition-aware
execution for this fixture.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -1687,19 +1695,266 @@ static Map<String, Map<String, String>>
getNextAssignment(Map<String, Map<String
private static Map<String, Map<String, String>>
getNextAssignment(Map<String, Map<String, String>> currentAssignment,
Map<String, Map<String, String>> targetAssignment, int
minAvailableReplicas, boolean enableStrictReplicaGroup,
boolean lowDiskMode, int batchSizePerServer,
Object2IntOpenHashMap<String> segmentPartitionIdMap,
- PartitionIdFetcher partitionIdFetcher, DataLossRiskAssessor
dataLossRiskAssessor, Logger tableRebalanceLogger) {
+ PartitionIdFetcher partitionIdFetcher, DataLossRiskAssessor
dataLossRiskAssessor, Logger tableRebalanceLogger,
+ @Nullable DiskUsageBudget diskUsageBudget) {
+ if (!lowDiskMode) {
+ return computeNextAssignment(currentAssignment, targetAssignment,
minAvailableReplicas, enableStrictReplicaGroup,
+ false, batchSizePerServer, segmentPartitionIdMap,
partitionIdFetcher, dataLossRiskAssessor,
+ tableRebalanceLogger, null);
+ }
+
+ // We use DiskUsageBudget and StepDiskBudget here to keep track of
per-server byte gain/loss and to guarantee the
+ // invariant of lowDiskMode where each server won't get more bytes than
its net gain at any intermediate steps.
+ Preconditions.checkState(diskUsageBudget != null, "Low disk mode requires
a disk usage budget");
+ StepDiskBudget stepBudget = diskUsageBudget.forStep(currentAssignment,
targetAssignment);
+ Map<String, Map<String, String>> nextAssignment =
+ computeNextAssignment(currentAssignment, targetAssignment,
minAvailableReplicas, enableStrictReplicaGroup, true,
+ batchSizePerServer, segmentPartitionIdMap, partitionIdFetcher,
dataLossRiskAssessor, tableRebalanceLogger,
+ stepBudget);
+ if (!nextAssignment.equals(currentAssignment)) {
+ return nextAssignment;
+ }
+
+ // Can't progress while respecting the budget. This could happen if
servers circular wait for others to drop first.
+ tableRebalanceLogger.warn("Cannot make progress in low disk mode without
exceeding the disk usage a server "
+ + "started the rebalance with. Allowing the additions for this
step, which temporarily increases the disk "
+ + "usage of these servers. Bytes each server could still take on:
{}", stepBudget.getRemainingBytes());
+ return computeNextAssignment(currentAssignment, targetAssignment,
minAvailableReplicas, enableStrictReplicaGroup,
+ true, batchSizePerServer, segmentPartitionIdMap, partitionIdFetcher,
dataLossRiskAssessor,
+ tableRebalanceLogger, null);
+ }
+
+ /// @param stepBudget bounds the disk each server may take on in this step,
`null` to apply no bound
+ private static Map<String, Map<String, String>> computeNextAssignment(
+ Map<String, Map<String, String>> currentAssignment, Map<String,
Map<String, String>> targetAssignment,
+ int minAvailableReplicas, boolean enableStrictReplicaGroup, boolean
lowDiskMode, int batchSizePerServer,
+ Object2IntOpenHashMap<String> segmentPartitionIdMap, PartitionIdFetcher
partitionIdFetcher,
+ DataLossRiskAssessor dataLossRiskAssessor, Logger tableRebalanceLogger,
+ @Nullable StepDiskBudget stepBudget) {
return enableStrictReplicaGroup
? getNextStrictReplicaGroupAssignment(currentAssignment,
targetAssignment, minAvailableReplicas, lowDiskMode,
- batchSizePerServer, segmentPartitionIdMap, partitionIdFetcher,
dataLossRiskAssessor, tableRebalanceLogger)
+ batchSizePerServer, segmentPartitionIdMap, partitionIdFetcher,
dataLossRiskAssessor, tableRebalanceLogger,
+ stepBudget)
: getNextNonStrictReplicaGroupAssignment(currentAssignment,
targetAssignment, minAvailableReplicas,
- lowDiskMode, batchSizePerServer, dataLossRiskAssessor);
+ lowDiskMode, batchSizePerServer, dataLossRiskAssessor, stepBudget);
+ }
+
+ /// Bounds the disk a server may use while a low disk mode rebalance is in
flight.
+ ///
+ /// If a server results in net byte loss, its ceiling is as many bytes as it
originally hosted.
+ /// If a server results in net byte gain, its ceiling is as many bytes as it
will eventually host.
+ ///
+ /// This invariant helps us guarantee that a rebalance won't introduce bytes
more than necessary on the disk at any
+ /// moment during the rebalance. Thus, rebalance can safely run as long as
we know the target assignment won't go
+ /// beyond their disk utilization (this can be guard by either pre-check or
resource utilization checker etc.)
+ ///
+ /// In the case when new segments appear in ideal state by external sources
(e.g. segment upload, consuming segment
+ /// committed), there are two cases:
+ /// 1. They are added to the ideal state as the target assignment. This case
the rebalance steps outcome won't change
+ /// 2. They are added to the ideal state that's different to their target
assignments. For example when strict
+ /// replica routing is in place. This case, it might lead to a different
computeNextAssignment result.
+ ///
+ /// When no per-segment size is known every segment counts as one byte,
which degenerates to bounding the number of
+ /// segments hosted rather than the bytes.
+ @VisibleForTesting
+ static class DiskUsageBudget {
+ private final Map<String, Long> _segmentSizeBytes;
+ private final long _defaultSegmentSizeBytes;
+ private final Map<String, Long> _anchorServerBytes;
+ private final Set<String> _accountedSegments;
+
+ private DiskUsageBudget(Map<String, Long> segmentSizeBytes, long
defaultSegmentSizeBytes,
+ Map<String, Long> anchorServerBytes, Set<String> accountedSegments) {
+ _segmentSizeBytes = segmentSizeBytes;
+ _defaultSegmentSizeBytes = defaultSegmentSizeBytes;
+ _anchorServerBytes = anchorServerBytes;
+ _accountedSegments = accountedSegments;
+ }
+
+ /// @param initialAssignment the assignment the rebalance starts from,
used to anchor the per-server ceiling
+ /// @param tableSizeDetails per-segment sizes, `null` to fall back to
counting segments instead of bytes
+ static DiskUsageBudget create(Map<String, Map<String, String>>
initialAssignment,
+ @Nullable TableSizeReader.TableSubTypeSizeDetails tableSizeDetails) {
+ Map<String, Long> segmentSizeBytes = new HashMap<>();
+ long totalKnownBytes = 0;
+ if (tableSizeDetails != null) {
+ for (Map.Entry<String, TableSizeReader.SegmentSizeDetails> entry :
tableSizeDetails._segments.entrySet()) {
+ // The size a single replica takes up on disk, which is what one
server pays for hosting the segment
+ long sizeBytes = entry.getValue()._maxReportedSizePerReplicaInBytes;
+ if (sizeBytes > 0) {
+ segmentSizeBytes.put(entry.getKey(), sizeBytes);
+ totalKnownBytes += sizeBytes;
+ }
+ }
+ }
+ // Segments whose size could not be read (missing from all servers, or
still consuming) are charged the average
+ // size of the segments that could be read. When no per-segment size is
available at all, fall back to the
+ // average over the whole table, which is the same estimate the disk
utilization pre-check works with. Failing
+ // that, charge every segment one byte, which turns the budget into a
bound on the number of segments hosted
+ long defaultSegmentSizeBytes;
+ if (!segmentSizeBytes.isEmpty()) {
+ defaultSegmentSizeBytes = Math.max(1L, totalKnownBytes /
segmentSizeBytes.size());
+ } else if (tableSizeDetails != null &&
tableSizeDetails._reportedSizePerReplicaInBytes > 0
+ && !initialAssignment.isEmpty()) {
+ defaultSegmentSizeBytes =
+ Math.max(1L, tableSizeDetails._reportedSizePerReplicaInBytes /
initialAssignment.size());
+ } else {
+ defaultSegmentSizeBytes = 1L;
+ }
+ Map<String, Long> initialServerBytes = new HashMap<>();
+ for (Map.Entry<String, Map<String, String>> entry :
initialAssignment.entrySet()) {
+ long sizeBytes = segmentSizeBytes.getOrDefault(entry.getKey(),
defaultSegmentSizeBytes);
+ for (String instance : entry.getValue().keySet()) {
+ initialServerBytes.merge(instance, sizeBytes, Long::sum);
+ }
+ }
+ return new DiskUsageBudget(segmentSizeBytes, defaultSegmentSizeBytes,
initialServerBytes,
+ new HashSet<>(initialAssignment.keySet()));
+ }
+
+ long getSegmentSizeBytes(String segmentName) {
+ return _segmentSizeBytes.getOrDefault(segmentName,
_defaultSegmentSizeBytes);
+ }
+
+ /// Adds the segments that appeared since the anchor was taken - an
uploaded segment, or a new consuming segment -
+ /// to the anchor of the servers hosting them.
+ ///
+ /// With this, this segment wouldn't be account for the budget if it
doesn't need to be moved at all.
+ private void accountForNewSegments(Map<String, Map<String, String>>
currentAssignment) {
+ for (Map.Entry<String, Map<String, String>> entry :
currentAssignment.entrySet()) {
+ if (_accountedSegments.add(entry.getKey())) {
+ long sizeBytes = getSegmentSizeBytes(entry.getKey());
+ for (String instance : entry.getValue().keySet()) {
+ _anchorServerBytes.merge(instance, sizeBytes, Long::sum);
+ }
+ }
+ }
+ }
+
+ Map<String, Long> getServerToHostedBytes(Map<String, Map<String, String>>
assignment) {
+ Map<String, Long> serverToHostedBytes = new HashMap<>();
+ for (Map.Entry<String, Map<String, String>> entry :
assignment.entrySet()) {
+ long sizeBytes = getSegmentSizeBytes(entry.getKey());
+ for (String instance : entry.getValue().keySet()) {
+ serverToHostedBytes.merge(instance, sizeBytes, Long::sum);
+ }
+ }
+ return serverToHostedBytes;
+ }
+
+ /// Totals the budgeted bytes of the segments sharing each pair of current
and target instances, over whichever
+ /// part of the assignment it is given. Those segments are all assigned
the same instances, so an instance added
+ /// for one of them takes on all of them.
+ Map<Pair<Set<String>, Set<String>>, Long> getInstancePairToBytes(
+ Map<String, Map<String, String>> currentAssignment, Map<String,
Map<String, String>> targetAssignment) {
+ Map<Pair<Set<String>, Set<String>>, Long> instancePairToBytes = new
HashMap<>();
+ for (Map.Entry<String, Map<String, String>> entry :
currentAssignment.entrySet()) {
+ Map<String, String> targetInstanceStateMap =
targetAssignment.get(entry.getKey());
+ if (targetInstanceStateMap != null) {
+ instancePairToBytes.merge(Pair.of(entry.getValue().keySet(),
targetInstanceStateMap.keySet()),
+ getSegmentSizeBytes(entry.getKey()), Long::sum);
+ }
+ }
+ return instancePairToBytes;
+ }
+
+ /// Returns the bytes each server may still take on in this step, i.e.
`ceiling - currently hosted`.
+ StepDiskBudget forStep(Map<String, Map<String, String>> currentAssignment,
+ Map<String, Map<String, String>> targetAssignment) {
+ accountForNewSegments(currentAssignment);
+ Map<String, Long> hostedBytes =
getServerToHostedBytes(currentAssignment);
+ Map<String, Long> targetBytes = getServerToHostedBytes(targetAssignment);
+ Set<String> servers = new HashSet<>(_anchorServerBytes.keySet());
+ servers.addAll(hostedBytes.keySet());
+ servers.addAll(targetBytes.keySet());
+ Map<String, Long> remainingBytes = new HashMap<>();
+ for (String server : servers) {
+ long ceilingBytes =
+ Math.max(_anchorServerBytes.getOrDefault(server, 0L),
targetBytes.getOrDefault(server, 0L));
+ remainingBytes.put(server, Math.max(0L, ceilingBytes -
hostedBytes.getOrDefault(server, 0L)));
+ }
+ // Segments that share a current and target instance pair are all
assigned the same instances, so a newly added
+ // instance takes on all of them. Total their size up so that the
instances to add can be picked with that in
+ // mind rather than only on the number of segments to offload
+ return new StepDiskBudget(this, remainingBytes,
+ getInstancePairToBytes(currentAssignment, targetAssignment));
+ }
+ }
+
+ /// The [DiskUsageBudget] left for a single rebalance step, drawn down as
segments are assigned.
+ @VisibleForTesting
+ static class StepDiskBudget {
+ private final DiskUsageBudget _budget;
+ private final Map<String, Long> _remainingBytes;
+ // this is to record for each strict replica group, how many bytes they
contribute altogether since they'll be
+ // moved together
+ private final Map<Pair<Set<String>, Set<String>>, Long>
_instancePairToRequiredBytes;
+
+ private StepDiskBudget(DiskUsageBudget budget, Map<String, Long>
remainingBytes,
+ Map<Pair<Set<String>, Set<String>>, Long> instancePairToRequiredBytes)
{
+ _budget = budget;
+ _remainingBytes = remainingBytes;
+ _instancePairToRequiredBytes = instancePairToRequiredBytes;
+ }
+
+ /// Moves the instances that cannot take on all the segments sharing this
current and target instance pair to the
+ /// back of the list so that they will be picked as the next step first.
+ ///
+ /// The instances that do not fit are kept at the back rather than
dropped, so that an assignment is still produced
+ /// when none of them fits and the budget stays the only thing that
decides whether it is applied.
+ List<Triple<String, String, Integer>> retainInstancesThatFit(
+ List<Triple<String, String, Integer>> instancesInfo, Set<String>
currentInstances,
+ Set<String> targetInstances) {
+ long requiredBytes =
_instancePairToRequiredBytes.getOrDefault(Pair.of(currentInstances,
targetInstances), 0L);
+ List<Triple<String, String, Integer>> fits = new
ArrayList<>(instancesInfo.size());
+ for (Triple<String, String, Integer> instanceInfo : instancesInfo) {
+ if (_remainingBytes.getOrDefault(instanceInfo.getLeft(), 0L) >=
requiredBytes) {
+ fits.add(instanceInfo);
Review Comment:
**[P1] Use segment-sized admission checks in the non-strict path**
This filter is shared with `getNextNonStrictReplicaGroupAssignment()`
through `getNextSingleSegmentAssignment()`. It requires room for every segment
sharing the current/target instance pair, although the non-strict caller
applies batching and disk charging independently to each segment. The shared
`assignmentMap` keeps candidate destinations consistent; it does not make
adoption atomic. Whole-pair sizing is appropriate for strict, unbatched
movement, but it can reject legal individual moves here.
This causes an avoidable unsafe fallback. With
`enableStrictReplicaGroup=false`, `minAvailableReplicas=3`,
`batchSizePerServer=1`, and all replicas ONLINE, the following five-segment
assignment reproduces it. Host numbers denote distinct servers; sizes can be
interpreted as MiB.
| Segment | Size | Current hosts | Target hosts |
|---|---:|---|---|
| segment007 | 3 | 0,1,2,3 | 2,4,5,6 |
| segment017 | 1 | 2,4,5,6 | 0,1,3,4 |
| segment018 | 4 | 2,4,5,6 | 0,1,3,4 |
| segment031 | 1 | 1,2,3,4 | 0,2,5,6 |
| segment032 | 1 | 1,2,3,4 | 0,2,5,6 |
At step 5, segment017 needs 1 unit, and hosts 0, 1, and 3 have 2, 4, and 4
units available. This filter nevertheless requires 5 units for segment017 plus
segment018, rejecting all three destinations. The bounded pass stalls and the
unrestricted fallback adds 3 units to host5 and 1 to host6, both with zero
budget remaining. Changing only the non-strict prefilter's required amount to
the current segment's size in an isolated comparison reaches the target without
any overrun.
Please size the admission check to the unit the caller actually accepts or
defers: a segment in this path, and the relevant partition group for strict
batching. Preserve whole-group admission where movement is atomic.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/DefaultRebalancePreChecker.java:
##########
@@ -329,10 +333,70 @@ protected RebalancePreCheckerResult
checkDiskUtilization(PreCheckContext preChec
}
String serversGoingOver = " Servers that would go over it DURING the
rebalance: " + String.join(", ",
serversUnsafeDuringRebalance) + ".";
+ // lowDiskMode bounds the disk each server takes on to what it starts the
rebalance with, but when the rebalance
+ // cannot progress at all within those bounds it relaxes them for a step
rather than stalling. Replay the rebalance
+ // to find out whether it has to, instead of assuming lowDiskMode always
avoids the transient usage
+ Map<String, Long> serversForcedOverBudget =
getServersForcedOverDiskBudget(preCheckContext);
+ String unsafeDuringRebalance =
+ getUnsafeDiskUtilizationMessage("DURING rebalance",
serversUnsafeDuringRebalance, threshold);
+ if (serversForcedOverBudget == null) {
+ // The replay could not be finished due to exception or step limit, so
nothing was established. This is expected
+ // to be rare.
+ return RebalancePreCheckerResult.error(unsafeDuringRebalance + ".
Whether lowDiskMode can avoid it for this "
+ + "target assignment could not be established, see the controller
log. Treat the transient disk usage above "
+ + "as unsafe");
+ }
+ if (!serversForcedOverBudget.isEmpty()) {
+ return RebalancePreCheckerResult.error(unsafeDuringRebalance
+ + ". lowDiskMode cannot avoid it for this target assignment: the
rebalance cannot make progress without "
+ + "going over the disk these servers start with, by up to " +
formatBytesOverBudget(serversForcedOverBudget)
+ + ". Rebalance to a target assignment that frees up space on them
first, or add capacity");
Review Comment:
**[P2] Compare the replayed peak with the disk-utilization threshold**
A nonempty replay result establishes that the per-table start/target byte
budget is exceeded; it does not establish that a server crosses the configured
disk-utilization threshold. This branch nevertheless returns ERROR using the
earlier worst-case utilization estimate.
The new
`testOverThresholdDuringRebalanceIsAnErrorWhenLowDiskModeCannotAvoidIt`
demonstrates the false alarm: host05 starts with 7,800 MiB used out of 20,000
MiB, and the exact replay adds at most 31 MiB above its initial hosted bytes.
Its actual peak is therefore 7,831 MiB (39.155%), safely below the 50% / 10,000
MiB threshold, although the test expects ERROR and recommends adding capacity.
Please compare each server's replayed peak, combined with its existing disk
usage, against its threshold headroom. Keep a budget-constraint warning
separate if useful, and add a case where budget excess remains within the disk
threshold.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]