Skip to content

Commit 9361ae5

Browse files
authored
HBASE-28385 Improve scan quota estimates when using block bytes scanned (apache#5713)
Signed-off-by: Bryan Beaudreault <bbeaudreault@apache.org>
1 parent 2984474 commit 9361ae5

File tree

12 files changed

+427
-55
lines changed

12 files changed

+427
-55
lines changed

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/DefaultOperationQuota.java

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@
2727
import org.apache.yetus.audience.InterfaceAudience;
2828
import org.apache.yetus.audience.InterfaceStability;
2929

30+
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
31+
3032
@InterfaceAudience.Private
3133
@InterfaceStability.Evolving
3234
public class DefaultOperationQuota implements OperationQuota {
3335

36+
// a single scan estimate can consume no more than this proportion of the limiter's limit
37+
// this prevents a long-running scan from being estimated at, say, 100MB of IO against
38+
// a <100MB/IO throttle (because this would never succeed)
39+
private static final double MAX_SCAN_ESTIMATE_PROPORTIONAL_LIMIT_CONSUMPTION = 0.9;
40+
3441
protected final List<QuotaLimiter> limiters;
3542
private final long writeCapacityUnit;
3643
private final long readCapacityUnit;
@@ -53,13 +60,17 @@ public class DefaultOperationQuota implements OperationQuota {
5360
protected long readCapacityUnitDiff = 0;
5461
private boolean useResultSizeBytes;
5562
private long blockSizeBytes;
63+
private long maxScanEstimate;
5664

5765
public DefaultOperationQuota(final Configuration conf, final int blockSizeBytes,
5866
final QuotaLimiter... limiters) {
5967
this(conf, Arrays.asList(limiters));
6068
this.useResultSizeBytes =
6169
conf.getBoolean(OperationQuota.USE_RESULT_SIZE_BYTES, USE_RESULT_SIZE_BYTES_DEFAULT);
6270
this.blockSizeBytes = blockSizeBytes;
71+
long readSizeLimit =
72+
Arrays.stream(limiters).mapToLong(QuotaLimiter::getReadLimit).min().orElse(Long.MAX_VALUE);
73+
maxScanEstimate = Math.round(MAX_SCAN_ESTIMATE_PROPORTIONAL_LIMIT_CONSUMPTION * readSizeLimit);
6374
}
6475

6576
/**
@@ -80,21 +91,34 @@ public DefaultOperationQuota(final Configuration conf, final List<QuotaLimiter>
8091
}
8192

8293
@Override
83-
public void checkQuota(int numWrites, int numReads, int numScans) throws RpcThrottlingException {
84-
updateEstimateConsumeQuota(numWrites, numReads, numScans);
94+
public void checkBatchQuota(int numWrites, int numReads) throws RpcThrottlingException {
95+
updateEstimateConsumeBatchQuota(numWrites, numReads);
96+
checkQuota(numWrites, numReads);
97+
}
98+
99+
@Override
100+
public void checkScanQuota(ClientProtos.ScanRequest scanRequest, long maxScannerResultSize,
101+
long maxBlockBytesScanned, long prevBlockBytesScannedDifference) throws RpcThrottlingException {
102+
updateEstimateConsumeScanQuota(scanRequest, maxScannerResultSize, maxBlockBytesScanned,
103+
prevBlockBytesScannedDifference);
104+
checkQuota(0, 1);
105+
}
85106

107+
private void checkQuota(long numWrites, long numReads) throws RpcThrottlingException {
86108
readAvailable = Long.MAX_VALUE;
87109
for (final QuotaLimiter limiter : limiters) {
88-
if (limiter.isBypass()) continue;
110+
if (limiter.isBypass()) {
111+
continue;
112+
}
89113

90-
limiter.checkQuota(numWrites, writeConsumed, numReads + numScans, readConsumed,
114+
limiter.checkQuota(numWrites, writeConsumed, numReads, readConsumed,
91115
writeCapacityUnitConsumed, readCapacityUnitConsumed);
92116
readAvailable = Math.min(readAvailable, limiter.getReadAvailable());
93117
}
94118

95119
for (final QuotaLimiter limiter : limiters) {
96-
limiter.grabQuota(numWrites, writeConsumed, numReads + numScans, readConsumed,
97-
writeCapacityUnitConsumed, readCapacityUnitConsumed);
120+
limiter.grabQuota(numWrites, writeConsumed, numReads, readConsumed, writeCapacityUnitConsumed,
121+
readCapacityUnitConsumed);
98122
}
99123
}
100124

@@ -158,24 +182,69 @@ public void addMutation(final Mutation mutation) {
158182
* Update estimate quota(read/write size/capacityUnits) which will be consumed
159183
* @param numWrites the number of write requests
160184
* @param numReads the number of read requests
161-
* @param numScans the number of scan requests
162185
*/
163-
protected void updateEstimateConsumeQuota(int numWrites, int numReads, int numScans) {
186+
protected void updateEstimateConsumeBatchQuota(int numWrites, int numReads) {
164187
writeConsumed = estimateConsume(OperationType.MUTATE, numWrites, 100);
165188

166189
if (useResultSizeBytes) {
167190
readConsumed = estimateConsume(OperationType.GET, numReads, 100);
168-
readConsumed += estimateConsume(OperationType.SCAN, numScans, 1000);
169191
} else {
170192
// assume 1 block required for reads. this is probably a low estimate, which is okay
171193
readConsumed = numReads > 0 ? blockSizeBytes : 0;
172-
readConsumed += numScans > 0 ? blockSizeBytes : 0;
173194
}
174195

175196
writeCapacityUnitConsumed = calculateWriteCapacityUnit(writeConsumed);
176197
readCapacityUnitConsumed = calculateReadCapacityUnit(readConsumed);
177198
}
178199

200+
/**
201+
* Update estimate quota(read/write size/capacityUnits) which will be consumed
202+
* @param scanRequest the scan to be executed
203+
* @param maxScannerResultSize the maximum bytes to be returned by the scanner
204+
* @param maxBlockBytesScanned the maximum bytes scanned in a single RPC call by the
205+
* scanner
206+
* @param prevBlockBytesScannedDifference the difference between BBS of the previous two next
207+
* calls
208+
*/
209+
protected void updateEstimateConsumeScanQuota(ClientProtos.ScanRequest scanRequest,
210+
long maxScannerResultSize, long maxBlockBytesScanned, long prevBlockBytesScannedDifference) {
211+
if (useResultSizeBytes) {
212+
readConsumed = estimateConsume(OperationType.SCAN, 1, 1000);
213+
} else {
214+
long estimate = getScanReadConsumeEstimate(blockSizeBytes, scanRequest.getNextCallSeq(),
215+
maxScannerResultSize, maxBlockBytesScanned, prevBlockBytesScannedDifference);
216+
readConsumed = Math.min(maxScanEstimate, estimate);
217+
}
218+
219+
readCapacityUnitConsumed = calculateReadCapacityUnit(readConsumed);
220+
}
221+
222+
protected static long getScanReadConsumeEstimate(long blockSizeBytes, long nextCallSeq,
223+
long maxScannerResultSize, long maxBlockBytesScanned, long prevBlockBytesScannedDifference) {
224+
/*
225+
* Estimating scan workload is more complicated, and if we severely underestimate workloads then
226+
* throttled clients will exhaust retries too quickly, and could saturate the RPC layer
227+
*/
228+
if (nextCallSeq == 0) {
229+
// start scanners with an optimistic 1 block IO estimate
230+
// it is better to underestimate a large scan in the beginning
231+
// than to overestimate, and block, a small scan
232+
return blockSizeBytes;
233+
}
234+
235+
boolean isWorkloadGrowing = prevBlockBytesScannedDifference > blockSizeBytes;
236+
if (isWorkloadGrowing) {
237+
// if nextCallSeq > 0 and the workload is growing then our estimate
238+
// should consider that the workload may continue to increase
239+
return Math.min(maxScannerResultSize, nextCallSeq * maxBlockBytesScanned);
240+
} else {
241+
// if nextCallSeq > 0 and the workload is shrinking or flat
242+
// then our workload has likely plateaued. We can just rely on the existing
243+
// maxBlockBytesScanned as our estimate in this case.
244+
return maxBlockBytesScanned;
245+
}
246+
}
247+
179248
private long estimateConsume(final OperationType type, int numReqs, long avgSize) {
180249
if (numReqs > 0) {
181250
return avgSize * numReqs;

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/ExceedOperationQuota.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.slf4j.Logger;
2424
import org.slf4j.LoggerFactory;
2525

26+
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
27+
2628
/*
2729
* Internal class used to check and consume quota if exceed throttle quota is enabled. Exceed
2830
* throttle quota means, user can over consume user/namespace/table quota if region server has
@@ -47,27 +49,44 @@ public ExceedOperationQuota(final Configuration conf, int blockSizeBytes,
4749
}
4850

4951
@Override
50-
public void checkQuota(int numWrites, int numReads, int numScans) throws RpcThrottlingException {
52+
public void checkBatchQuota(int numWrites, int numReads) throws RpcThrottlingException {
53+
Runnable estimateQuota = () -> updateEstimateConsumeBatchQuota(numWrites, numReads);
54+
CheckQuotaRunnable checkQuota = () -> super.checkBatchQuota(numWrites, numReads);
55+
checkQuota(estimateQuota, checkQuota, numWrites, numReads, 0);
56+
}
57+
58+
@Override
59+
public void checkScanQuota(ClientProtos.ScanRequest scanRequest, long maxScannerResultSize,
60+
long maxBlockBytesScanned, long prevBlockBytesScannedDifference) throws RpcThrottlingException {
61+
Runnable estimateQuota = () -> updateEstimateConsumeScanQuota(scanRequest, maxScannerResultSize,
62+
maxBlockBytesScanned, prevBlockBytesScannedDifference);
63+
CheckQuotaRunnable checkQuota = () -> super.checkScanQuota(scanRequest, maxScannerResultSize,
64+
maxBlockBytesScanned, prevBlockBytesScannedDifference);
65+
checkQuota(estimateQuota, checkQuota, 0, 0, 1);
66+
}
67+
68+
private void checkQuota(Runnable estimateQuota, CheckQuotaRunnable checkQuota, int numWrites,
69+
int numReads, int numScans) throws RpcThrottlingException {
5170
if (regionServerLimiter.isBypass()) {
5271
// If region server limiter is bypass, which means no region server quota is set, check and
5372
// throttle by all other quotas. In this condition, exceed throttle quota will not work.
5473
LOG.warn("Exceed throttle quota is enabled but no region server quotas found");
55-
super.checkQuota(numWrites, numReads, numScans);
74+
checkQuota.run();
5675
} else {
5776
// 1. Update estimate quota which will be consumed
58-
updateEstimateConsumeQuota(numWrites, numReads, numScans);
77+
estimateQuota.run();
5978
// 2. Check if region server limiter is enough. If not, throw RpcThrottlingException.
6079
regionServerLimiter.checkQuota(numWrites, writeConsumed, numReads + numScans, readConsumed,
6180
writeCapacityUnitConsumed, readCapacityUnitConsumed);
6281
// 3. Check if other limiters are enough. If not, exceed other limiters because region server
6382
// limiter is enough.
6483
boolean exceed = false;
6584
try {
66-
super.checkQuota(numWrites, numReads, numScans);
85+
checkQuota.run();
6786
} catch (RpcThrottlingException e) {
6887
exceed = true;
6988
if (LOG.isDebugEnabled()) {
70-
LOG.debug("Read/Write requests num exceeds quota: writes:{} reads:{} scan:{}, "
89+
LOG.debug("Read/Write requests num exceeds quota: writes:{} reads:{}, scans:{}, "
7190
+ "try use region server quota", numWrites, numReads, numScans);
7291
}
7392
}
@@ -96,4 +115,8 @@ public void close() {
96115
regionServerLimiter.consumeRead(readDiff, readCapacityUnitDiff);
97116
}
98117
}
118+
119+
private interface CheckQuotaRunnable {
120+
void run() throws RpcThrottlingException;
121+
}
99122
}

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/NoopOperationQuota.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.yetus.audience.InterfaceAudience;
2424
import org.apache.yetus.audience.InterfaceStability;
2525

26+
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
27+
2628
/**
2729
* Noop operation quota returned when no quota is associated to the user/table
2830
*/
@@ -40,7 +42,13 @@ public static OperationQuota get() {
4042
}
4143

4244
@Override
43-
public void checkQuota(int numWrites, int numReads, int numScans) throws RpcThrottlingException {
45+
public void checkBatchQuota(int numWrites, int numReads) throws RpcThrottlingException {
46+
// no-op
47+
}
48+
49+
@Override
50+
public void checkScanQuota(ClientProtos.ScanRequest scanRequest, long maxScannerResultSize,
51+
long maxBlockBytesScanned, long prevBlockBytesScannedDifference) throws RpcThrottlingException {
4452
// no-op
4553
}
4654

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/NoopQuotaLimiter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public long getReadAvailable() {
7070
throw new UnsupportedOperationException();
7171
}
7272

73+
@Override
74+
public long getReadLimit() {
75+
return Long.MAX_VALUE;
76+
}
77+
7378
@Override
7479
public String toString() {
7580
return "NoopQuotaLimiter";

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/OperationQuota.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.yetus.audience.InterfaceAudience;
2424
import org.apache.yetus.audience.InterfaceStability;
2525

26+
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
27+
2628
/**
2729
* Interface that allows to check the quota available for an operation.
2830
*/
@@ -51,11 +53,25 @@ public enum OperationType {
5153
* on the number of operations to perform and the average size accumulated during time.
5254
* @param numWrites number of write operation that will be performed
5355
* @param numReads number of small-read operation that will be performed
54-
* @param numScans number of long-read operation that will be performed
5556
* @throws RpcThrottlingException if the operation cannot be performed because RPC quota is
5657
* exceeded.
5758
*/
58-
void checkQuota(int numWrites, int numReads, int numScans) throws RpcThrottlingException;
59+
void checkBatchQuota(int numWrites, int numReads) throws RpcThrottlingException;
60+
61+
/**
62+
* Checks if it is possible to execute the scan. The quota will be estimated based on the
63+
* composition of the scan.
64+
* @param scanRequest the given scan operation
65+
* @param maxScannerResultSize the maximum bytes to be returned by the scanner
66+
* @param maxBlockBytesScanned the maximum bytes scanned in a single RPC call by the
67+
* scanner
68+
* @param prevBlockBytesScannedDifference the difference between BBS of the previous two next
69+
* calls
70+
* @throws RpcThrottlingException if the operation cannot be performed because RPC quota is
71+
* exceeded.
72+
*/
73+
void checkScanQuota(ClientProtos.ScanRequest scanRequest, long maxScannerResultSize,
74+
long maxBlockBytesScanned, long prevBlockBytesScannedDifference) throws RpcThrottlingException;
5975

6076
/** Cleanup method on operation completion */
6177
void close();

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaLimiter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ void grabQuota(long writeReqs, long writeSize, long readReqs, long readSize,
7676
/** Returns the number of bytes available to read to avoid exceeding the quota */
7777
long getReadAvailable();
7878

79+
/** Returns the maximum number of bytes ever available to read */
80+
long getReadLimit();
81+
7982
/** Returns the number of bytes available to write to avoid exceeding the quota */
8083
long getWriteAvailable();
8184
}

0 commit comments

Comments
 (0)