Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
271ff36
Add support for region level Scan Metrics
Mar 30, 2025
32a7e8e
Make TestScanMetricsByRegion work for sync client
Mar 30, 2025
a91e6fe
Fix checkstyle issues
Mar 31, 2025
c88b6d7
Fix spot bug
Mar 31, 2025
5babeaa
Apply spotless fixes
Mar 31, 2025
bab2be1
Minor nit
Mar 31, 2025
8fc1be9
Extend ScanMetrics to capture region level metrics
Apr 24, 2025
fdc8c5f
Refactor ScanMetrics by region usage
Apr 25, 2025
666e191
Do non-test changes to refactor ScanMetrics by region
Apr 25, 2025
37e2c24
Add test coverage for new ScanMetricsByRegion design
Apr 28, 2025
07c5628
Add test covrage for non-snapshot scanners
Apr 29, 2025
0457771
Complete adding test coverage
Apr 29, 2025
4c840bb
Run spotless:apply
Apr 29, 2025
e1f0470
Remove extra changes
Apr 29, 2025
4fa7f79
Merge remote-tracking branch 'apache/master' into granular-scan-metrics
Apr 29, 2025
9f04f29
Remover unwanted changes
Apr 29, 2025
87c6cf9
Run spotless apply
Apr 29, 2025
0c65103
Fix spotbugs check
Apr 30, 2025
dfb41dd
Add test coverage for RIT scenario
May 3, 2025
011bae7
Do spotless:apply
May 3, 2025
0cdc421
Empty commit to trigger build
May 4, 2025
392dca3
Fix style checks
May 9, 2025
9a9bd25
Fix spotless
May 9, 2025
54189d8
Add getters for ScanMetrics to fix spotbugs
May 11, 2025
bca08ef
Make API backward compatible
Jun 5, 2025
2c7788c
Spotless:apply
Jun 5, 2025
4ded690
Address the remaining comments not invovling redesign
Jun 5, 2025
83081cb
Address the remaining comments not invovling redesign
Jun 5, 2025
746029e
Spotless:apply
Jun 5, 2025
4d2fdf8
Add UT in a separate class
Jun 6, 2025
286d7c9
Address Viraj's comments
Jun 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ linklint/
**/*.log
tmp
**/.flattened-pom.xml
.vscode/
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
@InterfaceAudience.Private
public abstract class AbstractClientScanner implements ResultScanner {
protected ScanMetrics scanMetrics;
private boolean isScanMetricsByRegionEnabled = false;

/**
* Check and initialize if application wants to collect scan metrics
Expand All @@ -34,6 +35,9 @@ protected void initScanMetrics(Scan scan) {
// check if application wants to collect scan metrics
if (scan.isScanMetricsEnabled()) {
scanMetrics = new ScanMetrics();
if (scan.isScanMetricsByRegionEnabled()) {
isScanMetricsByRegionEnabled = true;
}
}
Comment on lines 36 to 41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should make it so that if user enables scanMetricsByRegion, then scanMetrics also gets initialized (even if scanMetricsEnabled is not true).
On the other hand, the current approach is also good but now we need user to set both scanMetrics and scanMetricsByRegion to true, two flags for one feature?

I don't have strong opinion here btw, i was just thinking about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also thought the same but I also didn't have any strong opinion so, kept it this way as was wondering what to do or should I handle the case when isScanMetricsEnabled() is false and isScanMetricsByRegionEnabled() is true given I will be setting scanMetricsEnabled if scanMetricsByRegion are enabled. So, kept it simple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe single flag per feature would make it simpler from user's viewpoint:

  1. Enable flag scanMetrics to enable scan metrics
  2. Enable flag scanMetricsByRegion to enable per region scan metrics

Let's also wait for Nick's suggestion here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to have a flag per feature (you have to retain the old flag for backwards compatibility). I think it also makes sense that that isScanMetricsByRegionEnabled=true implies isScanMetricsEnabled=true -- enabling the new feature implicitly enables the old feature.

How you capture and accurately propagate this logic through the Scan state management will be an (perhaps annoying) implementation detail.

}

Expand All @@ -46,4 +50,12 @@ protected void initScanMetrics(Scan scan) {
public ScanMetrics getScanMetrics() {
return scanMetrics;
}

protected void setIsScanMetricsByRegionEnabled(boolean isScanMetricsByRegionEnabled) {
this.isScanMetricsByRegionEnabled = isScanMetricsByRegionEnabled;
}

protected boolean isScanMetricsByRegionEnabled() {
return isScanMetricsByRegionEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class AsyncClientScanner {

private final Map<String, byte[]> requestAttributes;

private final boolean isScanMetricsByRegionEnabled;

public AsyncClientScanner(Scan scan, AdvancedScanResultConsumer consumer, TableName tableName,
AsyncConnectionImpl conn, Timer retryTimer, long pauseNs, long pauseNsForServerOverloaded,
int maxAttempts, long scanTimeoutNs, long rpcTimeoutNs, int startLogErrorsCnt,
Expand All @@ -118,12 +120,17 @@ public AsyncClientScanner(Scan scan, AdvancedScanResultConsumer consumer, TableN
this.startLogErrorsCnt = startLogErrorsCnt;
this.resultCache = createScanResultCache(scan);
this.requestAttributes = requestAttributes;
boolean isScanMetricsByRegionEnabled = false;
if (scan.isScanMetricsEnabled()) {
this.scanMetrics = new ScanMetrics();
consumer.onScanMetricsCreated(scanMetrics);
if (this.scan.isScanMetricsByRegionEnabled()) {
isScanMetricsByRegionEnabled = true;
}
} else {
this.scanMetrics = null;
}
this.isScanMetricsByRegionEnabled = isScanMetricsByRegionEnabled;

/*
* Assumes that the `start()` method is called immediately after construction. If this is no
Expand Down Expand Up @@ -250,6 +257,9 @@ private long getPrimaryTimeoutNs() {
}

private void openScanner() {
if (this.isScanMetricsByRegionEnabled) {
scanMetrics.moveToNextRegion();
}
incRegionCountMetrics(scanMetrics);
openScannerTries.set(1);
addListener(timelineConsistentRead(conn.getLocator(), tableName, scan, scan.getStartRow(),
Expand All @@ -265,6 +275,11 @@ private void openScanner() {
span.end();
}
}
if (this.isScanMetricsByRegionEnabled) {
HRegionLocation loc = resp.loc;
this.scanMetrics.initScanMetricsRegionInfo(loc.getRegion().getEncodedName(),
loc.getServerName());
}
startScan(resp);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,19 +341,19 @@ static void incRPCCallsMetrics(ScanMetrics scanMetrics, boolean isRegionServerRe
if (scanMetrics == null) {
return;
}
scanMetrics.countOfRPCcalls.incrementAndGet();
scanMetrics.addToCounter(ScanMetrics.RPC_CALLS_METRIC_NAME, 1);
if (isRegionServerRemote) {
scanMetrics.countOfRemoteRPCcalls.incrementAndGet();
scanMetrics.addToCounter(ScanMetrics.REMOTE_RPC_CALLS_METRIC_NAME, 1);
}
}

static void incRPCRetriesMetrics(ScanMetrics scanMetrics, boolean isRegionServerRemote) {
if (scanMetrics == null) {
return;
}
scanMetrics.countOfRPCRetries.incrementAndGet();
scanMetrics.addToCounter(ScanMetrics.RPC_RETRIES_METRIC_NAME, 1);
if (isRegionServerRemote) {
scanMetrics.countOfRemoteRPCRetries.incrementAndGet();
scanMetrics.addToCounter(ScanMetrics.REMOTE_RPC_RETRIES_METRIC_NAME, 1);
}
}

Expand All @@ -368,9 +368,9 @@ static void updateResultsMetrics(ScanMetrics scanMetrics, Result[] rrs,
resultSize += PrivateCellUtil.estimatedSerializedSizeOf(cell);
}
}
scanMetrics.countOfBytesInResults.addAndGet(resultSize);
scanMetrics.addToCounter(ScanMetrics.BYTES_IN_RESULTS_METRIC_NAME, resultSize);
if (isRegionServerRemote) {
scanMetrics.countOfBytesInRemoteResults.addAndGet(resultSize);
scanMetrics.addToCounter(ScanMetrics.BYTES_IN_REMOTE_RESULTS_METRIC_NAME, resultSize);
}
}

Expand All @@ -390,7 +390,7 @@ static void incRegionCountMetrics(ScanMetrics scanMetrics) {
if (scanMetrics == null) {
return;
}
scanMetrics.countOfRegions.incrementAndGet();
scanMetrics.addToCounter(ScanMetrics.REGIONS_SCANNED_METRIC_NAME, 1);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ public Scan setScanMetricsEnabled(final boolean enabled) {
"ImmutableScan does not allow access to setScanMetricsEnabled");
}

@Override
public Scan setEnableScanMetricsByRegion(final boolean enable) {
throw new UnsupportedOperationException(
"ImmutableScan does not allow access to setEnableScanMetricsByRegion");
}

@Override
@Deprecated
public Scan setAsyncPrefetch(boolean asyncPrefetch) {
Expand Down Expand Up @@ -402,6 +408,11 @@ public boolean isScanMetricsEnabled() {
return this.delegateScan.isScanMetricsEnabled();
}

@Override
public boolean isScanMetricsByRegionEnabled() {
return this.delegateScan.isScanMetricsByRegionEnabled();
}

@Override
public Boolean isAsyncPrefetch() {
return this.delegateScan.isAsyncPrefetch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public class Scan extends Query {
// define this attribute with the appropriate table name by calling
// scan.setAttribute(Scan.SCAN_ATTRIBUTES_TABLE_NAME, Bytes.toBytes(tableName))
static public final String SCAN_ATTRIBUTES_TABLE_NAME = "scan.attributes.table.name";
static private final String SCAN_ATTRIBUTES_METRICS_BY_REGION_ENABLE =
"scan.attributes.metrics.byregion.enable";

/**
* -1 means no caching specified and the value of {@link HConstants#HBASE_CLIENT_SCANNER_CACHING}
Expand Down Expand Up @@ -905,11 +907,15 @@ public Scan setPriority(int priority) {
}

/**
* Enable collection of {@link ScanMetrics}. For advanced users.
* Enable collection of {@link ScanMetrics}. For advanced users. While disabling scan metrics,
* will also disable region level scan metrics.
* @param enabled Set to true to enable accumulating scan metrics
*/
public Scan setScanMetricsEnabled(final boolean enabled) {
setAttribute(Scan.SCAN_ATTRIBUTES_METRICS_ENABLE, Bytes.toBytes(Boolean.valueOf(enabled)));
if (!enabled) {
setEnableScanMetricsByRegion(false);
}
return this;
}

Expand Down Expand Up @@ -1033,4 +1039,22 @@ public boolean isNeedCursorResult() {
public static Scan createScanFromCursor(Cursor cursor) {
return new Scan().withStartRow(cursor.getRow());
}

/**
* Enables region level scan metrics. If scan metrics are disabled then first enables scan metrics
* followed by region level scan metrics.
* @param enable Set to true to enable region level scan metrics.
*/
public Scan setEnableScanMetricsByRegion(final boolean enable) {
if (enable) {
setScanMetricsEnabled(true);
}
setAttribute(Scan.SCAN_ATTRIBUTES_METRICS_BY_REGION_ENABLE, Bytes.toBytes(enable));
return this;
}

public boolean isScanMetricsByRegionEnabled() {
byte[] attr = getAttribute(Scan.SCAN_ATTRIBUTES_METRICS_BY_REGION_ENABLE);
return attr != null && Bytes.toBoolean(attr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.client.metrics;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.hadoop.hbase.ServerName;
import org.apache.yetus.audience.InterfaceAudience;

/**
* Captures region level scan metrics as a map of metric name ({@link String}) -> Value
* ({@link AtomicLong}). <br/>
* <br/>
* One instance stores scan metrics for a single region only.
*/
@InterfaceAudience.Private
public class RegionScanMetricsData {
private final Map<String, AtomicLong> counters = new HashMap<>();
private ScanMetricsRegionInfo scanMetricsRegionInfo =
ScanMetricsRegionInfo.EMPTY_SCAN_METRICS_REGION_INFO;

AtomicLong createCounter(String counterName) {
return ScanMetricsUtil.createCounter(counters, counterName);
}

void setCounter(String counterName, long value) {
ScanMetricsUtil.setCounter(counters, counterName, value);
}

void addToCounter(String counterName, long delta) {
ScanMetricsUtil.addToCounter(counters, counterName, delta);
}

Map<String, Long> collectMetrics(boolean reset) {
return ScanMetricsUtil.collectMetrics(counters, reset);
}

@Override
public String toString() {
return getClass().getSimpleName() + "[" + scanMetricsRegionInfo + "," + "Counters=" + counters
+ "]";
}

/**
* Populate encoded region name and server name details if not already populated. If details are
* already populated and a re-attempt is done then {@link UnsupportedOperationException} is
* thrown.
*/
void initScanMetricsRegionInfo(String encodedRegionName, ServerName serverName) {
// Check by reference
if (scanMetricsRegionInfo == ScanMetricsRegionInfo.EMPTY_SCAN_METRICS_REGION_INFO) {
scanMetricsRegionInfo = new ScanMetricsRegionInfo(encodedRegionName, serverName);
} else {
throw new UnsupportedOperationException("ScanMetricsRegionInfo has already been initialized");
}
}

ScanMetricsRegionInfo getScanMetricsRegionInfo() {
return scanMetricsRegionInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,22 @@ public class ScanMetrics extends ServerSideScanMetrics {
public final AtomicLong countOfRemoteRPCRetries = createCounter(REMOTE_RPC_RETRIES_METRIC_NAME);

/**
* constructor
* Constructor
*/
public ScanMetrics() {
}

@Override
public void moveToNextRegion() {
super.moveToNextRegion();
currentRegionScanMetricsData.createCounter(RPC_CALLS_METRIC_NAME);
currentRegionScanMetricsData.createCounter(REMOTE_RPC_CALLS_METRIC_NAME);
currentRegionScanMetricsData.createCounter(MILLIS_BETWEEN_NEXTS_METRIC_NAME);
currentRegionScanMetricsData.createCounter(NOT_SERVING_REGION_EXCEPTION_METRIC_NAME);
currentRegionScanMetricsData.createCounter(BYTES_IN_RESULTS_METRIC_NAME);
currentRegionScanMetricsData.createCounter(BYTES_IN_REMOTE_RESULTS_METRIC_NAME);
currentRegionScanMetricsData.createCounter(REGIONS_SCANNED_METRIC_NAME);
currentRegionScanMetricsData.createCounter(RPC_RETRIES_METRIC_NAME);
currentRegionScanMetricsData.createCounter(REMOTE_RPC_RETRIES_METRIC_NAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.client.metrics;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.hadoop.hbase.ServerName;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.yetus.audience.InterfaceStability;

/**
* POJO for capturing region level details when region level scan metrics are enabled. <br>
* <br>
* Currently, encoded region name and server name (host name, ports and startcode) are captured as
* region details. <br>
* <br>
* Instance of this class serves as key in the Map returned by
* {@link ServerSideScanMetrics#collectMetricsByRegion()} or
* {@link ServerSideScanMetrics#collectMetricsByRegion(boolean)}.
*/
@InterfaceAudience.Public
@InterfaceStability.Evolving
public class ScanMetricsRegionInfo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we are not expecting much changes, until we have gained more confidence after prod use, let's also add @InterfaceStability.Evolving

/**
* Users should only compare against this constant by reference and should not make any
* assumptions regarding content of the constant.
*/
public static final ScanMetricsRegionInfo EMPTY_SCAN_METRICS_REGION_INFO =
new ScanMetricsRegionInfo(null, null);

private final String encodedRegionName;
private final ServerName serverName;

ScanMetricsRegionInfo(String encodedRegionName, ServerName serverName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why non-public constructor in a public class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional as I don't want users to instantiate an instance of this class by themselves from outside HBase code. The public facing part of this class is the getters but not constructor.

this.encodedRegionName = encodedRegionName;
this.serverName = serverName;
}

@Override
public boolean equals(Object obj) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commons lang has Equals and HashCode Builders that you can use out of the box.

if (!(obj instanceof ScanMetricsRegionInfo other)) {
return false;
}
return new EqualsBuilder().append(encodedRegionName, other.encodedRegionName)
.append(serverName, other.serverName).isEquals();
}

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(encodedRegionName).append(serverName).toHashCode();
}

@Override
public String toString() {
return getClass().getSimpleName() + "[encodedRegionName=" + encodedRegionName + ",serverName="
+ serverName + "]";
}

public String getEncodedRegionName() {
return encodedRegionName;
}

public ServerName getServerName() {
return serverName;
}
}
Loading