Skip to content

Commit 17c266e

Browse files
authored
HBASE-27380 RitDuration histogram metric is broken (#7830)
Signed-off-by: Duo Zhang <zhangduo@apache.org> Reviewed-by: Vaibhav Joshi <vjoshi@cloudera.com>
1 parent 3c446d7 commit 17c266e

5 files changed

Lines changed: 381 additions & 43 deletions

File tree

hbase-client/src/main/java/org/apache/hadoop/hbase/master/RegionState.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,6 @@ public static State convert(ClusterStatusProtos.RegionState.State protoState) {
183183
private final RegionInfo hri;
184184
private final ServerName serverName;
185185
private final State state;
186-
// The duration of region in transition
187-
private long ritDuration;
188186

189187
public static RegionState createForTesting(RegionInfo region, State state) {
190188
return new RegionState(region, state, EnvironmentEdgeManager.currentTime(), null);
@@ -195,16 +193,10 @@ public RegionState(RegionInfo region, State state, ServerName serverName) {
195193
}
196194

197195
public RegionState(RegionInfo region, State state, long stamp, ServerName serverName) {
198-
this(region, state, stamp, serverName, 0);
199-
}
200-
201-
public RegionState(RegionInfo region, State state, long stamp, ServerName serverName,
202-
long ritDuration) {
203196
this.hri = region;
204197
this.state = state;
205198
this.stamp = stamp;
206199
this.serverName = serverName;
207-
this.ritDuration = ritDuration;
208200
}
209201

210202
public State getState() {
@@ -223,19 +215,6 @@ public ServerName getServerName() {
223215
return serverName;
224216
}
225217

226-
public long getRitDuration() {
227-
return ritDuration;
228-
}
229-
230-
/**
231-
* Update the duration of region in transition
232-
* @param previousStamp previous RegionState's timestamp
233-
*/
234-
@InterfaceAudience.Private
235-
void updateRitDuration(long previousStamp) {
236-
this.ritDuration += (this.stamp - previousStamp);
237-
}
238-
239218
public boolean isClosing() {
240219
return state == State.CLOSING;
241220
}

hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/AssignmentManager.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,7 @@ public class AssignmentManager {
235235

236236
private final int forceRegionRetainmentRetries;
237237

238-
private final RegionInTransitionTracker regionInTransitionTracker =
239-
new RegionInTransitionTracker();
238+
private final RegionInTransitionTracker regionInTransitionTracker;
240239

241240
public AssignmentManager(MasterServices master, MasterRegion masterRegion) {
242241
this(master, masterRegion, new RegionStateStore(master, masterRegion));
@@ -246,6 +245,7 @@ public AssignmentManager(MasterServices master, MasterRegion masterRegion) {
246245
this.master = master;
247246
this.regionStateStore = stateStore;
248247
this.metrics = new MetricsAssignmentManager();
248+
this.regionInTransitionTracker = new RegionInTransitionTracker(metrics::updateRitDuration);
249249
this.masterRegion = masterRegion;
250250

251251
final Configuration conf = master.getConfiguration();
@@ -1717,7 +1717,7 @@ public boolean isRegionTwiceOverThreshold(final RegionInfo regionInfo) {
17171717
if (state == null) {
17181718
return false;
17191719
}
1720-
return (statTimestamp - state.getStamp()) > (ritThreshold * 2);
1720+
return (statTimestamp - state.getStamp()) > (ritThreshold * 2L);
17211721
}
17221722

17231723
protected void update(final AssignmentManager am) {
@@ -1747,7 +1747,7 @@ private void update(final Collection<RegionState> regions, final long currentTim
17471747
ritsOverThreshold = new HashMap<String, RegionState>();
17481748
}
17491749
ritsOverThreshold.put(state.getRegion().getEncodedName(), state);
1750-
totalRITsTwiceThreshold += (ritTime > (ritThreshold * 2)) ? 1 : 0;
1750+
totalRITsTwiceThreshold += (ritTime > (ritThreshold * 2L)) ? 1 : 0;
17511751
}
17521752
if (oldestRITTime < ritTime) {
17531753
oldestRITTime = ritTime;
@@ -1903,7 +1903,7 @@ public void visitRegionState(Result result, final RegionInfo regionInfo, final S
19031903
if (timeOfCrash != 0) {
19041904
regionNode.crashed(timeOfCrash);
19051905
}
1906-
regionInTransitionTracker.regionCrashed(regionNode);
1906+
regionInTransitionTracker.regionCrashed(regionNode, timeOfCrash);
19071907
}
19081908
}
19091909
};
@@ -2125,7 +2125,7 @@ public int getRegionTransitScheduledCount() {
21252125
* Get the number of regions in transition.
21262126
*/
21272127
public int getRegionsInTransitionCount() {
2128-
return regionInTransitionTracker.getRegionsInTransition().size();
2128+
return regionInTransitionTracker.getRegionsInTransitionCount();
21292129
}
21302130

21312131
public SortedSet<RegionState> getRegionsStateInTransition() {
@@ -2354,7 +2354,7 @@ public void markRegionsAsCrashed(List<RegionInfo> regionsOnCrashedServer,
23542354
RegionStateNode node = regionStates.getOrCreateRegionStateNode(regionInfo);
23552355
if (crashedServerName.equals(node.getRegionLocation())) {
23562356
node.crashed(scp.getSubmittedTime());
2357-
regionInTransitionTracker.regionCrashed(node);
2357+
regionInTransitionTracker.regionCrashed(node, scp.getSubmittedTime());
23582358
} else {
23592359
LOG.warn("Region {} should be on crashed region server {} but is recorded on {}",
23602360
regionInfo, crashedServerName, node.getRegionLocation());

hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/RegionInTransitionTracker.java

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717
*/
1818
package org.apache.hadoop.hbase.master.assignment;
1919

20+
import com.google.errorprone.annotations.RestrictedApi;
2021
import java.util.ArrayList;
2122
import java.util.List;
22-
import java.util.concurrent.ConcurrentSkipListMap;
23+
import java.util.Objects;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
import java.util.function.LongConsumer;
2326
import org.apache.hadoop.hbase.TableName;
2427
import org.apache.hadoop.hbase.client.RegionInfo;
2528
import org.apache.hadoop.hbase.client.TableState;
2629
import org.apache.hadoop.hbase.master.RegionState;
2730
import org.apache.hadoop.hbase.master.TableStateManager;
31+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
32+
import org.apache.hadoop.hbase.util.Pair;
2833
import org.apache.yetus.audience.InterfaceAudience;
2934
import org.slf4j.Logger;
3035
import org.slf4j.LoggerFactory;
@@ -36,31 +41,50 @@
3641
public class RegionInTransitionTracker {
3742
private static final Logger LOG = LoggerFactory.getLogger(RegionInTransitionTracker.class);
3843

39-
private final List<RegionState.State> DISABLE_TABLE_REGION_STATE =
44+
private static final List<RegionState.State> DISABLE_TABLE_REGION_STATE =
4045
List.of(RegionState.State.OFFLINE, RegionState.State.CLOSED);
4146

42-
private final List<RegionState.State> ENABLE_TABLE_REGION_STATE = List.of(RegionState.State.OPEN);
47+
private static final List<RegionState.State> ENABLE_TABLE_REGION_STATE =
48+
List.of(RegionState.State.OPEN);
4349

44-
// DO NOT USE containsKey()/remove() on regionInTransition with a different RegionInfo instance:
45-
// this map is ordered by RegionInfo.COMPARATOR, and that comparator includes the offline flag.
46-
// Lookups can therefore fail if the RegionInfo used as the key has a different offline value,
47-
// even when it refers to the same region. Offline value changes with splitting.
48-
private final ConcurrentSkipListMap<RegionInfo, RegionStateNode> regionInTransition =
49-
new ConcurrentSkipListMap<>(RegionInfo.COMPARATOR);
50+
// DO NOT USE containsKey()/remove() on regionInTransition with a RegionInfo instance whose
51+
// offline flag differs from the one stored as the key: RegionInfo#equals and #hashCode both
52+
// include the offline flag, so such a lookup misses even when it refers to the same region.
53+
// Offline value changes with splitting.
54+
private final ConcurrentHashMap<RegionInfo, Pair<RegionStateNode, Long>> regionInTransition =
55+
new ConcurrentHashMap<>();
5056

57+
private final LongConsumer ritDurationConsumer;
5158
private TableStateManager tableStateManager;
5259

60+
public RegionInTransitionTracker(LongConsumer ritDurationConsumer) {
61+
this.ritDurationConsumer = Objects.requireNonNull(ritDurationConsumer);
62+
}
63+
64+
@RestrictedApi(explanation = "Should only be called in tests", link = "",
65+
allowedOnPath = ".*/src/test/.*")
66+
boolean isRegionInTransition(final RegionInfo regionInfo) {
67+
return regionInTransition.containsKey(regionInfo);
68+
}
69+
5370
/**
5471
* Handles a region whose hosting RegionServer has crashed. When a RegionServer fails, all regions
5572
* it was hosting are automatically added to the RIT list since they need to be reassigned to
5673
* other servers.
74+
* @param regionStateNode the region whose hosting server crashed
75+
* @param crashTime the RIT start time to use when the region is not already in transition
76+
* (an existing entry keeps its earlier start). Passed explicitly rather
77+
* than read from {@link RegionStateNode#getLastUpdate()}, which a stale
78+
* procedure can mask. A non-positive value falls back to the node's last
79+
* update.
5780
*/
58-
public void regionCrashed(RegionStateNode regionStateNode) {
81+
public void regionCrashed(RegionStateNode regionStateNode, long crashTime) {
5982
if (isReplica(regionStateNode)) {
6083
return;
6184
}
6285

63-
if (addRegionInTransition(regionStateNode)) {
86+
long startTime = crashTime > 0 ? crashTime : regionStateNode.getLastUpdate();
87+
if (addRegionInTransition(regionStateNode, startTime)) {
6488
LOG.debug("{} added to RIT list because hosting region server is crashed ",
6589
regionStateNode.getRegionInfo().getEncodedName());
6690
}
@@ -128,11 +152,26 @@ public void handleRegionDelete(RegionInfo regionInfo) {
128152
}
129153

130154
private boolean addRegionInTransition(final RegionStateNode regionStateNode) {
131-
return regionInTransition.putIfAbsent(regionStateNode.getRegionInfo(), regionStateNode) == null;
155+
return addRegionInTransition(regionStateNode, regionStateNode.getLastUpdate());
156+
}
157+
158+
private boolean addRegionInTransition(final RegionStateNode regionStateNode, long startTime) {
159+
if (startTime <= 0) {
160+
startTime = EnvironmentEdgeManager.currentTime();
161+
}
162+
return regionInTransition.putIfAbsent(regionStateNode.getRegionInfo(),
163+
Pair.newPair(regionStateNode, startTime)) == null;
132164
}
133165

134166
private boolean removeRegionInTransition(final RegionInfo regionInfo) {
135-
return regionInTransition.remove(regionInfo) != null;
167+
Pair<RegionStateNode, Long> removed = regionInTransition.remove(regionInfo);
168+
if (removed != null) {
169+
long duration = EnvironmentEdgeManager.currentTime() - removed.getSecond();
170+
if (duration >= 0) {
171+
ritDurationConsumer.accept(duration);
172+
}
173+
}
174+
return removed != null;
136175
}
137176

138177
public void stop() {
@@ -143,8 +182,16 @@ public boolean hasRegionsInTransition() {
143182
return !regionInTransition.isEmpty();
144183
}
145184

185+
public int getRegionsInTransitionCount() {
186+
return regionInTransition.size();
187+
}
188+
146189
public List<RegionStateNode> getRegionsInTransition() {
147-
return new ArrayList<>(regionInTransition.values());
190+
List<RegionStateNode> regions = new ArrayList<>(regionInTransition.size());
191+
for (Pair<RegionStateNode, Long> entry : regionInTransition.values()) {
192+
regions.add(entry.getFirst());
193+
}
194+
return regions;
148195
}
149196

150197
public void setTableStateManager(TableStateManager tableStateManager) {
@@ -154,5 +201,4 @@ public void setTableStateManager(TableStateManager tableStateManager) {
154201
private static boolean isReplica(RegionStateNode regionStateNode) {
155202
return regionStateNode.getRegionInfo().getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID;
156203
}
157-
158204
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.master;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import org.apache.hadoop.hbase.HBaseTestingUtil;
25+
import org.apache.hadoop.hbase.ServerName;
26+
import org.apache.hadoop.hbase.TableName;
27+
import org.apache.hadoop.hbase.client.RegionInfo;
28+
import org.apache.hadoop.hbase.client.Table;
29+
import org.apache.hadoop.hbase.master.assignment.AssignmentTestingUtil;
30+
import org.apache.hadoop.hbase.testclassification.MasterTests;
31+
import org.apache.hadoop.hbase.testclassification.MediumTests;
32+
import org.apache.hadoop.hbase.util.Bytes;
33+
import org.apache.hadoop.metrics2.AbstractMetric;
34+
import org.apache.hadoop.metrics2.MetricsRecord;
35+
import org.apache.hadoop.metrics2.MetricsSource;
36+
import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl;
37+
import org.junit.jupiter.api.AfterAll;
38+
import org.junit.jupiter.api.BeforeAll;
39+
import org.junit.jupiter.api.Tag;
40+
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.TestInfo;
42+
43+
@Tag(MasterTests.TAG)
44+
@Tag(MediumTests.TAG)
45+
public class TestAssignmentManagerRitDurationMetrics {
46+
47+
private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
48+
private static final byte[] FAMILY = Bytes.toBytes("family");
49+
private static final long WAIT_TIMEOUT_MS = 10_000L;
50+
51+
private static HMaster MASTER;
52+
private static final String RIT_DURATION_NUM_OPS_METRIC = "RitDuration_num_ops";
53+
54+
@BeforeAll
55+
public static void startCluster() throws Exception {
56+
TEST_UTIL.startMiniCluster(2);
57+
MASTER = TEST_UTIL.getMiniHBaseCluster().getMaster();
58+
}
59+
60+
@AfterAll
61+
public static void after() throws Exception {
62+
TEST_UTIL.shutdownMiniCluster();
63+
}
64+
65+
@Test
66+
public void testRitDurationHistogramMetric(TestInfo testInfo) throws Exception {
67+
TableName tableName = TableName.valueOf(testInfo.getTestMethod().orElseThrow().getName());
68+
try (Table table = TEST_UTIL.createTable(tableName, FAMILY)) {
69+
RegionInfo regionInfo =
70+
MASTER.getAssignmentManager().getRegionStates().getRegionsOfTable(tableName).get(0);
71+
TEST_UTIL.waitFor(WAIT_TIMEOUT_MS,
72+
() -> !AssignmentTestingUtil.isRegionInTransition(regionInfo, MASTER.getAssignmentManager())
73+
&& MASTER.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionInfo)
74+
!= null);
75+
76+
MetricsAssignmentManagerSource amSource =
77+
MASTER.getAssignmentManager().getAssignmentManagerMetrics().getMetricsProcSource();
78+
long ritDurationNumOps =
79+
getMetricValue(snapshotMetrics(amSource), RIT_DURATION_NUM_OPS_METRIC);
80+
81+
ServerName current =
82+
MASTER.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionInfo);
83+
ServerName target = MASTER.getServerManager().getOnlineServersList().stream()
84+
.filter(sn -> !sn.equals(current)).findFirst()
85+
.orElseThrow(() -> new IllegalStateException("Need at least two regionservers"));
86+
87+
TEST_UTIL.getAdmin().move(regionInfo.getEncodedNameAsBytes(), target);
88+
TEST_UTIL.waitFor(WAIT_TIMEOUT_MS, () -> target
89+
.equals(MASTER.getAssignmentManager().getRegionStates().getRegionServerOfRegion(regionInfo))
90+
&& !AssignmentTestingUtil.isRegionInTransition(regionInfo, MASTER.getAssignmentManager()));
91+
92+
// num_ops is cumulative (never reset on snapshot); an increase proves the histogram is now
93+
// fed on RIT completion. Use >= not ==: background RIT may also add. _max is not asserted --
94+
// snapshot() resets it on every read, racing the metrics2 sampler.
95+
long ritDurationNumOpsAfter =
96+
getMetricValue(snapshotMetrics(amSource), RIT_DURATION_NUM_OPS_METRIC);
97+
assertTrue(ritDurationNumOpsAfter >= ritDurationNumOps + 1,
98+
"RitDuration histogram num_ops should increase after a region transition");
99+
}
100+
}
101+
102+
private MetricsRecord snapshotMetrics(MetricsAssignmentManagerSource amSource) {
103+
MetricsCollectorImpl collector = new MetricsCollectorImpl();
104+
assertInstanceOf(MetricsSource.class, amSource,
105+
"MetricsAssignmentManagerSource should also implement MetricsSource");
106+
((MetricsSource) amSource).getMetrics(collector, true);
107+
assertEquals(1, collector.getRecords().size());
108+
return collector.getRecords().get(0);
109+
}
110+
111+
private long getMetricValue(MetricsRecord record, String metricName) {
112+
for (AbstractMetric metric : record.metrics()) {
113+
if (metricName.equals(metric.name())) {
114+
return metric.value().longValue();
115+
}
116+
}
117+
throw new AssertionError("Metric not found: " + metricName);
118+
}
119+
}

0 commit comments

Comments
 (0)