Skip to content

Commit 7f531f3

Browse files
rmdmattinglybbeaudreault
authored andcommitted
HBASE-27800: Add support for default user quotas (#5666)
Signed-off-by: Bryan Beaudreault <[email protected]>
1 parent 3ea4fef commit 7f531f3

File tree

3 files changed

+200
-2
lines changed

3 files changed

+200
-2
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ public QuotaLimiter getUserLimiter(final UserGroupInformation ugi, final TableNa
137137
* @return the quota info associated to specified user
138138
*/
139139
public UserQuotaState getUserQuotaState(final UserGroupInformation ugi) {
140-
return computeIfAbsent(userQuotaCache, getQuotaUserName(ugi), UserQuotaState::new,
140+
return computeIfAbsent(userQuotaCache, getQuotaUserName(ugi),
141+
() -> QuotaUtil.buildDefaultUserQuotaState(rsServices.getConfiguration()),
141142
this::triggerCacheRefresh);
142143
}
143144

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.HashMap;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Optional;
2526
import org.apache.hadoop.conf.Configuration;
2627
import org.apache.hadoop.hbase.Cell;
2728
import org.apache.hadoop.hbase.DoNotRetryIOException;
@@ -49,7 +50,9 @@
4950
import org.slf4j.Logger;
5051
import org.slf4j.LoggerFactory;
5152

53+
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
5254
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.TimeUnit;
55+
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
5356
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.QuotaScope;
5457
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
5558
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Throttle;
@@ -73,6 +76,26 @@ public class QuotaUtil extends QuotaTableUtil {
7376
// the default one write capacity unit is 1024 bytes (1KB)
7477
public static final long DEFAULT_WRITE_CAPACITY_UNIT = 1024;
7578

79+
/*
80+
* The below defaults, if configured, will be applied to otherwise unthrottled users. For example,
81+
* set `hbase.quota.default.user.machine.read.size` to `1048576` in your hbase-site.xml to ensure
82+
* that any given user may not query more than 1mb per second from any given machine, unless
83+
* explicitly permitted by a persisted quota. All of these defaults use TimeUnit.SECONDS and
84+
* QuotaScope.MACHINE.
85+
*/
86+
public static final String QUOTA_DEFAULT_USER_MACHINE_READ_NUM =
87+
"hbase.quota.default.user.machine.read.num";
88+
public static final String QUOTA_DEFAULT_USER_MACHINE_READ_SIZE =
89+
"hbase.quota.default.user.machine.read.size";
90+
public static final String QUOTA_DEFAULT_USER_MACHINE_REQUEST_NUM =
91+
"hbase.quota.default.user.machine.request.num";
92+
public static final String QUOTA_DEFAULT_USER_MACHINE_REQUEST_SIZE =
93+
"hbase.quota.default.user.machine.request.size";
94+
public static final String QUOTA_DEFAULT_USER_MACHINE_WRITE_NUM =
95+
"hbase.quota.default.user.machine.write.num";
96+
public static final String QUOTA_DEFAULT_USER_MACHINE_WRITE_SIZE =
97+
"hbase.quota.default.user.machine.write.size";
98+
7699
/** Table descriptor for Quota internal table */
77100
public static final TableDescriptor QUOTA_TABLE_DESC =
78101
TableDescriptorBuilder.newBuilder(QUOTA_TABLE_NAME)
@@ -284,10 +307,14 @@ public static Map<String, UserQuotaState> fetchUserQuotas(final Connection conne
284307
assert isUserRowKey(key);
285308
String user = getUserFromRowKey(key);
286309

310+
if (results[i].isEmpty()) {
311+
userQuotas.put(user, buildDefaultUserQuotaState(connection.getConfiguration()));
312+
continue;
313+
}
314+
287315
final UserQuotaState quotaInfo = new UserQuotaState(nowTs);
288316
userQuotas.put(user, quotaInfo);
289317

290-
if (results[i].isEmpty()) continue;
291318
assert Bytes.equals(key, results[i].getRow());
292319

293320
try {
@@ -321,6 +348,38 @@ public void visitUserQuotas(String userName, Quotas quotas) {
321348
return userQuotas;
322349
}
323350

351+
protected static UserQuotaState buildDefaultUserQuotaState(Configuration conf) {
352+
QuotaProtos.Throttle.Builder throttleBuilder = QuotaProtos.Throttle.newBuilder();
353+
354+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_NUM)
355+
.ifPresent(throttleBuilder::setReadNum);
356+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_SIZE)
357+
.ifPresent(throttleBuilder::setReadSize);
358+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_NUM)
359+
.ifPresent(throttleBuilder::setReqNum);
360+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_SIZE)
361+
.ifPresent(throttleBuilder::setReqSize);
362+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_NUM)
363+
.ifPresent(throttleBuilder::setWriteNum);
364+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_SIZE)
365+
.ifPresent(throttleBuilder::setWriteSize);
366+
367+
UserQuotaState state = new UserQuotaState();
368+
QuotaProtos.Quotas defaultQuotas =
369+
QuotaProtos.Quotas.newBuilder().setThrottle(throttleBuilder.build()).build();
370+
state.setQuotas(defaultQuotas);
371+
return state;
372+
}
373+
374+
private static Optional<TimedQuota> buildDefaultTimedQuota(Configuration conf, String key) {
375+
int defaultSoftLimit = conf.getInt(key, -1);
376+
if (defaultSoftLimit == -1) {
377+
return Optional.empty();
378+
}
379+
return Optional.of(ProtobufUtil.toTimedQuota(defaultSoftLimit,
380+
java.util.concurrent.TimeUnit.SECONDS, org.apache.hadoop.hbase.quotas.QuotaScope.MACHINE));
381+
}
382+
324383
public static Map<TableName, QuotaState> fetchTableQuotas(final Connection connection,
325384
final List<Get> gets, Map<TableName, Double> tableMachineFactors) throws IOException {
326385
return fetchGlobalQuotas("table", connection, gets, new KeyFromRow<TableName>() {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.quotas;
19+
20+
import static org.apache.hadoop.hbase.quotas.ThrottleQuotaTestUtil.triggerUserCacheRefresh;
21+
import static org.apache.hadoop.hbase.quotas.ThrottleQuotaTestUtil.waitMinuteQuota;
22+
23+
import java.io.IOException;
24+
import java.util.UUID;
25+
import java.util.concurrent.TimeUnit;
26+
import org.apache.hadoop.hbase.HBaseClassTestRule;
27+
import org.apache.hadoop.hbase.HBaseTestingUtil;
28+
import org.apache.hadoop.hbase.HConstants;
29+
import org.apache.hadoop.hbase.TableName;
30+
import org.apache.hadoop.hbase.client.Admin;
31+
import org.apache.hadoop.hbase.client.Table;
32+
import org.apache.hadoop.hbase.security.User;
33+
import org.apache.hadoop.hbase.testclassification.MediumTests;
34+
import org.apache.hadoop.hbase.testclassification.RegionServerTests;
35+
import org.apache.hadoop.hbase.util.Bytes;
36+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
37+
import org.junit.After;
38+
import org.junit.BeforeClass;
39+
import org.junit.ClassRule;
40+
import org.junit.Test;
41+
import org.junit.experimental.categories.Category;
42+
43+
@Category({ RegionServerTests.class, MediumTests.class })
44+
public class TestDefaultQuota {
45+
@ClassRule
46+
public static final HBaseClassTestRule CLASS_RULE =
47+
HBaseClassTestRule.forClass(TestDefaultQuota.class);
48+
private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
49+
private static final TableName TABLE_NAME = TableName.valueOf(UUID.randomUUID().toString());
50+
private static final int REFRESH_TIME = 5000;
51+
private static final byte[] FAMILY = Bytes.toBytes("cf");
52+
private static final byte[] QUALIFIER = Bytes.toBytes("q");
53+
54+
@After
55+
public void tearDown() throws Exception {
56+
ThrottleQuotaTestUtil.clearQuotaCache(TEST_UTIL);
57+
EnvironmentEdgeManager.reset();
58+
TEST_UTIL.deleteTable(TABLE_NAME);
59+
TEST_UTIL.shutdownMiniCluster();
60+
}
61+
62+
@BeforeClass
63+
public static void setUpBeforeClass() throws Exception {
64+
// quotas enabled, using block bytes scanned
65+
TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true);
66+
TEST_UTIL.getConfiguration().setInt(QuotaCache.REFRESH_CONF_KEY, REFRESH_TIME);
67+
TEST_UTIL.getConfiguration().setInt(QuotaUtil.QUOTA_DEFAULT_USER_MACHINE_READ_NUM, 1);
68+
69+
// don't cache blocks to make IO predictable
70+
TEST_UTIL.getConfiguration().setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.0f);
71+
72+
TEST_UTIL.startMiniCluster(1);
73+
TEST_UTIL.waitTableAvailable(QuotaTableUtil.QUOTA_TABLE_NAME);
74+
TEST_UTIL.createTable(TABLE_NAME, FAMILY);
75+
TEST_UTIL.waitTableAvailable(TABLE_NAME);
76+
QuotaCache.TEST_FORCE_REFRESH = true;
77+
78+
try (Admin admin = TEST_UTIL.getAdmin()) {
79+
ThrottleQuotaTestUtil.doPuts(1_000, FAMILY, QUALIFIER,
80+
admin.getConnection().getTable(TABLE_NAME));
81+
}
82+
TEST_UTIL.flush(TABLE_NAME);
83+
}
84+
85+
@Test
86+
public void testDefaultUserReadNum() throws Exception {
87+
// Should have a strict throttle by default
88+
TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) < 100);
89+
90+
// Add big quota and should be effectively unlimited
91+
configureLenientThrottle();
92+
refreshQuotas();
93+
// Should run without error
94+
TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) == 100);
95+
96+
// Remove all the limits, and should revert to strict default
97+
unsetQuota();
98+
TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) < 100);
99+
}
100+
101+
private void configureLenientThrottle() throws IOException {
102+
try (Admin admin = TEST_UTIL.getAdmin()) {
103+
admin.setQuota(QuotaSettingsFactory.throttleUser(getUserName(), ThrottleType.READ_NUMBER,
104+
100_000, TimeUnit.SECONDS));
105+
}
106+
}
107+
108+
private static String getUserName() throws IOException {
109+
return User.getCurrent().getShortName();
110+
}
111+
112+
private void refreshQuotas() throws Exception {
113+
triggerUserCacheRefresh(TEST_UTIL, false, TABLE_NAME);
114+
waitMinuteQuota();
115+
}
116+
117+
private void unsetQuota() throws Exception {
118+
try (Admin admin = TEST_UTIL.getAdmin()) {
119+
admin.setQuota(QuotaSettingsFactory.unthrottleUser(getUserName()));
120+
}
121+
refreshQuotas();
122+
}
123+
124+
private long runGetsTest(int attempts) throws Exception {
125+
refreshQuotas();
126+
try (Table table = getTable()) {
127+
return ThrottleQuotaTestUtil.doGets(attempts, FAMILY, QUALIFIER, table);
128+
}
129+
}
130+
131+
private Table getTable() throws IOException {
132+
TEST_UTIL.getConfiguration().setInt("hbase.client.pause", 100);
133+
TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 1);
134+
return TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null).setOperationTimeout(250)
135+
.build();
136+
}
137+
138+
}

0 commit comments

Comments
 (0)