001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.hbase.quotas; 020 021import java.io.IOException; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.concurrent.ConcurrentHashMap; 029 030import org.apache.commons.lang3.builder.HashCodeBuilder; 031import org.apache.hadoop.hbase.DoNotRetryIOException; 032import org.apache.hadoop.hbase.MetaTableAccessor; 033import org.apache.hadoop.hbase.NamespaceDescriptor; 034import org.apache.hadoop.hbase.RegionStateListener; 035import org.apache.hadoop.hbase.TableName; 036import org.apache.hadoop.hbase.client.RegionInfo; 037import org.apache.hadoop.hbase.master.MasterServices; 038import org.apache.hadoop.hbase.namespace.NamespaceAuditor; 039import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 040import org.apache.yetus.audience.InterfaceAudience; 041import org.apache.yetus.audience.InterfaceStability; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; 045import org.apache.hbase.thirdparty.com.google.protobuf.TextFormat; 046import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; 047import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaRequest; 048import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaResponse; 049 050/** 051 * Master Quota Manager. 052 * It is responsible for initialize the quota table on the first-run and 053 * provide the admin operations to interact with the quota table. 054 * 055 * TODO: FUTURE: The master will be responsible to notify each RS of quota changes 056 * and it will do the "quota aggregation" when the QuotaScope is CLUSTER. 057 */ 058@InterfaceAudience.Private 059@InterfaceStability.Evolving 060public class MasterQuotaManager implements RegionStateListener { 061 private static final Logger LOG = LoggerFactory.getLogger(MasterQuotaManager.class); 062 private static final Map<RegionInfo, Long> EMPTY_MAP = Collections.unmodifiableMap( 063 new HashMap<>()); 064 065 private final MasterServices masterServices; 066 private NamedLock<String> namespaceLocks; 067 private NamedLock<TableName> tableLocks; 068 private NamedLock<String> userLocks; 069 private boolean initialized = false; 070 private NamespaceAuditor namespaceQuotaManager; 071 private ConcurrentHashMap<RegionInfo, SizeSnapshotWithTimestamp> regionSizes; 072 073 public MasterQuotaManager(final MasterServices masterServices) { 074 this.masterServices = masterServices; 075 } 076 077 public void start() throws IOException { 078 // If the user doesn't want the quota support skip all the initializations. 079 if (!QuotaUtil.isQuotaEnabled(masterServices.getConfiguration())) { 080 LOG.info("Quota support disabled"); 081 return; 082 } 083 084 // Create the quota table if missing 085 if (!MetaTableAccessor.tableExists(masterServices.getConnection(), 086 QuotaUtil.QUOTA_TABLE_NAME)) { 087 LOG.info("Quota table not found. Creating..."); 088 createQuotaTable(); 089 } 090 091 LOG.info("Initializing quota support"); 092 namespaceLocks = new NamedLock<>(); 093 tableLocks = new NamedLock<>(); 094 userLocks = new NamedLock<>(); 095 regionSizes = new ConcurrentHashMap<>(); 096 097 namespaceQuotaManager = new NamespaceAuditor(masterServices); 098 namespaceQuotaManager.start(); 099 initialized = true; 100 } 101 102 public void stop() { 103 } 104 105 public boolean isQuotaInitialized() { 106 return initialized && namespaceQuotaManager.isInitialized(); 107 } 108 109 /* ========================================================================== 110 * Admin operations to manage the quota table 111 */ 112 public SetQuotaResponse setQuota(final SetQuotaRequest req) 113 throws IOException, InterruptedException { 114 checkQuotaSupport(); 115 116 if (req.hasUserName()) { 117 userLocks.lock(req.getUserName()); 118 try { 119 if (req.hasTableName()) { 120 setUserQuota(req.getUserName(), ProtobufUtil.toTableName(req.getTableName()), req); 121 } else if (req.hasNamespace()) { 122 setUserQuota(req.getUserName(), req.getNamespace(), req); 123 } else { 124 setUserQuota(req.getUserName(), req); 125 } 126 } finally { 127 userLocks.unlock(req.getUserName()); 128 } 129 } else if (req.hasTableName()) { 130 TableName table = ProtobufUtil.toTableName(req.getTableName()); 131 tableLocks.lock(table); 132 try { 133 setTableQuota(table, req); 134 } finally { 135 tableLocks.unlock(table); 136 } 137 } else if (req.hasNamespace()) { 138 namespaceLocks.lock(req.getNamespace()); 139 try { 140 setNamespaceQuota(req.getNamespace(), req); 141 } finally { 142 namespaceLocks.unlock(req.getNamespace()); 143 } 144 } else { 145 throw new DoNotRetryIOException( 146 new UnsupportedOperationException("a user, a table or a namespace must be specified")); 147 } 148 return SetQuotaResponse.newBuilder().build(); 149 } 150 151 public void setUserQuota(final String userName, final SetQuotaRequest req) 152 throws IOException, InterruptedException { 153 setQuota(req, new SetQuotaOperations() { 154 @Override 155 public GlobalQuotaSettingsImpl fetch() throws IOException { 156 return new GlobalQuotaSettingsImpl(req.getUserName(), null, null, QuotaUtil.getUserQuota( 157 masterServices.getConnection(), userName)); 158 } 159 @Override 160 public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 161 QuotaUtil.addUserQuota(masterServices.getConnection(), userName, quotaPojo.toQuotas()); 162 } 163 @Override 164 public void delete() throws IOException { 165 QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName); 166 } 167 @Override 168 public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 169 masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, quotaPojo); 170 } 171 @Override 172 public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 173 masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, quotaPojo); 174 } 175 }); 176 } 177 178 public void setUserQuota(final String userName, final TableName table, 179 final SetQuotaRequest req) throws IOException, InterruptedException { 180 setQuota(req, new SetQuotaOperations() { 181 @Override 182 public GlobalQuotaSettingsImpl fetch() throws IOException { 183 return new GlobalQuotaSettingsImpl(userName, table, null, QuotaUtil.getUserQuota( 184 masterServices.getConnection(), userName, table)); 185 } 186 @Override 187 public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 188 QuotaUtil.addUserQuota(masterServices.getConnection(), userName, table, 189 quotaPojo.toQuotas()); 190 } 191 @Override 192 public void delete() throws IOException { 193 QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, table); 194 } 195 @Override 196 public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 197 masterServices.getMasterCoprocessorHost().preSetUserQuota(userName, table, quotaPojo); 198 } 199 @Override 200 public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 201 masterServices.getMasterCoprocessorHost().postSetUserQuota(userName, table, quotaPojo); 202 } 203 }); 204 } 205 206 public void setUserQuota(final String userName, final String namespace, 207 final SetQuotaRequest req) throws IOException, InterruptedException { 208 setQuota(req, new SetQuotaOperations() { 209 @Override 210 public GlobalQuotaSettingsImpl fetch() throws IOException { 211 return new GlobalQuotaSettingsImpl(userName, null, namespace, QuotaUtil.getUserQuota( 212 masterServices.getConnection(), userName, namespace)); 213 } 214 @Override 215 public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 216 QuotaUtil.addUserQuota(masterServices.getConnection(), userName, namespace, 217 quotaPojo.toQuotas()); 218 } 219 @Override 220 public void delete() throws IOException { 221 QuotaUtil.deleteUserQuota(masterServices.getConnection(), userName, namespace); 222 } 223 @Override 224 public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 225 masterServices.getMasterCoprocessorHost().preSetUserQuota( 226 userName, namespace, quotaPojo); 227 } 228 @Override 229 public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 230 masterServices.getMasterCoprocessorHost().postSetUserQuota( 231 userName, namespace, quotaPojo); 232 } 233 }); 234 } 235 236 public void setTableQuota(final TableName table, final SetQuotaRequest req) 237 throws IOException, InterruptedException { 238 setQuota(req, new SetQuotaOperations() { 239 @Override 240 public GlobalQuotaSettingsImpl fetch() throws IOException { 241 return new GlobalQuotaSettingsImpl(null, table, null, QuotaUtil.getTableQuota( 242 masterServices.getConnection(), table)); 243 } 244 @Override 245 public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 246 QuotaUtil.addTableQuota(masterServices.getConnection(), table, quotaPojo.toQuotas()); 247 } 248 @Override 249 public void delete() throws IOException { 250 QuotaUtil.deleteTableQuota(masterServices.getConnection(), table); 251 } 252 @Override 253 public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 254 masterServices.getMasterCoprocessorHost().preSetTableQuota(table, quotaPojo); 255 } 256 @Override 257 public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 258 masterServices.getMasterCoprocessorHost().postSetTableQuota(table, quotaPojo); 259 } 260 }); 261 } 262 263 public void setNamespaceQuota(final String namespace, final SetQuotaRequest req) 264 throws IOException, InterruptedException { 265 setQuota(req, new SetQuotaOperations() { 266 @Override 267 public GlobalQuotaSettingsImpl fetch() throws IOException { 268 return new GlobalQuotaSettingsImpl(null, null, namespace, QuotaUtil.getNamespaceQuota( 269 masterServices.getConnection(), namespace)); 270 } 271 @Override 272 public void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 273 QuotaUtil.addNamespaceQuota(masterServices.getConnection(), namespace, 274 ((GlobalQuotaSettingsImpl) quotaPojo).toQuotas()); 275 } 276 @Override 277 public void delete() throws IOException { 278 QuotaUtil.deleteNamespaceQuota(masterServices.getConnection(), namespace); 279 } 280 @Override 281 public void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 282 masterServices.getMasterCoprocessorHost().preSetNamespaceQuota(namespace, quotaPojo); 283 } 284 @Override 285 public void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException { 286 masterServices.getMasterCoprocessorHost().postSetNamespaceQuota(namespace, quotaPojo); 287 } 288 }); 289 } 290 291 public void setNamespaceQuota(NamespaceDescriptor desc) throws IOException { 292 if (initialized) { 293 this.namespaceQuotaManager.addNamespace(desc); 294 } 295 } 296 297 public void removeNamespaceQuota(String namespace) throws IOException { 298 if (initialized) { 299 this.namespaceQuotaManager.deleteNamespace(namespace); 300 } 301 } 302 303 private void setQuota(final SetQuotaRequest req, final SetQuotaOperations quotaOps) 304 throws IOException, InterruptedException { 305 if (req.hasRemoveAll() && req.getRemoveAll() == true) { 306 quotaOps.preApply(null); 307 quotaOps.delete(); 308 quotaOps.postApply(null); 309 return; 310 } 311 312 // Apply quota changes 313 GlobalQuotaSettingsImpl currentQuota = quotaOps.fetch(); 314 if (LOG.isTraceEnabled()) { 315 LOG.trace( 316 "Current quota for request(" + TextFormat.shortDebugString(req) 317 + "): " + currentQuota); 318 } 319 // Call the appropriate "pre" CP hook with the current quota value (may be null) 320 quotaOps.preApply(currentQuota); 321 // Translate the protobuf request back into a POJO 322 QuotaSettings newQuota = QuotaSettings.buildFromProto(req); 323 if (LOG.isTraceEnabled()) { 324 LOG.trace("Deserialized quota from request: " + newQuota); 325 } 326 327 // Merge the current quota settings with the new quota settings the user provided. 328 // 329 // NB: while SetQuotaRequest technically allows for multi types of quotas to be set in one 330 // message, the Java API (in Admin/AsyncAdmin) does not. Assume there is only one type. 331 GlobalQuotaSettingsImpl mergedQuota = currentQuota.merge(newQuota); 332 if (LOG.isTraceEnabled()) { 333 LOG.trace("Computed merged quota from current quota and user request: " + mergedQuota); 334 } 335 336 // Submit new changes 337 if (mergedQuota == null) { 338 quotaOps.delete(); 339 } else { 340 quotaOps.update(mergedQuota); 341 } 342 // Advertise the final result via the "post" CP hook 343 quotaOps.postApply(mergedQuota); 344 } 345 346 public void checkNamespaceTableAndRegionQuota(TableName tName, int regions) throws IOException { 347 if (initialized) { 348 namespaceQuotaManager.checkQuotaToCreateTable(tName, regions); 349 } 350 } 351 352 public void checkAndUpdateNamespaceRegionQuota(TableName tName, int regions) throws IOException { 353 if (initialized) { 354 namespaceQuotaManager.checkQuotaToUpdateRegion(tName, regions); 355 } 356 } 357 358 /** 359 * @return cached region count, or -1 if quota manager is disabled or table status not found 360 */ 361 public int getRegionCountOfTable(TableName tName) throws IOException { 362 if (initialized) { 363 return namespaceQuotaManager.getRegionCountOfTable(tName); 364 } 365 return -1; 366 } 367 368 @Override 369 public void onRegionMerged(RegionInfo mergedRegion) throws IOException { 370 if (initialized) { 371 namespaceQuotaManager.updateQuotaForRegionMerge(mergedRegion); 372 } 373 } 374 375 @Override 376 public void onRegionSplit(RegionInfo hri) throws IOException { 377 if (initialized) { 378 namespaceQuotaManager.checkQuotaToSplitRegion(hri); 379 } 380 } 381 382 /** 383 * Remove table from namespace quota. 384 * 385 * @param tName - The table name to update quota usage. 386 * @throws IOException Signals that an I/O exception has occurred. 387 */ 388 public void removeTableFromNamespaceQuota(TableName tName) throws IOException { 389 if (initialized) { 390 namespaceQuotaManager.removeFromNamespaceUsage(tName); 391 } 392 } 393 394 public NamespaceAuditor getNamespaceQuotaManager() { 395 return this.namespaceQuotaManager; 396 } 397 398 /** 399 * Encapsulates CRUD quota operations for some subject. 400 */ 401 private static interface SetQuotaOperations { 402 /** 403 * Fetches the current quota settings for the subject. 404 */ 405 GlobalQuotaSettingsImpl fetch() throws IOException; 406 /** 407 * Deletes the quota for the subject. 408 */ 409 void delete() throws IOException; 410 /** 411 * Persist the given quota for the subject. 412 */ 413 void update(GlobalQuotaSettingsImpl quotaPojo) throws IOException; 414 /** 415 * Performs some action before {@link #update(GlobalQuotaSettingsImpl)} with the current 416 * quota for the subject. 417 */ 418 void preApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException; 419 /** 420 * Performs some action after {@link #update(GlobalQuotaSettingsImpl)} with the resulting 421 * quota from the request action for the subject. 422 */ 423 void postApply(GlobalQuotaSettingsImpl quotaPojo) throws IOException; 424 } 425 426 /* ========================================================================== 427 * Helpers 428 */ 429 430 private void checkQuotaSupport() throws IOException { 431 if (!QuotaUtil.isQuotaEnabled(masterServices.getConfiguration())) { 432 throw new DoNotRetryIOException( 433 new UnsupportedOperationException("quota support disabled")); 434 } 435 if (!initialized) { 436 long maxWaitTime = masterServices.getConfiguration().getLong( 437 "hbase.master.wait.for.quota.manager.init", 30000); // default is 30 seconds. 438 long startTime = EnvironmentEdgeManager.currentTime(); 439 do { 440 try { 441 Thread.sleep(100); 442 } catch (InterruptedException e) { 443 LOG.warn("Interrupted while waiting for Quota Manager to be initialized."); 444 break; 445 } 446 } while (!initialized && (EnvironmentEdgeManager.currentTime() - startTime) < maxWaitTime); 447 if (!initialized) { 448 throw new IOException("Quota manager is uninitialized, please retry later."); 449 } 450 } 451 } 452 453 private void createQuotaTable() throws IOException { 454 masterServices.createSystemTable(QuotaUtil.QUOTA_TABLE_DESC); 455 } 456 457 private static class NamedLock<T> { 458 private final HashSet<T> locks = new HashSet<>(); 459 460 public void lock(final T name) throws InterruptedException { 461 synchronized (locks) { 462 while (locks.contains(name)) { 463 locks.wait(); 464 } 465 locks.add(name); 466 } 467 } 468 469 public void unlock(final T name) { 470 synchronized (locks) { 471 locks.remove(name); 472 locks.notifyAll(); 473 } 474 } 475 } 476 477 @Override 478 public void onRegionSplitReverted(RegionInfo hri) throws IOException { 479 if (initialized) { 480 this.namespaceQuotaManager.removeRegionFromNamespaceUsage(hri); 481 } 482 } 483 484 /** 485 * Holds the size of a region at the given time, millis since the epoch. 486 */ 487 private static class SizeSnapshotWithTimestamp { 488 private final long size; 489 private final long time; 490 491 public SizeSnapshotWithTimestamp(long size, long time) { 492 this.size = size; 493 this.time = time; 494 } 495 496 public long getSize() { 497 return size; 498 } 499 500 public long getTime() { 501 return time; 502 } 503 504 @Override 505 public boolean equals(Object o) { 506 if (o instanceof SizeSnapshotWithTimestamp) { 507 SizeSnapshotWithTimestamp other = (SizeSnapshotWithTimestamp) o; 508 return size == other.size && time == other.time; 509 } 510 return false; 511 } 512 513 @Override 514 public int hashCode() { 515 HashCodeBuilder hcb = new HashCodeBuilder(); 516 return hcb.append(size).append(time).toHashCode(); 517 } 518 519 @Override 520 public String toString() { 521 StringBuilder sb = new StringBuilder(32); 522 sb.append("SizeSnapshotWithTimestamp={size=").append(size).append("B, "); 523 sb.append("time=").append(time).append("}"); 524 return sb.toString(); 525 } 526 } 527 528 @VisibleForTesting 529 void initializeRegionSizes() { 530 assert regionSizes == null; 531 this.regionSizes = new ConcurrentHashMap<>(); 532 } 533 534 public void addRegionSize(RegionInfo hri, long size, long time) { 535 if (regionSizes == null) { 536 return; 537 } 538 regionSizes.put(hri, new SizeSnapshotWithTimestamp(size, time)); 539 } 540 541 public Map<RegionInfo, Long> snapshotRegionSizes() { 542 if (regionSizes == null) { 543 return EMPTY_MAP; 544 } 545 546 Map<RegionInfo, Long> copy = new HashMap<>(); 547 for (Entry<RegionInfo, SizeSnapshotWithTimestamp> entry : regionSizes.entrySet()) { 548 copy.put(entry.getKey(), entry.getValue().getSize()); 549 } 550 return copy; 551 } 552 553 int pruneEntriesOlderThan(long timeToPruneBefore) { 554 if (regionSizes == null) { 555 return 0; 556 } 557 int numEntriesRemoved = 0; 558 Iterator<Entry<RegionInfo,SizeSnapshotWithTimestamp>> iterator = 559 regionSizes.entrySet().iterator(); 560 while (iterator.hasNext()) { 561 long currentEntryTime = iterator.next().getValue().getTime(); 562 if (currentEntryTime < timeToPruneBefore) { 563 iterator.remove(); 564 numEntriesRemoved++; 565 } 566 } 567 return numEntriesRemoved; 568 } 569} 570