Skip to content

Commit 240dfa0

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

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;
@@ -48,7 +49,9 @@
4849
import org.slf4j.Logger;
4950
import org.slf4j.LoggerFactory;
5051

52+
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
5153
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.TimeUnit;
54+
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
5255
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.QuotaScope;
5356
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
5457
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Throttle;
@@ -72,6 +75,26 @@ public class QuotaUtil extends QuotaTableUtil {
7275
// the default one write capacity unit is 1024 bytes (1KB)
7376
public static final long DEFAULT_WRITE_CAPACITY_UNIT = 1024;
7477

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

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

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

292319
try {
@@ -320,6 +347,38 @@ public void visitUserQuotas(String userName, Quotas quotas) {
320347
return userQuotas;
321348
}
322349

350+
protected static UserQuotaState buildDefaultUserQuotaState(Configuration conf) {
351+
QuotaProtos.Throttle.Builder throttleBuilder = QuotaProtos.Throttle.newBuilder();
352+
353+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_NUM)
354+
.ifPresent(throttleBuilder::setReadNum);
355+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_SIZE)
356+
.ifPresent(throttleBuilder::setReadSize);
357+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_NUM)
358+
.ifPresent(throttleBuilder::setReqNum);
359+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_SIZE)
360+
.ifPresent(throttleBuilder::setReqSize);
361+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_NUM)
362+
.ifPresent(throttleBuilder::setWriteNum);
363+
buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_SIZE)
364+
.ifPresent(throttleBuilder::setWriteSize);
365+
366+
UserQuotaState state = new UserQuotaState();
367+
QuotaProtos.Quotas defaultQuotas =
368+
QuotaProtos.Quotas.newBuilder().setThrottle(throttleBuilder.build()).build();
369+
state.setQuotas(defaultQuotas);
370+
return state;
371+
}
372+
373+
private static Optional<TimedQuota> buildDefaultTimedQuota(Configuration conf, String key) {
374+
int defaultSoftLimit = conf.getInt(key, -1);
375+
if (defaultSoftLimit == -1) {
376+
return Optional.empty();
377+
}
378+
return Optional.of(ProtobufUtil.toTimedQuota(defaultSoftLimit,
379+
java.util.concurrent.TimeUnit.SECONDS, org.apache.hadoop.hbase.quotas.QuotaScope.MACHINE));
380+
}
381+
323382
public static Map<TableName, QuotaState> fetchTableQuotas(final Connection connection,
324383
final List<Get> gets, Map<TableName, Double> tableMachineFactors) throws IOException {
325384
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.HBaseTestingUtility;
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 HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
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)