-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HBASE-28963 Updating Quota Factors is too expensive #6451
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
import static org.apache.hadoop.hbase.util.ConcurrentMapUtils.computeIfAbsent; | ||
|
||
import java.io.IOException; | ||
import java.time.Duration; | ||
import java.util.ArrayList; | ||
import java.util.EnumSet; | ||
import java.util.List; | ||
|
@@ -28,6 +29,7 @@ | |
import java.util.Set; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentMap; | ||
import java.util.concurrent.TimeUnit; | ||
import org.apache.hadoop.conf.Configuration; | ||
import org.apache.hadoop.hbase.ClusterMetrics; | ||
import org.apache.hadoop.hbase.ClusterMetrics.Option; | ||
|
@@ -48,6 +50,10 @@ | |
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import org.apache.hbase.thirdparty.com.google.common.cache.CacheBuilder; | ||
import org.apache.hbase.thirdparty.com.google.common.cache.CacheLoader; | ||
import org.apache.hbase.thirdparty.com.google.common.cache.LoadingCache; | ||
|
||
/** | ||
* Cache that keeps track of the quota settings for the users and tables that are interacting with | ||
* it. To avoid blocking the operations if the requested quota is not in cache an "empty quota" will | ||
|
@@ -61,6 +67,10 @@ public class QuotaCache implements Stoppable { | |
private static final Logger LOG = LoggerFactory.getLogger(QuotaCache.class); | ||
|
||
public static final String REFRESH_CONF_KEY = "hbase.quota.refresh.period"; | ||
public static final String TABLE_REGION_STATES_CACHE_TTL_MS = | ||
"hbase.quota.cache.ttl.region.states.ms"; | ||
public static final String REGION_SERVERS_SIZE_CACHE_TTL_MS = | ||
"hbase.quota.cache.ttl.servers.size.ms"; | ||
|
||
// defines the request attribute key which, when provided, will override the request's username | ||
// from the perspective of user quotas | ||
|
@@ -102,7 +112,7 @@ public void start() throws IOException { | |
// TODO: This will be replaced once we have the notification bus ready. | ||
Configuration conf = rsServices.getConfiguration(); | ||
int period = conf.getInt(REFRESH_CONF_KEY, REFRESH_DEFAULT_PERIOD); | ||
refreshChore = new QuotaRefresherChore(period, this); | ||
refreshChore = new QuotaRefresherChore(conf, period, this); | ||
rsServices.getChoreService().scheduleChore(refreshChore); | ||
} | ||
|
||
|
@@ -140,8 +150,7 @@ public QuotaLimiter getUserLimiter(final UserGroupInformation ugi, final TableNa | |
*/ | ||
public UserQuotaState getUserQuotaState(final UserGroupInformation ugi) { | ||
return computeIfAbsent(userQuotaCache, getQuotaUserName(ugi), | ||
() -> QuotaUtil.buildDefaultUserQuotaState(rsServices.getConfiguration(), 0L), | ||
this::triggerCacheRefresh); | ||
() -> QuotaUtil.buildDefaultUserQuotaState(rsServices.getConfiguration(), 0L)); | ||
Comment on lines
-143
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The change here, and in getQuotaState, are critical. These changes ensure that each cache miss does not trigger an immediate refresh — particularly given that cache entries are evicted after 5 refresh periods, this approach is too heavy handed. |
||
} | ||
|
||
/** | ||
|
@@ -202,7 +211,7 @@ private String getQuotaUserName(final UserGroupInformation ugi) { | |
* returned and the quota request will be enqueued for the next cache refresh. | ||
*/ | ||
private <K> QuotaState getQuotaState(final ConcurrentMap<K, QuotaState> quotasMap, final K key) { | ||
return computeIfAbsent(quotasMap, key, QuotaState::new, this::triggerCacheRefresh); | ||
return computeIfAbsent(quotasMap, key, QuotaState::new); | ||
} | ||
|
||
void triggerCacheRefresh() { | ||
|
@@ -233,8 +242,33 @@ Map<String, UserQuotaState> getUserQuotaCache() { | |
private class QuotaRefresherChore extends ScheduledChore { | ||
private long lastUpdate = 0; | ||
|
||
public QuotaRefresherChore(final int period, final Stoppable stoppable) { | ||
// Querying cluster metrics so often, per-RegionServer, limits horizontal scalability. | ||
// So we cache the results to reduce that load. | ||
private final RefreshableExpiringValueCache<ClusterMetrics> tableRegionStatesClusterMetrics; | ||
private final RefreshableExpiringValueCache<Integer> regionServersSize; | ||
|
||
public QuotaRefresherChore(Configuration conf, final int period, final Stoppable stoppable) { | ||
super("QuotaRefresherChore", stoppable, period); | ||
|
||
Duration tableRegionStatesCacheTtl = | ||
Duration.ofMillis(conf.getLong(TABLE_REGION_STATES_CACHE_TTL_MS, period)); | ||
this.tableRegionStatesClusterMetrics = | ||
new RefreshableExpiringValueCache<>("tableRegionStatesClusterMetrics", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a follow-up JIRA, should these configuration values be hot-reloadable ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would definitely be nice, and would probably be a larger refactor so it would definitely be nice to make that a separate issue. The quota refresh period is also static, and should probably be made dynamic in that same push |
||
tableRegionStatesCacheTtl, () -> rsServices.getConnection().getAdmin() | ||
.getClusterMetrics(EnumSet.of(Option.SERVERS_NAME, Option.TABLE_TO_REGIONS_COUNT))); | ||
|
||
Duration regionServersSizeCacheTtl = | ||
Duration.ofMillis(conf.getLong(REGION_SERVERS_SIZE_CACHE_TTL_MS, period)); | ||
regionServersSize = | ||
new RefreshableExpiringValueCache<>("regionServersSize", regionServersSizeCacheTtl, | ||
() -> rsServices.getConnection().getAdmin().getRegionServers().size()); | ||
} | ||
|
||
@Override | ||
public synchronized boolean triggerNow() { | ||
tableRegionStatesClusterMetrics.invalidate(); | ||
regionServersSize.invalidate(); | ||
return super.triggerNow(); | ||
} | ||
|
||
@Override | ||
|
@@ -395,21 +429,40 @@ private <K, V extends QuotaState> void fetch(final String type, | |
* over table quota, use [1 / TotalTableRegionNum * MachineTableRegionNum] as machine factor. | ||
*/ | ||
private void updateQuotaFactors() { | ||
// Update machine quota factor | ||
ClusterMetrics clusterMetrics; | ||
try { | ||
clusterMetrics = rsServices.getConnection().getAdmin() | ||
.getClusterMetrics(EnumSet.of(Option.SERVERS_NAME, Option.TABLE_TO_REGIONS_COUNT)); | ||
} catch (IOException e) { | ||
LOG.warn("Failed to get cluster metrics needed for updating quotas", e); | ||
return; | ||
boolean hasTableQuotas = !tableQuotaCache.entrySet().isEmpty() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if this is relevant here, but this check won't be atomic. the contents of tableQuotaCache can change while checking userQuotaCache. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's okay — the two conditions are definitely independent of each other, and the implication of that case would be that your tableQuotaCache addition missed the boat for this refresh and will be reflected in subsequent refreshes |
||
|| userQuotaCache.values().stream().anyMatch(UserQuotaState::hasTableLimiters); | ||
if (hasTableQuotas) { | ||
updateTableMachineQuotaFactors(); | ||
} else { | ||
updateOnlyMachineQuotaFactors(); | ||
Comment on lines
+434
to
+437
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check ensures that we only pull down every region state if we actually need to. Without table machine quotas, there is no point |
||
} | ||
} | ||
|
||
int rsSize = clusterMetrics.getServersName().size(); | ||
if (rsSize != 0) { | ||
// TODO if use rs group, the cluster limit should be shared by the rs group | ||
machineQuotaFactor = 1.0 / rsSize; | ||
/** | ||
* This method is cheaper than {@link #updateTableMachineQuotaFactors()} and should be used if | ||
* we don't have any table quotas in the cache. | ||
*/ | ||
private void updateOnlyMachineQuotaFactors() { | ||
Optional<Integer> rsSize = regionServersSize.get(); | ||
if (rsSize.isPresent()) { | ||
updateMachineQuotaFactors(rsSize.get()); | ||
} else { | ||
regionServersSize.refresh(); | ||
} | ||
} | ||
|
||
/** | ||
* This will call {@link #updateMachineQuotaFactors(int)}, and then update the table machine | ||
* factors as well. This relies on a more expensive query for ClusterMetrics. | ||
*/ | ||
private void updateTableMachineQuotaFactors() { | ||
Optional<ClusterMetrics> clusterMetricsMaybe = tableRegionStatesClusterMetrics.get(); | ||
if (!clusterMetricsMaybe.isPresent()) { | ||
tableRegionStatesClusterMetrics.refresh(); | ||
return; | ||
} | ||
ClusterMetrics clusterMetrics = clusterMetricsMaybe.get(); | ||
updateMachineQuotaFactors(clusterMetrics.getServersName().size()); | ||
|
||
Map<TableName, RegionStatesCount> tableRegionStatesCount = | ||
clusterMetrics.getTableRegionStatesCount(); | ||
|
@@ -436,6 +489,53 @@ private void updateQuotaFactors() { | |
} | ||
} | ||
} | ||
|
||
private void updateMachineQuotaFactors(int rsSize) { | ||
if (rsSize != 0) { | ||
// TODO if use rs group, the cluster limit should be shared by the rs group | ||
machineQuotaFactor = 1.0 / rsSize; | ||
} | ||
} | ||
} | ||
|
||
static class RefreshableExpiringValueCache<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this class because I don't think there's a good keyless equivalent to a LoadingCache, and a memoized supplier does not offer all of the functionality that I'd like (on-demand refresh, invalidation) |
||
private final String name; | ||
private final LoadingCache<String, Optional<T>> cache; | ||
|
||
RefreshableExpiringValueCache(String name, Duration refreshPeriod, | ||
ThrowingSupplier<T> supplier) { | ||
this.name = name; | ||
this.cache = | ||
CacheBuilder.newBuilder().expireAfterWrite(refreshPeriod.toMillis(), TimeUnit.MILLISECONDS) | ||
.build(new CacheLoader<>() { | ||
@Override | ||
public Optional<T> load(String key) { | ||
try { | ||
return Optional.of(supplier.get()); | ||
} catch (Exception e) { | ||
LOG.warn("Failed to refresh cache {}", name, e); | ||
return Optional.empty(); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
Optional<T> get() { | ||
return cache.getUnchecked(name); | ||
} | ||
|
||
void refresh() { | ||
cache.refresh(name); | ||
} | ||
|
||
void invalidate() { | ||
cache.invalidate(name); | ||
} | ||
} | ||
|
||
@FunctionalInterface | ||
static interface ThrowingSupplier<T> { | ||
T get() throws Exception; | ||
} | ||
|
||
static interface Fetcher<Key, Value> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,39 +78,32 @@ public static void tearDownAfterClass() throws Exception { | |
@Test | ||
public void testUserGlobalThrottleWithCustomOverride() throws Exception { | ||
final Admin admin = TEST_UTIL.getAdmin(); | ||
final String userOverrideWithQuota = User.getCurrent().getShortName() + "123"; | ||
final String userOverrideWithQuota = User.getCurrent().getShortName(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made a few small test changes to ensure that refreshes were called appropriately. Because of the logic in |
||
|
||
// Add 6req/min limit | ||
admin.setQuota(QuotaSettingsFactory.throttleUser(userOverrideWithQuota, | ||
ThrottleType.REQUEST_NUMBER, 6, TimeUnit.MINUTES)); | ||
ThrottleQuotaTestUtil.triggerUserCacheRefresh(TEST_UTIL, false, TABLE_NAME); | ||
|
||
Table tableWithThrottle = TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null) | ||
.setRequestAttribute(CUSTOM_OVERRIDE_KEY, Bytes.toBytes(userOverrideWithQuota)).build(); | ||
Table tableWithoutThrottle = TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null) | ||
.setRequestAttribute(QuotaCache.QUOTA_USER_REQUEST_ATTRIBUTE_OVERRIDE_KEY, | ||
Bytes.toBytes(userOverrideWithQuota)) | ||
.build(); | ||
Table tableWithoutThrottle2 = | ||
TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null).build(); | ||
.setRequestAttribute(CUSTOM_OVERRIDE_KEY, Bytes.toBytes("anotherUser")).build(); | ||
|
||
// warm things up | ||
doPuts(10, FAMILY, QUALIFIER, tableWithThrottle); | ||
doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle); | ||
doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle2); | ||
|
||
// should reject some requests | ||
assertTrue(10 > doPuts(10, FAMILY, QUALIFIER, tableWithThrottle)); | ||
// should accept all puts | ||
assertEquals(10, doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle)); | ||
// should accept all puts | ||
assertEquals(10, doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle2)); | ||
|
||
// Remove all the limits | ||
admin.setQuota(QuotaSettingsFactory.unthrottleUser(userOverrideWithQuota)); | ||
Thread.sleep(60_000); | ||
ThrottleQuotaTestUtil.triggerUserCacheRefresh(TEST_UTIL, true, TABLE_NAME); | ||
assertEquals(10, doPuts(10, FAMILY, QUALIFIER, tableWithThrottle)); | ||
assertEquals(10, doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle)); | ||
assertEquals(10, doPuts(10, FAMILY, QUALIFIER, tableWithoutThrottle2)); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for including a unit indicator in the config key! 🙇