Skip to content

Commit 4823a33

Browse files
crliaoCris LiaoCopilot
authored
Add state transition latency metrics for Helix participant (#3283)
* Add state transition latency metrics for Helix participant Add metrics to track BOOTSTRAP→STANDBY duration, OFFLINE→BOOTSTRAP duration, bootstrap failures, and max time-in-BOOTSTRAP. These fill a gap where no latency instrumentation exists today (Helix's native StateTransitionStatMonitor is denylisted from InGraph due to high cardinality). New metrics: - bootstrapToStandbyDurationMs: histogram of successful bootstrap durations - offlineToBootstrapDurationMs: histogram of offline→bootstrap transition time - bootstrapFailureCount: counter of partitions that failed during bootstrap - maxTimeInBootstrapMs: gauge of the longest currently-bootstrapping partition Motivation: INC-15835 (Helix cascading OOM) showed that we lacked visibility into how long partitions spend in BOOTSTRAP during rebalance storms. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Remove 4h stale bootstrap eviction — map bounded by partition count per instance (~1.2k max) --------- Co-authored-by: Cris Liao <crliao@linkedin.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 65a2679 commit 4823a33

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipant.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,7 @@ public void onPartitionBecomeBootstrapFromPreBootstrap(String partitionName) {
918918

919919
@Override
920920
public void onPartitionBecomeBootstrapFromOffline(String partitionName) {
921+
long transitionStartMs = System.currentTimeMillis();
921922
try {
922923
if (this.blockStateTransitionLatch != null && this.blockStateTransitionLatch.getCount() > 0) {
923924
logger.info("Bootstrapping is waiting for blockStateTransitionLatch...");
@@ -963,10 +964,13 @@ public void onPartitionBecomeBootstrapFromOffline(String partitionName) {
963964
logger.error("Waiting for state transition to be unblocked was interrupted", e);
964965
} catch (Exception e) {
965966
localPartitionAndState.put(partitionName, ReplicaState.ERROR);
967+
participantMetrics.recordBootstrapFailure(partitionName);
966968
throw e;
967969
}
968970
logger.info("Before setting partition {} to bootstrap", partitionName);
969971
localPartitionAndState.put(partitionName, ReplicaState.BOOTSTRAP);
972+
participantMetrics.recordOfflineToBootstrapDuration(System.currentTimeMillis() - transitionStartMs);
973+
participantMetrics.recordBootstrapStart(partitionName);
970974
participantMetrics.decStateTransitionMetric(partitionName, ReplicaState.OFFLINE, ReplicaState.BOOTSTRAP);
971975
}
972976

@@ -992,13 +996,16 @@ public void onPartitionBecomeStandbyFromBootstrap(String partitionName) {
992996
} catch (InterruptedException e) {
993997
logger.error("Bootstrap was interrupted on partition {}", partitionName);
994998
localPartitionAndState.put(partitionName, ReplicaState.ERROR);
999+
participantMetrics.recordBootstrapFailure(partitionName);
9951000
throw new StateTransitionException("Bootstrap failed or was interrupted", BootstrapFailure);
9961001
} catch (StateTransitionException e) {
9971002
logger.error("Bootstrap didn't complete on partition {}", partitionName, e);
9981003
localPartitionAndState.put(partitionName, ReplicaState.ERROR);
1004+
participantMetrics.recordBootstrapFailure(partitionName);
9991005
throw e;
10001006
}
10011007
localPartitionAndState.put(partitionName, ReplicaState.STANDBY);
1008+
participantMetrics.recordBootstrapComplete(partitionName);
10021009
participantMetrics.decStateTransitionMetric(partitionName, ReplicaState.BOOTSTRAP, ReplicaState.STANDBY);
10031010
}
10041011

ambry-clustermap/src/main/java/com/github/ambry/clustermap/HelixParticipantMetrics.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515

1616
import com.codahale.metrics.Counter;
1717
import com.codahale.metrics.Gauge;
18+
import com.codahale.metrics.Histogram;
1819
import com.codahale.metrics.MetricRegistry;
1920
import java.util.EnumSet;
2021
import java.util.HashMap;
2122
import java.util.Map;
23+
import java.util.concurrent.ConcurrentHashMap;
2224

2325

2426
/**
@@ -38,6 +40,16 @@ class HelixParticipantMetrics {
3840

3941
public final Counter updateDiskCapacityCounter;
4042

43+
// --- State Transition Latency Metrics ---
44+
// Tracks when each partition entered BOOTSTRAP state (epoch ms)
45+
private final ConcurrentHashMap<String, Long> bootstrapStartTimeMs = new ConcurrentHashMap<>();
46+
// Histogram of successful BOOTSTRAP→STANDBY durations in milliseconds
47+
final Histogram bootstrapToStandbyDurationMs;
48+
// Histogram of OFFLINE→BOOTSTRAP transition durations in milliseconds
49+
final Histogram offlineToBootstrapDurationMs;
50+
// Counter for bootstrap failures (partition went to ERROR from BOOTSTRAP)
51+
final Counter bootstrapFailureCount;
52+
4153
final Map<String, Counter> partitionTransitionToCount;
4254

4355
HelixParticipantMetrics(MetricRegistry metricRegistry, String zkConnectStr,
@@ -72,6 +84,17 @@ class HelixParticipantMetrics {
7284
updateDiskCapacityCounter =
7385
metricRegistry.counter(MetricRegistry.name(HelixParticipant.class, "updateDiskCapacityCount"));
7486
partitionTransitionToCount = new HashMap<>();
87+
88+
// State transition latency metrics
89+
bootstrapToStandbyDurationMs = metricRegistry.histogram(
90+
MetricRegistry.name(HelixParticipant.class, "bootstrapToStandbyDurationMs" + zkSuffix));
91+
offlineToBootstrapDurationMs = metricRegistry.histogram(
92+
MetricRegistry.name(HelixParticipant.class, "offlineToBootstrapDurationMs" + zkSuffix));
93+
bootstrapFailureCount = metricRegistry.counter(
94+
MetricRegistry.name(HelixParticipant.class, "bootstrapFailureCount" + zkSuffix));
95+
Gauge<Long> maxTimeInBootstrap = this::computeMaxTimeInBootstrap;
96+
registry.gauge(MetricRegistry.name(HelixParticipant.class, "maxTimeInBootstrapMs" + zkSuffix),
97+
() -> maxTimeInBootstrap);
7598
}
7699

77100
/**
@@ -95,6 +118,54 @@ private int getReplicaCountInState(ReplicaState state) {
95118
return replicaCountByState.get(state);
96119
}
97120

121+
/**
122+
* Record that a partition has entered BOOTSTRAP state.
123+
* @param partitionName the partition that entered BOOTSTRAP
124+
*/
125+
void recordBootstrapStart(String partitionName) {
126+
bootstrapStartTimeMs.put(partitionName, System.currentTimeMillis());
127+
}
128+
129+
/**
130+
* Record that a partition has completed BOOTSTRAP→STANDBY transition successfully.
131+
* @param partitionName the partition that reached STANDBY
132+
*/
133+
void recordBootstrapComplete(String partitionName) {
134+
Long startTime = bootstrapStartTimeMs.remove(partitionName);
135+
if (startTime != null) {
136+
bootstrapToStandbyDurationMs.update(System.currentTimeMillis() - startTime);
137+
}
138+
}
139+
140+
/**
141+
* Record that a partition failed during BOOTSTRAP (went to ERROR).
142+
* @param partitionName the partition that failed
143+
*/
144+
void recordBootstrapFailure(String partitionName) {
145+
bootstrapStartTimeMs.remove(partitionName);
146+
bootstrapFailureCount.inc();
147+
}
148+
149+
/**
150+
* Record the duration of an OFFLINE→BOOTSTRAP transition.
151+
* @param durationMs time in milliseconds the transition took
152+
*/
153+
void recordOfflineToBootstrapDuration(long durationMs) {
154+
offlineToBootstrapDurationMs.update(durationMs);
155+
}
156+
157+
/**
158+
* Compute the maximum time any partition has been in BOOTSTRAP state.
159+
*/
160+
private long computeMaxTimeInBootstrap() {
161+
long now = System.currentTimeMillis();
162+
long maxDuration = 0;
163+
for (Map.Entry<String, Long> entry : bootstrapStartTimeMs.entrySet()) {
164+
maxDuration = Math.max(maxDuration, now - entry.getValue());
165+
}
166+
return maxDuration;
167+
}
168+
98169
/**
99170
* Creates and increments the metric object for given partition's state transition
100171
* @param partitionName partition name

0 commit comments

Comments
 (0)