Skip to content

Commit 6669be8

Browse files
sjainitCopilot
andcommitted
Add WAGED rebalance build/solve latency split and partial movement counter
Improve WAGED rebalancer observability on the global (baseline) and partial (best possible) paths: - Split the existing per-phase latency into a model-build span and an algorithm-solve span for each path, via four new latency gauges (GlobalBaselineCalcBuild/SolveLatencyGauge and PartialRebalanceBuild/SolveLatencyGauge). This lets a slow rebalance be attributed to cluster-model construction versus constraint solving. The existing overall latency gauges are retained for backward compatibility. - Count replica placements in the new best possible assignment that changed relative to the previous best possible assignment (PartialRebalanceReplicaMovementCounter) using a new ResourceUsageCalculator.countReplicaMovements helper. The best possible assignment is what actually drives state transitions, so this surfaces the churn / state-transition blast radius of each partial rebalance at intent-time (before throttling), attributable to the partial phase -- complementing the existing downstream Pending*RebalanceReplica gauges. Add a unit test for countReplicaMovements and extend TestWagedRebalancerMetrics to assert the new latency gauges and movement counter are populated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4875331 commit 6669be8

6 files changed

Lines changed: 198 additions & 4 deletions

File tree

helix-core/src/main/java/org/apache/helix/controller/rebalancer/util/ResourceUsageCalculator.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* under the License.
2020
*/
2121

22+
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.Map;
2425

@@ -143,6 +144,49 @@ public static double measureBaselineDivergence(Map<String, ResourceAssignment> b
143144
: (1.0d - (double) numMatchedReplicas / (double) numTotalBestPossibleReplicas);
144145
}
145146

147+
/**
148+
* Counts the number of replica placements in {@code newAssignment} that are new or changed
149+
* relative to {@code oldAssignment}. A replica placement (identified by partition name and
150+
* instance name) counts as a movement when the instance does not hold that partition's replica in
151+
* the previous assignment, or holds it in a different state. This approximates the amount of churn
152+
* a rebalance introduces (i.e. the number of state-transition messages it will generate), which is
153+
* a useful blast-radius signal independent of how long the rebalance took.
154+
*
155+
* Note: replicas that were dropped (present in {@code oldAssignment} but absent in
156+
* {@code newAssignment}) are intentionally not counted; the metric measures work introduced by the
157+
* new assignment. A replica whose instance is unchanged but whose state changed (e.g. a
158+
* SLAVE promoted to MASTER) is counted, because it still produces a state transition.
159+
*
160+
* @param oldAssignment the previous assignment (e.g. previous baseline or best possible)
161+
* @param newAssignment the newly computed assignment
162+
* @return the number of new or changed replica placements introduced by {@code newAssignment}
163+
*/
164+
public static long countReplicaMovements(Map<String, ResourceAssignment> oldAssignment,
165+
Map<String, ResourceAssignment> newAssignment) {
166+
long movements = 0L;
167+
for (Map.Entry<String, ResourceAssignment> resourceEntry : newAssignment.entrySet()) {
168+
String resourceName = resourceEntry.getKey();
169+
Map<String, Map<String, String>> newPartitions =
170+
resourceEntry.getValue().getRecord().getMapFields();
171+
ResourceAssignment oldResource = oldAssignment.get(resourceName);
172+
Map<String, Map<String, String>> oldPartitions =
173+
oldResource == null ? Collections.emptyMap() : oldResource.getRecord().getMapFields();
174+
175+
for (Map.Entry<String, Map<String, String>> partitionEntry : newPartitions.entrySet()) {
176+
Map<String, String> newReplicas = partitionEntry.getValue();
177+
Map<String, String> oldReplicas =
178+
oldPartitions.getOrDefault(partitionEntry.getKey(), Collections.emptyMap());
179+
for (Map.Entry<String, String> replicaEntry : newReplicas.entrySet()) {
180+
// New or changed placement for this (partition, instance) => a movement.
181+
if (!replicaEntry.getValue().equals(oldReplicas.get(replicaEntry.getKey()))) {
182+
movements++;
183+
}
184+
}
185+
}
186+
}
187+
return movements;
188+
}
189+
146190
/**
147191
* Calculates average partition weight per capacity key for a resource config. Example as below:
148192
* Input =

helix-core/src/main/java/org/apache/helix/controller/rebalancer/waged/GlobalRebalanceRunner.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class GlobalRebalanceRunner implements AutoCloseable {
8080
private final LatencyMetric _writeLatency;
8181
private final CountMetric _baselineCalcCounter;
8282
private final LatencyMetric _baselineCalcLatency;
83+
// Sub-phase latencies of _baselineCalcLatency: time spent building the cluster model vs. solving
84+
// it with the assignment algorithm. Together they let a slow baseline be attributed to either half.
85+
private final LatencyMetric _baselineCalcBuildLatency;
86+
private final LatencyMetric _baselineCalcSolveLatency;
8387
// Reporter that ticks RebalanceFailureCounter plus the per-FailureCategory counters on both
8488
// the Rebalancer-domain and ClusterStatus-domain MBeans. Owned by WagedRebalancer; injected
8589
// so the runner doesn't need a direct ClusterStatusMonitor reference. For the Baseline phase this
@@ -114,6 +118,12 @@ public GlobalRebalanceRunner(AssignmentManager assignmentManager,
114118
_baselineCalcLatency = metricCollector.getMetric(
115119
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.GlobalBaselineCalcLatencyGauge.name(),
116120
LatencyMetric.class);
121+
_baselineCalcBuildLatency = metricCollector.getMetric(
122+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.GlobalBaselineCalcBuildLatencyGauge.name(),
123+
LatencyMetric.class);
124+
_baselineCalcSolveLatency = metricCollector.getMetric(
125+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.GlobalBaselineCalcSolveLatencyGauge.name(),
126+
LatencyMetric.class);
117127
_asyncFailureReporter = asyncFailureReporter;
118128
_baselineComputeStatusReporter = baselineComputeStatusReporter;
119129
_asyncGlobalRebalanceEnabled = isAsyncGlobalRebalanceEnabled;
@@ -217,16 +227,25 @@ private void doGlobalRebalance(ResourceControllerDataProvider clusterData,
217227
Map<String, ResourceAssignment> currentBaseline =
218228
_assignmentManager.getBaselineAssignment(_assignmentMetadataStore, currentStateOutput, resourceMap.keySet());
219229
ClusterModel clusterModel;
230+
_baselineCalcBuildLatency.startMeasuringLatency();
220231
try {
221232
clusterModel = ClusterModelProvider.generateClusterModelForBaseline(clusterData, resourceMap,
222233
allAssignableInstances, clusterChanges, currentBaseline);
223234
} catch (Exception ex) {
224235
throw new HelixRebalanceException("Failed to generate cluster model for global rebalance.",
225236
HelixRebalanceException.Type.INVALID_CLUSTER_STATUS,
226237
HelixRebalanceException.FailureCategory.INVALID_CLUSTER_CONFIG, ex);
238+
} finally {
239+
_baselineCalcBuildLatency.endMeasuringLatency();
227240
}
228241

229-
Map<String, ResourceAssignment> newBaseline = WagedRebalanceUtil.calculateAssignment(clusterModel, algorithm);
242+
_baselineCalcSolveLatency.startMeasuringLatency();
243+
Map<String, ResourceAssignment> newBaseline;
244+
try {
245+
newBaseline = WagedRebalanceUtil.calculateAssignment(clusterModel, algorithm);
246+
} finally {
247+
_baselineCalcSolveLatency.endMeasuringLatency();
248+
}
230249
boolean isBaselineChanged =
231250
_assignmentMetadataStore != null && _assignmentMetadataStore.isBaselineChanged(newBaseline);
232251
// Write the new baseline to metadata store

helix-core/src/main/java/org/apache/helix/controller/rebalancer/waged/PartialRebalanceRunner.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.function.Consumer;
3131
import org.apache.helix.HelixRebalanceException;
3232
import org.apache.helix.controller.dataproviders.ResourceControllerDataProvider;
33+
import org.apache.helix.controller.rebalancer.util.ResourceUsageCalculator;
3334
import org.apache.helix.controller.rebalancer.util.WagedRebalanceUtil;
3435
import org.apache.helix.controller.rebalancer.waged.model.ClusterModel;
3536
import org.apache.helix.controller.rebalancer.waged.model.ClusterModelProvider;
@@ -78,6 +79,14 @@ class PartialRebalanceRunner implements AutoCloseable {
7879
private final Runnable _partialRebalanceSuccessReporter;
7980
private final CountMetric _partialRebalanceCounter;
8081
private final LatencyMetric _partialRebalanceLatency;
82+
// Sub-phase latencies of _partialRebalanceLatency: time spent building the cluster model vs.
83+
// solving it with the assignment algorithm. Together they let a slow partial rebalance be
84+
// attributed to either half.
85+
private final LatencyMetric _partialRebalanceBuildLatency;
86+
private final LatencyMetric _partialRebalanceSolveLatency;
87+
// Count of replica placements in the new best possible assignment that differ from the previous
88+
// one, i.e. the churn (state-transition blast radius) this partial rebalance introduces.
89+
private final CountMetric _partialRebalanceReplicaMovementCounter;
8190

8291
private boolean _asyncPartialRebalanceEnabled;
8392
private Future<Boolean> _asyncPartialRebalanceResult;
@@ -105,6 +114,18 @@ public PartialRebalanceRunner(AssignmentManager assignmentManager,
105114
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceLatencyGauge
106115
.name(),
107116
LatencyMetric.class);
117+
_partialRebalanceBuildLatency = metricCollector.getMetric(
118+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceBuildLatencyGauge
119+
.name(),
120+
LatencyMetric.class);
121+
_partialRebalanceSolveLatency = metricCollector.getMetric(
122+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceSolveLatencyGauge
123+
.name(),
124+
LatencyMetric.class);
125+
_partialRebalanceReplicaMovementCounter = metricCollector.getMetric(
126+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceReplicaMovementCounter
127+
.name(),
128+
CountMetric.class);
108129
_baselineDivergenceGauge = metricCollector.getMetric(
109130
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.BaselineDivergenceGauge.name(),
110131
BaselineDivergenceGauge.class);
@@ -199,6 +220,7 @@ private void doPartialRebalance(ResourceControllerDataProvider clusterData, Map<
199220
_assignmentManager.getBestPossibleAssignment(_assignmentMetadataStore, currentStateOutput,
200221
resourceMap.keySet());
201222
ClusterModel clusterModel;
223+
_partialRebalanceBuildLatency.startMeasuringLatency();
202224
try {
203225
clusterModel = ClusterModelProvider
204226
.generateClusterModelForPartialRebalance(clusterData, resourceMap, activeNodes,
@@ -207,8 +229,25 @@ private void doPartialRebalance(ResourceControllerDataProvider clusterData, Map<
207229
throw new HelixRebalanceException("Failed to generate cluster model for partial rebalance.",
208230
HelixRebalanceException.Type.INVALID_CLUSTER_STATUS,
209231
HelixRebalanceException.FailureCategory.INVALID_CLUSTER_CONFIG, ex);
232+
} finally {
233+
_partialRebalanceBuildLatency.endMeasuringLatency();
234+
}
235+
_partialRebalanceSolveLatency.startMeasuringLatency();
236+
Map<String, ResourceAssignment> newAssignment;
237+
try {
238+
newAssignment = WagedRebalanceUtil.calculateAssignment(clusterModel, algorithm);
239+
} finally {
240+
_partialRebalanceSolveLatency.endMeasuringLatency();
241+
}
242+
boolean isBestPossibleChanged = _assignmentMetadataStore != null
243+
&& _assignmentMetadataStore.isBestPossibleChanged(newAssignment);
244+
// Report how many replica placements changed relative to the previous best possible assignment.
245+
// This best possible assignment is what actually drives state transitions, so this is the churn
246+
// magnitude the cluster will experience. Skip the diff pass when nothing changed.
247+
if (isBestPossibleChanged) {
248+
_partialRebalanceReplicaMovementCounter.increment(ResourceUsageCalculator
249+
.countReplicaMovements(currentBestPossibleAssignment, newAssignment));
210250
}
211-
Map<String, ResourceAssignment> newAssignment = WagedRebalanceUtil.calculateAssignment(clusterModel, algorithm);
212251

213252
// Asynchronously report baseline divergence metric before persisting to metadata store,
214253
// just in case if persisting fails, we still have the metric.
@@ -223,7 +262,7 @@ private void doPartialRebalance(ResourceControllerDataProvider clusterData, Map<
223262
currentBaseline, newAssignmentCopy);
224263

225264
boolean bestPossibleUpdateSuccessful = false;
226-
if (_assignmentMetadataStore != null && _assignmentMetadataStore.isBestPossibleChanged(newAssignment)) {
265+
if (isBestPossibleChanged) {
227266
// This will not persist the new Best Possible Assignment into ZK. It will only update the in-memory cache.
228267
// If this is done successfully, the new Best Possible Assignment will be persisted into ZK the next time that
229268
// the pipeline is triggered. We schedule the pipeline to run below.

helix-core/src/main/java/org/apache/helix/monitoring/metrics/WagedRebalancerMetricCollector.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ public enum WagedRebalancerMetricNames {
4646
EmergencyRebalanceLatencyGauge,
4747
RebalanceOverwriteLatencyGauge,
4848

49+
// Sub-phase breakdown of the Global Baseline and Partial rebalance latencies above. Each of the
50+
// two phases is timed separately so a slow rebalance can be attributed to either building the
51+
// cluster model (constructing assignable nodes/replicas and pinning existing placement) or
52+
// solving it (running the constraint-based assignment algorithm).
53+
GlobalBaselineCalcBuildLatencyGauge,
54+
GlobalBaselineCalcSolveLatencyGauge,
55+
PartialRebalanceBuildLatencyGauge,
56+
PartialRebalanceSolveLatencyGauge,
57+
4958
// The following latency metrics are related to AssignmentMetadataStore
5059
StateReadLatencyGauge,
5160
StateWriteLatencyGauge,
@@ -87,7 +96,13 @@ public enum WagedRebalancerMetricNames {
8796
GlobalBaselineCalcCounter,
8897
PartialRebalanceCounter,
8998
EmergencyRebalanceCounter,
90-
RebalanceOverwriteCounter
99+
RebalanceOverwriteCounter,
100+
101+
// Count of replica placements in the new best possible assignment that changed relative to the
102+
// previous best possible assignment, i.e. the churn (state-transition blast radius) that the
103+
// partial rebalance introduces (the best possible assignment is what actually drives state
104+
// transitions in the cluster).
105+
PartialRebalanceReplicaMovementCounter
91106
}
92107

93108
public WagedRebalancerMetricCollector(String clusterName) {
@@ -129,6 +144,20 @@ private void createMetrics() {
129144
LatencyMetric rebalanceOverwriteLatencyGauge =
130145
new RebalanceLatencyGauge(WagedRebalancerMetricNames.RebalanceOverwriteLatencyGauge.name(),
131146
getResetIntervalInMs());
147+
LatencyMetric globalBaselineCalcBuildLatencyGauge =
148+
new RebalanceLatencyGauge(
149+
WagedRebalancerMetricNames.GlobalBaselineCalcBuildLatencyGauge.name(),
150+
getResetIntervalInMs());
151+
LatencyMetric globalBaselineCalcSolveLatencyGauge =
152+
new RebalanceLatencyGauge(
153+
WagedRebalancerMetricNames.GlobalBaselineCalcSolveLatencyGauge.name(),
154+
getResetIntervalInMs());
155+
LatencyMetric partialRebalanceBuildLatencyGauge =
156+
new RebalanceLatencyGauge(WagedRebalancerMetricNames.PartialRebalanceBuildLatencyGauge.name(),
157+
getResetIntervalInMs());
158+
LatencyMetric partialRebalanceSolveLatencyGauge =
159+
new RebalanceLatencyGauge(WagedRebalancerMetricNames.PartialRebalanceSolveLatencyGauge.name(),
160+
getResetIntervalInMs());
132161
LatencyMetric stateReadLatencyGauge =
133162
new RebalanceLatencyGauge(WagedRebalancerMetricNames.StateReadLatencyGauge.name(),
134163
getResetIntervalInMs());
@@ -177,12 +206,19 @@ private void createMetrics() {
177206
new RebalanceCounter(WagedRebalancerMetricNames.EmergencyRebalanceCounter.name());
178207
CountMetric rebalanceOverwriteCounter =
179208
new RebalanceCounter(WagedRebalancerMetricNames.RebalanceOverwriteCounter.name());
209+
CountMetric partialRebalanceReplicaMovementCounter =
210+
new RebalanceCounter(
211+
WagedRebalancerMetricNames.PartialRebalanceReplicaMovementCounter.name());
180212

181213
// Add metrics to WagedRebalancerMetricCollector
182214
addMetric(globalBaselineCalcLatencyGauge);
183215
addMetric(partialRebalanceLatencyGauge);
184216
addMetric(emergencyRebalanceLatencyGauge);
185217
addMetric(rebalanceOverwriteLatencyGauge);
218+
addMetric(globalBaselineCalcBuildLatencyGauge);
219+
addMetric(globalBaselineCalcSolveLatencyGauge);
220+
addMetric(partialRebalanceBuildLatencyGauge);
221+
addMetric(partialRebalanceSolveLatencyGauge);
186222
addMetric(stateReadLatencyGauge);
187223
addMetric(stateWriteLatencyGauge);
188224
addMetric(baselineDivergenceGauge);
@@ -206,5 +242,6 @@ private void createMetrics() {
206242
addMetric(partialRebalanceCounter);
207243
addMetric(emergencyRebalanceCounter);
208244
addMetric(rebalanceOverwriteCounter);
245+
addMetric(partialRebalanceReplicaMovementCounter);
209246
}
210247
}

helix-core/src/test/java/org/apache/helix/controller/rebalancer/util/TestResourceUsageCalculator.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,40 @@ public void testMeasureBaselineDivergence(Map<String, Map<String, Map<String, St
6060
0.0d);
6161
}
6262

63+
@Test
64+
public void testCountReplicaMovements() {
65+
Map<String, ResourceAssignment> previous = buildResourceAssignment(ImmutableMap.of("DB",
66+
ImmutableMap.of(
67+
"DB_0", ImmutableMap.of("host_A", "MASTER", "host_B", "SLAVE", "host_C", "SLAVE"),
68+
"DB_1", ImmutableMap.of("host_A", "MASTER", "host_B", "SLAVE", "host_C", "SLAVE"))));
69+
70+
// Identical assignment => no movements.
71+
Assert.assertEquals(ResourceUsageCalculator.countReplicaMovements(previous, previous), 0L);
72+
73+
// Empty new assignment => nothing was placed, so nothing moved.
74+
Assert.assertEquals(
75+
ResourceUsageCalculator.countReplicaMovements(previous, Collections.emptyMap()), 0L);
76+
77+
// Empty previous assignment => every replica in the new assignment is a fresh placement (2 x 3).
78+
Assert.assertEquals(
79+
ResourceUsageCalculator.countReplicaMovements(Collections.emptyMap(), previous), 6L);
80+
81+
// Mixed changes:
82+
// - DB_0: host_C's SLAVE relocates to host_D (1 movement). The host_C drop is NOT counted.
83+
// - DB_1: host_A MASTER->SLAVE and host_B SLAVE->MASTER (2 state changes); host_C unchanged.
84+
// Total = 3.
85+
Map<String, ResourceAssignment> updated = buildResourceAssignment(ImmutableMap.of("DB",
86+
ImmutableMap.of(
87+
"DB_0", ImmutableMap.of("host_A", "MASTER", "host_B", "SLAVE", "host_D", "SLAVE"),
88+
"DB_1", ImmutableMap.of("host_A", "SLAVE", "host_B", "MASTER", "host_C", "SLAVE"))));
89+
Assert.assertEquals(ResourceUsageCalculator.countReplicaMovements(previous, updated), 3L);
90+
91+
// A resource absent from the previous assignment => all its replicas count as movements.
92+
Map<String, ResourceAssignment> newResource = buildResourceAssignment(ImmutableMap.of("DB2",
93+
ImmutableMap.of("DB2_0", ImmutableMap.of("host_A", "MASTER", "host_B", "SLAVE"))));
94+
Assert.assertEquals(ResourceUsageCalculator.countReplicaMovements(previous, newResource), 2L);
95+
}
96+
6397
@Test
6498
public void testCalculateAveragePartitionWeight() {
6599
Map<String, Map<String, Integer>> partitionCapacityMap = ImmutableMap.of(

helix-core/src/test/java/org/apache/helix/controller/rebalancer/waged/TestWagedRebalancerMetrics.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.apache.helix.monitoring.metrics.MetricCollector;
6060
import org.apache.helix.monitoring.metrics.WagedRebalancerMetricCollector;
6161
import org.apache.helix.monitoring.metrics.model.CountMetric;
62+
import org.apache.helix.monitoring.metrics.model.LatencyMetric;
6263
import org.apache.helix.monitoring.metrics.model.RatioMetric;
6364
import org.mockito.stubbing.Answer;
6465
import org.testng.Assert;
@@ -205,6 +206,26 @@ public void testWagedRebalanceMetrics()
205206
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceCounter.name(),
206207
CountMetric.class).getLastEmittedMetricValue(), 1L);
207208

209+
// O1: both the build and solve sub-phase latencies were measured for the baseline and the
210+
// partial rebalance (a measured gauge reports >= 0; an unmeasured one would still be -1).
211+
Assert.assertTrue((long) metricCollector.getMetric(
212+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.GlobalBaselineCalcBuildLatencyGauge
213+
.name(), LatencyMetric.class).getLastEmittedMetricValue() >= 0L);
214+
Assert.assertTrue((long) metricCollector.getMetric(
215+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.GlobalBaselineCalcSolveLatencyGauge
216+
.name(), LatencyMetric.class).getLastEmittedMetricValue() >= 0L);
217+
Assert.assertTrue((long) metricCollector.getMetric(
218+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceBuildLatencyGauge
219+
.name(), LatencyMetric.class).getLastEmittedMetricValue() >= 0L);
220+
Assert.assertTrue((long) metricCollector.getMetric(
221+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceSolveLatencyGauge
222+
.name(), LatencyMetric.class).getLastEmittedMetricValue() >= 0L);
223+
224+
// O2: the from-scratch assignment moved replicas, so the partial movement counter is positive.
225+
Assert.assertTrue((long) metricCollector.getMetric(
226+
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.PartialRebalanceReplicaMovementCounter
227+
.name(), CountMetric.class).getLastEmittedMetricValue() > 0L);
228+
208229
// Wait for asyncReportBaselineDivergenceGauge to complete and verify.
209230
Assert.assertTrue(TestHelper.verify(() -> (double) metricCollector.getMetric(
210231
WagedRebalancerMetricCollector.WagedRebalancerMetricNames.BaselineDivergenceGauge.name(),

0 commit comments

Comments
 (0)