You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by bb...@apache.org on 2024/02/07 15:01:11 UTC
(hbase) branch branch-2.6 updated: HBASE-27800: Add support for default user quotas (#5666)
This is an automated email from the ASF dual-hosted git repository.
bbeaudreault pushed a commit to branch branch-2.6
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-2.6 by this push:
new 240dfa0a5eb HBASE-27800: Add support for default user quotas (#5666)
240dfa0a5eb is described below
commit 240dfa0a5eb621ca4c65acde47a76df59b6c9d65
Author: Ray Mattingly <rm...@gmail.com>
AuthorDate: Wed Feb 7 09:23:17 2024 -0500
HBASE-27800: Add support for default user quotas (#5666)
Signed-off-by: Bryan Beaudreault <bb...@apache.org>
---
.../org/apache/hadoop/hbase/quotas/QuotaCache.java | 3 +-
.../org/apache/hadoop/hbase/quotas/QuotaUtil.java | 61 ++++++++-
.../hadoop/hbase/quotas/TestDefaultQuota.java | 138 +++++++++++++++++++++
3 files changed, 200 insertions(+), 2 deletions(-)
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaCache.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaCache.java
index 0a57b9fd8f8..67b2aecc544 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaCache.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaCache.java
@@ -137,7 +137,8 @@ public class QuotaCache implements Stoppable {
* @return the quota info associated to specified user
*/
public UserQuotaState getUserQuotaState(final UserGroupInformation ugi) {
- return computeIfAbsent(userQuotaCache, getQuotaUserName(ugi), UserQuotaState::new,
+ return computeIfAbsent(userQuotaCache, getQuotaUserName(ugi),
+ () -> QuotaUtil.buildDefaultUserQuotaState(rsServices.getConfiguration()),
this::triggerCacheRefresh);
}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaUtil.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaUtil.java
index b307c5d7dab..8e7bce8b1e7 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaUtil.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaUtil.java
@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.DoNotRetryIOException;
@@ -48,7 +49,9 @@ import org.apache.yetus.audience.InterfaceStability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.TimeUnit;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.QuotaScope;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Throttle;
@@ -72,6 +75,26 @@ public class QuotaUtil extends QuotaTableUtil {
// the default one write capacity unit is 1024 bytes (1KB)
public static final long DEFAULT_WRITE_CAPACITY_UNIT = 1024;
+ /*
+ * The below defaults, if configured, will be applied to otherwise unthrottled users. For example,
+ * set `hbase.quota.default.user.machine.read.size` to `1048576` in your hbase-site.xml to ensure
+ * that any given user may not query more than 1mb per second from any given machine, unless
+ * explicitly permitted by a persisted quota. All of these defaults use TimeUnit.SECONDS and
+ * QuotaScope.MACHINE.
+ */
+ public static final String QUOTA_DEFAULT_USER_MACHINE_READ_NUM =
+ "hbase.quota.default.user.machine.read.num";
+ public static final String QUOTA_DEFAULT_USER_MACHINE_READ_SIZE =
+ "hbase.quota.default.user.machine.read.size";
+ public static final String QUOTA_DEFAULT_USER_MACHINE_REQUEST_NUM =
+ "hbase.quota.default.user.machine.request.num";
+ public static final String QUOTA_DEFAULT_USER_MACHINE_REQUEST_SIZE =
+ "hbase.quota.default.user.machine.request.size";
+ public static final String QUOTA_DEFAULT_USER_MACHINE_WRITE_NUM =
+ "hbase.quota.default.user.machine.write.num";
+ public static final String QUOTA_DEFAULT_USER_MACHINE_WRITE_SIZE =
+ "hbase.quota.default.user.machine.write.size";
+
/** Table descriptor for Quota internal table */
public static final HTableDescriptor QUOTA_TABLE_DESC = new HTableDescriptor(QUOTA_TABLE_NAME);
static {
@@ -283,10 +306,14 @@ public class QuotaUtil extends QuotaTableUtil {
assert isUserRowKey(key);
String user = getUserFromRowKey(key);
+ if (results[i].isEmpty()) {
+ userQuotas.put(user, buildDefaultUserQuotaState(connection.getConfiguration()));
+ continue;
+ }
+
final UserQuotaState quotaInfo = new UserQuotaState(nowTs);
userQuotas.put(user, quotaInfo);
- if (results[i].isEmpty()) continue;
assert Bytes.equals(key, results[i].getRow());
try {
@@ -320,6 +347,38 @@ public class QuotaUtil extends QuotaTableUtil {
return userQuotas;
}
+ protected static UserQuotaState buildDefaultUserQuotaState(Configuration conf) {
+ QuotaProtos.Throttle.Builder throttleBuilder = QuotaProtos.Throttle.newBuilder();
+
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_NUM)
+ .ifPresent(throttleBuilder::setReadNum);
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_READ_SIZE)
+ .ifPresent(throttleBuilder::setReadSize);
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_NUM)
+ .ifPresent(throttleBuilder::setReqNum);
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_REQUEST_SIZE)
+ .ifPresent(throttleBuilder::setReqSize);
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_NUM)
+ .ifPresent(throttleBuilder::setWriteNum);
+ buildDefaultTimedQuota(conf, QUOTA_DEFAULT_USER_MACHINE_WRITE_SIZE)
+ .ifPresent(throttleBuilder::setWriteSize);
+
+ UserQuotaState state = new UserQuotaState();
+ QuotaProtos.Quotas defaultQuotas =
+ QuotaProtos.Quotas.newBuilder().setThrottle(throttleBuilder.build()).build();
+ state.setQuotas(defaultQuotas);
+ return state;
+ }
+
+ private static Optional<TimedQuota> buildDefaultTimedQuota(Configuration conf, String key) {
+ int defaultSoftLimit = conf.getInt(key, -1);
+ if (defaultSoftLimit == -1) {
+ return Optional.empty();
+ }
+ return Optional.of(ProtobufUtil.toTimedQuota(defaultSoftLimit,
+ java.util.concurrent.TimeUnit.SECONDS, org.apache.hadoop.hbase.quotas.QuotaScope.MACHINE));
+ }
+
public static Map<TableName, QuotaState> fetchTableQuotas(final Connection connection,
final List<Get> gets, Map<TableName, Double> tableMachineFactors) throws IOException {
return fetchGlobalQuotas("table", connection, gets, new KeyFromRow<TableName>() {
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/quotas/TestDefaultQuota.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/quotas/TestDefaultQuota.java
new file mode 100644
index 00000000000..c2cc94caf8b
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/quotas/TestDefaultQuota.java
@@ -0,0 +1,138 @@
+/*
+ * 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.quotas;
+
+import static org.apache.hadoop.hbase.quotas.ThrottleQuotaTestUtil.triggerUserCacheRefresh;
+import static org.apache.hadoop.hbase.quotas.ThrottleQuotaTestUtil.waitMinuteQuota;
+
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.testclassification.RegionServerTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ RegionServerTests.class, MediumTests.class })
+public class TestDefaultQuota {
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestDefaultQuota.class);
+ private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
+ private static final TableName TABLE_NAME = TableName.valueOf(UUID.randomUUID().toString());
+ private static final int REFRESH_TIME = 5000;
+ private static final byte[] FAMILY = Bytes.toBytes("cf");
+ private static final byte[] QUALIFIER = Bytes.toBytes("q");
+
+ @After
+ public void tearDown() throws Exception {
+ ThrottleQuotaTestUtil.clearQuotaCache(TEST_UTIL);
+ EnvironmentEdgeManager.reset();
+ TEST_UTIL.deleteTable(TABLE_NAME);
+ TEST_UTIL.shutdownMiniCluster();
+ }
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ // quotas enabled, using block bytes scanned
+ TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true);
+ TEST_UTIL.getConfiguration().setInt(QuotaCache.REFRESH_CONF_KEY, REFRESH_TIME);
+ TEST_UTIL.getConfiguration().setInt(QuotaUtil.QUOTA_DEFAULT_USER_MACHINE_READ_NUM, 1);
+
+ // don't cache blocks to make IO predictable
+ TEST_UTIL.getConfiguration().setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.0f);
+
+ TEST_UTIL.startMiniCluster(1);
+ TEST_UTIL.waitTableAvailable(QuotaTableUtil.QUOTA_TABLE_NAME);
+ TEST_UTIL.createTable(TABLE_NAME, FAMILY);
+ TEST_UTIL.waitTableAvailable(TABLE_NAME);
+ QuotaCache.TEST_FORCE_REFRESH = true;
+
+ try (Admin admin = TEST_UTIL.getAdmin()) {
+ ThrottleQuotaTestUtil.doPuts(1_000, FAMILY, QUALIFIER,
+ admin.getConnection().getTable(TABLE_NAME));
+ }
+ TEST_UTIL.flush(TABLE_NAME);
+ }
+
+ @Test
+ public void testDefaultUserReadNum() throws Exception {
+ // Should have a strict throttle by default
+ TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) < 100);
+
+ // Add big quota and should be effectively unlimited
+ configureLenientThrottle();
+ refreshQuotas();
+ // Should run without error
+ TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) == 100);
+
+ // Remove all the limits, and should revert to strict default
+ unsetQuota();
+ TEST_UTIL.waitFor(60_000, () -> runGetsTest(100) < 100);
+ }
+
+ private void configureLenientThrottle() throws IOException {
+ try (Admin admin = TEST_UTIL.getAdmin()) {
+ admin.setQuota(QuotaSettingsFactory.throttleUser(getUserName(), ThrottleType.READ_NUMBER,
+ 100_000, TimeUnit.SECONDS));
+ }
+ }
+
+ private static String getUserName() throws IOException {
+ return User.getCurrent().getShortName();
+ }
+
+ private void refreshQuotas() throws Exception {
+ triggerUserCacheRefresh(TEST_UTIL, false, TABLE_NAME);
+ waitMinuteQuota();
+ }
+
+ private void unsetQuota() throws Exception {
+ try (Admin admin = TEST_UTIL.getAdmin()) {
+ admin.setQuota(QuotaSettingsFactory.unthrottleUser(getUserName()));
+ }
+ refreshQuotas();
+ }
+
+ private long runGetsTest(int attempts) throws Exception {
+ refreshQuotas();
+ try (Table table = getTable()) {
+ return ThrottleQuotaTestUtil.doGets(attempts, FAMILY, QUALIFIER, table);
+ }
+ }
+
+ private Table getTable() throws IOException {
+ TEST_UTIL.getConfiguration().setInt("hbase.client.pause", 100);
+ TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 1);
+ return TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null).setOperationTimeout(250)
+ .build();
+ }
+
+}