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 */ 018package org.apache.hadoop.hbase.master.normalizer; 019 020import static org.hamcrest.Matchers.comparesEqualTo; 021import static org.hamcrest.Matchers.greaterThanOrEqualTo; 022import static org.hamcrest.Matchers.lessThanOrEqualTo; 023import static org.hamcrest.Matchers.not; 024import static org.junit.Assert.assertEquals; 025import static org.junit.Assert.assertFalse; 026import static org.junit.Assert.assertNotNull; 027import static org.junit.Assert.assertTrue; 028import java.io.IOException; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.Comparator; 032import java.util.List; 033import java.util.concurrent.TimeUnit; 034import org.apache.hadoop.hbase.HBaseClassTestRule; 035import org.apache.hadoop.hbase.HBaseTestingUtility; 036import org.apache.hadoop.hbase.HConstants; 037import org.apache.hadoop.hbase.MatcherPredicate; 038import org.apache.hadoop.hbase.NamespaceDescriptor; 039import org.apache.hadoop.hbase.RegionMetrics; 040import org.apache.hadoop.hbase.ServerName; 041import org.apache.hadoop.hbase.Size; 042import org.apache.hadoop.hbase.TableName; 043import org.apache.hadoop.hbase.Waiter.ExplainingPredicate; 044import org.apache.hadoop.hbase.client.Admin; 045import org.apache.hadoop.hbase.client.NormalizeTableFilterParams; 046import org.apache.hadoop.hbase.client.Put; 047import org.apache.hadoop.hbase.client.RegionInfo; 048import org.apache.hadoop.hbase.client.RegionLocator; 049import org.apache.hadoop.hbase.client.Table; 050import org.apache.hadoop.hbase.client.TableDescriptor; 051import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 052import org.apache.hadoop.hbase.master.HMaster; 053import org.apache.hadoop.hbase.master.MasterServices; 054import org.apache.hadoop.hbase.master.TableNamespaceManager; 055import org.apache.hadoop.hbase.master.normalizer.NormalizationPlan.PlanType; 056import org.apache.hadoop.hbase.namespace.TestNamespaceAuditor; 057import org.apache.hadoop.hbase.quotas.QuotaUtil; 058import org.apache.hadoop.hbase.regionserver.HRegion; 059import org.apache.hadoop.hbase.regionserver.Region; 060import org.apache.hadoop.hbase.testclassification.MasterTests; 061import org.apache.hadoop.hbase.testclassification.MediumTests; 062import org.apache.hadoop.hbase.util.Bytes; 063import org.apache.hadoop.hbase.util.LoadTestKVGenerator; 064import org.hamcrest.Matcher; 065import org.hamcrest.Matchers; 066import org.junit.AfterClass; 067import org.junit.Before; 068import org.junit.BeforeClass; 069import org.junit.ClassRule; 070import org.junit.Rule; 071import org.junit.Test; 072import org.junit.experimental.categories.Category; 073import org.junit.rules.TestName; 074import org.slf4j.Logger; 075import org.slf4j.LoggerFactory; 076 077/** 078 * Testing {@link SimpleRegionNormalizer} on minicluster. 079 */ 080@Category({MasterTests.class, MediumTests.class}) 081public class TestSimpleRegionNormalizerOnCluster { 082 private static final Logger LOG = 083 LoggerFactory.getLogger(TestSimpleRegionNormalizerOnCluster.class); 084 085 @ClassRule 086 public static final HBaseClassTestRule CLASS_RULE = 087 HBaseClassTestRule.forClass(TestSimpleRegionNormalizerOnCluster.class); 088 089 private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 090 private static final byte[] FAMILY_NAME = Bytes.toBytes("fam"); 091 092 private static Admin admin; 093 private static HMaster master; 094 095 @Rule 096 public TestName name = new TestName(); 097 098 @BeforeClass 099 public static void beforeAllTests() throws Exception { 100 // we will retry operations when PleaseHoldException is thrown 101 TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 3); 102 TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true); 103 104 // no way for the test to set the regionId on a created region, so disable this feature. 105 TEST_UTIL.getConfiguration().setInt("hbase.normalizer.merge.min_region_age.days", 0); 106 107 // disable the normalizer coming along and running via Chore 108 TEST_UTIL.getConfiguration().setInt("hbase.normalizer.period", Integer.MAX_VALUE); 109 110 TEST_UTIL.startMiniCluster(1); 111 TestNamespaceAuditor.waitForQuotaInitialize(TEST_UTIL); 112 admin = TEST_UTIL.getAdmin(); 113 master = TEST_UTIL.getHBaseCluster().getMaster(); 114 assertNotNull(master); 115 } 116 117 @AfterClass 118 public static void afterAllTests() throws Exception { 119 TEST_UTIL.shutdownMiniCluster(); 120 } 121 122 @Before 123 public void before() throws Exception { 124 // disable the normalizer ahead of time, let the test enable it when its ready. 125 admin.normalizerSwitch(false); 126 } 127 128 @Test 129 public void testHonorsNormalizerSwitch() throws Exception { 130 assertFalse(admin.isNormalizerEnabled()); 131 assertFalse(admin.normalize()); 132 assertFalse(admin.normalizerSwitch(true)); 133 assertTrue(admin.normalize()); 134 } 135 136 /** 137 * Test that disabling normalizer via table configuration is honored. There's 138 * no side-effect to look for (other than a log message), so normalize two 139 * tables, one with the disabled setting, and look for change in one and no 140 * change in the other. 141 */ 142 @Test 143 public void testHonorsNormalizerTableSetting() throws Exception { 144 final TableName tn1 = TableName.valueOf(name.getMethodName() + "1"); 145 final TableName tn2 = TableName.valueOf(name.getMethodName() + "2"); 146 final TableName tn3 = TableName.valueOf(name.getMethodName() + "3"); 147 148 try { 149 final int tn1RegionCount = createTableBegsSplit(tn1, true, false); 150 final int tn2RegionCount = createTableBegsSplit(tn2, false, false); 151 final int tn3RegionCount = createTableBegsSplit(tn3, true, true); 152 153 assertFalse(admin.normalizerSwitch(true)); 154 assertTrue(admin.normalize()); 155 waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1)); 156 157 // confirm that tn1 has (tn1RegionCount + 1) number of regions. 158 // tn2 has tn2RegionCount number of regions because normalizer has not been enabled on it. 159 // tn3 has tn3RegionCount number of regions because two plans are run: 160 // 1. split one region to two 161 // 2. merge two regions into one 162 // and hence, total number of regions for tn3 remains same 163 assertEquals( 164 tn1 + " should have split.", 165 tn1RegionCount + 1, 166 getRegionCount(tn1)); 167 assertEquals( 168 tn2 + " should not have split.", 169 tn2RegionCount, 170 getRegionCount(tn2)); 171 LOG.debug("waiting for t3 to settle..."); 172 waitForTableRegionCount(tn3, comparesEqualTo(tn3RegionCount)); 173 } finally { 174 dropIfExists(tn1); 175 dropIfExists(tn2); 176 dropIfExists(tn3); 177 } 178 } 179 180 @Test 181 public void testRegionNormalizationSplitWithoutQuotaLimit() throws Exception { 182 testRegionNormalizationSplit(false); 183 } 184 185 @Test 186 public void testRegionNormalizationSplitWithQuotaLimit() throws Exception { 187 testRegionNormalizationSplit(true); 188 } 189 190 void testRegionNormalizationSplit(boolean limitedByQuota) throws Exception { 191 TableName tableName = null; 192 try { 193 tableName = limitedByQuota 194 ? buildTableNameForQuotaTest(name.getMethodName()) 195 : TableName.valueOf(name.getMethodName()); 196 197 final int currentRegionCount = createTableBegsSplit(tableName, true, false); 198 final long existingSkippedSplitCount = master.getRegionNormalizerManager() 199 .getSkippedCount(PlanType.SPLIT); 200 assertFalse(admin.normalizerSwitch(true)); 201 assertTrue(admin.normalize()); 202 if (limitedByQuota) { 203 waitForSkippedSplits(master, existingSkippedSplitCount); 204 assertEquals( 205 tableName + " should not have split.", 206 currentRegionCount, 207 getRegionCount(tableName)); 208 } else { 209 waitForTableRegionCount(tableName, greaterThanOrEqualTo(currentRegionCount + 1)); 210 assertEquals( 211 tableName + " should have split.", 212 currentRegionCount + 1, 213 getRegionCount(tableName)); 214 } 215 } finally { 216 dropIfExists(tableName); 217 } 218 } 219 220 @Test 221 public void testRegionNormalizationMerge() throws Exception { 222 final TableName tableName = TableName.valueOf(name.getMethodName()); 223 try { 224 final int currentRegionCount = createTableBegsMerge(tableName); 225 assertFalse(admin.normalizerSwitch(true)); 226 assertTrue(admin.normalize()); 227 waitForTableRegionCount(tableName, lessThanOrEqualTo(currentRegionCount - 1)); 228 assertEquals( 229 tableName + " should have merged.", 230 currentRegionCount - 1, 231 getRegionCount(tableName)); 232 } finally { 233 dropIfExists(tableName); 234 } 235 } 236 237 @Test 238 public void testHonorsNamespaceFilter() throws Exception { 239 final NamespaceDescriptor namespaceDescriptor = NamespaceDescriptor.create("ns").build(); 240 final TableName tn1 = TableName.valueOf("ns", name.getMethodName()); 241 final TableName tn2 = TableName.valueOf(name.getMethodName()); 242 243 try { 244 admin.createNamespace(namespaceDescriptor); 245 final int tn1RegionCount = createTableBegsSplit(tn1, true, false); 246 final int tn2RegionCount = createTableBegsSplit(tn2, true, false); 247 final NormalizeTableFilterParams ntfp = new NormalizeTableFilterParams.Builder() 248 .namespace("ns") 249 .build(); 250 251 assertFalse(admin.normalizerSwitch(true)); 252 assertTrue(admin.normalize(ntfp)); 253 waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1)); 254 255 // confirm that tn1 has (tn1RegionCount + 1) number of regions. 256 // tn2 has tn2RegionCount number of regions because it's not a member of the target namespace. 257 assertEquals( 258 tn1 + " should have split.", 259 tn1RegionCount + 1, 260 getRegionCount(tn1)); 261 waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount)); 262 } finally { 263 dropIfExists(tn1); 264 dropIfExists(tn2); 265 } 266 } 267 268 @Test 269 public void testHonorsPatternFilter() throws Exception { 270 final TableName tn1 = TableName.valueOf(name.getMethodName() + "1"); 271 final TableName tn2 = TableName.valueOf(name.getMethodName() + "2"); 272 273 try { 274 final int tn1RegionCount = createTableBegsSplit(tn1, true, false); 275 final int tn2RegionCount = createTableBegsSplit(tn2, true, false); 276 final NormalizeTableFilterParams ntfp = new NormalizeTableFilterParams.Builder() 277 .regex(".*[1]") 278 .build(); 279 280 assertFalse(admin.normalizerSwitch(true)); 281 assertTrue(admin.normalize(ntfp)); 282 waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1)); 283 284 // confirm that tn1 has (tn1RegionCount + 1) number of regions. 285 // tn2 has tn2RegionCount number of regions because it fails filter. 286 assertEquals( 287 tn1 + " should have split.", 288 tn1RegionCount + 1, 289 getRegionCount(tn1)); 290 waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount)); 291 } finally { 292 dropIfExists(tn1); 293 dropIfExists(tn2); 294 } 295 } 296 297 @Test 298 public void testHonorsNameFilter() throws Exception { 299 final TableName tn1 = TableName.valueOf(name.getMethodName() + "1"); 300 final TableName tn2 = TableName.valueOf(name.getMethodName() + "2"); 301 302 try { 303 final int tn1RegionCount = createTableBegsSplit(tn1, true, false); 304 final int tn2RegionCount = createTableBegsSplit(tn2, true, false); 305 final NormalizeTableFilterParams ntfp = new NormalizeTableFilterParams.Builder() 306 .tableNames(Collections.singletonList(tn1)) 307 .build(); 308 309 assertFalse(admin.normalizerSwitch(true)); 310 assertTrue(admin.normalize(ntfp)); 311 waitForTableRegionCount(tn1, greaterThanOrEqualTo(tn1RegionCount + 1)); 312 313 // confirm that tn1 has (tn1RegionCount + 1) number of regions. 314 // tn2 has tn3RegionCount number of regions because it fails filter: 315 assertEquals( 316 tn1 + " should have split.", 317 tn1RegionCount + 1, 318 getRegionCount(tn1)); 319 waitForTableRegionCount(tn2, comparesEqualTo(tn2RegionCount)); 320 } finally { 321 dropIfExists(tn1); 322 dropIfExists(tn2); 323 } 324 } 325 326 /** 327 * A test for when a region is the target of both a split and a merge plan. Does not define 328 * expected behavior, only that some change is applied to the table. 329 */ 330 @Test 331 public void testTargetOfSplitAndMerge() throws Exception { 332 final TableName tn = TableName.valueOf(name.getMethodName()); 333 try { 334 final int tnRegionCount = createTableTargetOfSplitAndMerge(tn); 335 assertFalse(admin.normalizerSwitch(true)); 336 assertTrue(admin.normalize()); 337 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5), new MatcherPredicate<>( 338 "expected " + tn + " to split or merge (probably split)", 339 () -> getRegionCountUnchecked(tn), 340 not(comparesEqualTo(tnRegionCount)))); 341 } finally { 342 dropIfExists(tn); 343 } 344 } 345 346 private static TableName buildTableNameForQuotaTest(final String methodName) throws Exception { 347 String nsp = "np2"; 348 NamespaceDescriptor nspDesc = 349 NamespaceDescriptor.create(nsp) 350 .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "5") 351 .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); 352 admin.createNamespace(nspDesc); 353 return TableName.valueOf(nsp, methodName); 354 } 355 356 private static void waitForSkippedSplits(final HMaster master, 357 final long existingSkippedSplitCount) { 358 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5), new MatcherPredicate<>( 359 "waiting to observe split attempt and skipped.", 360 () -> master.getRegionNormalizerManager().getSkippedCount(PlanType.SPLIT), 361 Matchers.greaterThan(existingSkippedSplitCount))); 362 } 363 364 private static void waitForTableRegionCount(final TableName tableName, 365 Matcher<? super Integer> matcher) { 366 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5), new MatcherPredicate<>( 367 "region count for table " + tableName + " does not match expected", 368 () -> getRegionCountUnchecked(tableName), 369 matcher)); 370 } 371 372 private static List<HRegion> generateTestData(final TableName tableName, 373 final int... regionSizesMb) throws IOException { 374 final List<HRegion> generatedRegions; 375 final int numRegions = regionSizesMb.length; 376 LOG.debug("generating test data into {}, {} regions of sizes (mb) {}", tableName, numRegions, 377 regionSizesMb); 378 try (Table ignored = TEST_UTIL.createMultiRegionTable(tableName, FAMILY_NAME, numRegions)) { 379 // Need to get sorted list of regions here 380 generatedRegions = new ArrayList<>(TEST_UTIL.getHBaseCluster().getRegions(tableName)); 381 generatedRegions.sort(Comparator.comparing(HRegion::getRegionInfo, RegionInfo.COMPARATOR)); 382 assertEquals(numRegions, generatedRegions.size()); 383 for (int i = 0; i < numRegions; i++) { 384 HRegion region = generatedRegions.get(i); 385 generateTestData(region, regionSizesMb[i]); 386 region.flush(true); 387 } 388 } 389 return generatedRegions; 390 } 391 392 private static void generateTestData(Region region, int numRows) throws IOException { 393 // generating 1Mb values 394 LOG.debug("writing {}mb to {}", numRows, region); 395 LoadTestKVGenerator dataGenerator = new LoadTestKVGenerator(1024 * 1024, 1024 * 1024); 396 for (int i = 0; i < numRows; ++i) { 397 byte[] key = Bytes.add(region.getRegionInfo().getStartKey(), Bytes.toBytes(i)); 398 for (int j = 0; j < 1; ++j) { 399 Put put = new Put(key); 400 byte[] col = Bytes.toBytes(String.valueOf(j)); 401 byte[] value = dataGenerator.generateRandomSizeValue(key, col); 402 put.addColumn(FAMILY_NAME, col, value); 403 region.put(put); 404 } 405 } 406 } 407 408 private static double getRegionSizeMB(final MasterServices masterServices, 409 final RegionInfo regionInfo) { 410 final ServerName sn = masterServices.getAssignmentManager() 411 .getRegionStates() 412 .getRegionServerOfRegion(regionInfo); 413 final RegionMetrics regionLoad = masterServices.getServerManager() 414 .getLoad(sn) 415 .getRegionMetrics() 416 .get(regionInfo.getRegionName()); 417 if (regionLoad == null) { 418 LOG.debug("{} was not found in RegionsLoad", regionInfo.getRegionNameAsString()); 419 return -1; 420 } 421 return regionLoad.getStoreFileSize().get(Size.Unit.MEGABYTE); 422 } 423 424 /** 425 * create a table with 5 regions, having region sizes so as to provoke a split 426 * of the largest region. 427 * <ul> 428 * <li>total table size: 12</li> 429 * <li>average region size: 2.4</li> 430 * <li>split threshold: 2.4 * 2 = 4.8</li> 431 * </ul> 432 */ 433 private static int createTableBegsSplit(final TableName tableName, 434 final boolean normalizerEnabled, final boolean isMergeEnabled) 435 throws Exception { 436 final List<HRegion> generatedRegions = generateTestData(tableName, 1, 1, 2, 3, 5); 437 assertEquals(5, getRegionCount(tableName)); 438 admin.flush(tableName); 439 440 final TableDescriptor td = TableDescriptorBuilder 441 .newBuilder(admin.getDescriptor(tableName)) 442 .setNormalizationEnabled(normalizerEnabled) 443 .setMergeEnabled(isMergeEnabled) 444 .build(); 445 admin.modifyTable(td); 446 447 // make sure relatively accurate region statistics are available for the test table. use 448 // the last/largest region as clue. 449 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(1), new ExplainingPredicate<IOException>() { 450 @Override public String explainFailure() { 451 return "expected largest region to be >= 4mb."; 452 } 453 @Override public boolean evaluate() { 454 return generatedRegions.stream() 455 .mapToDouble(val -> getRegionSizeMB(master, val.getRegionInfo())) 456 .allMatch(val -> val > 0) 457 && getRegionSizeMB(master, generatedRegions.get(4).getRegionInfo()) >= 4.0; 458 } 459 }); 460 return 5; 461 } 462 463 /** 464 * create a table with 5 regions, having region sizes so as to provoke a merge 465 * of the smallest regions. 466 * <ul> 467 * <li>total table size: 13</li> 468 * <li>average region size: 2.6</li> 469 * <li>sum of sizes of first two regions < average</li> 470 * </ul> 471 */ 472 private static int createTableBegsMerge(final TableName tableName) throws Exception { 473 // create 5 regions with sizes to trigger merge of small regions 474 final List<HRegion> generatedRegions = generateTestData(tableName, 1, 1, 3, 3, 5); 475 assertEquals(5, getRegionCount(tableName)); 476 admin.flush(tableName); 477 478 final TableDescriptor td = TableDescriptorBuilder 479 .newBuilder(admin.getDescriptor(tableName)) 480 .setNormalizationEnabled(true) 481 .build(); 482 admin.modifyTable(td); 483 484 // make sure relatively accurate region statistics are available for the test table. use 485 // the last/largest region as clue. 486 LOG.debug("waiting for region statistics to settle."); 487 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(1), new ExplainingPredicate<IOException>() { 488 @Override public String explainFailure() { 489 return "expected largest region to be >= 4mb."; 490 } 491 @Override public boolean evaluate() { 492 return generatedRegions.stream() 493 .mapToDouble(val -> getRegionSizeMB(master, val.getRegionInfo())) 494 .allMatch(val -> val > 0) 495 && getRegionSizeMB(master, generatedRegions.get(4).getRegionInfo()) >= 4.0; 496 } 497 }); 498 return 5; 499 } 500 501 /** 502 * Create a table with 4 regions, having region sizes so as to provoke a split of the largest 503 * region and a merge of an empty region into the largest. 504 * <ul> 505 * <li>total table size: 14</li> 506 * <li>average region size: 3.5</li> 507 * </ul> 508 */ 509 private static int createTableTargetOfSplitAndMerge(final TableName tableName) throws Exception { 510 final int[] regionSizesMb = { 10, 0, 2, 2 }; 511 final List<HRegion> generatedRegions = generateTestData(tableName, regionSizesMb); 512 assertEquals(4, getRegionCount(tableName)); 513 admin.flush(tableName); 514 515 final TableDescriptor td = TableDescriptorBuilder 516 .newBuilder(admin.getDescriptor(tableName)) 517 .setNormalizationEnabled(true) 518 .build(); 519 admin.modifyTable(td); 520 521 // make sure relatively accurate region statistics are available for the test table. use 522 // the last/largest region as clue. 523 LOG.debug("waiting for region statistics to settle."); 524 TEST_UTIL.waitFor(TimeUnit.MINUTES.toMillis(5), new ExplainingPredicate<IOException>() { 525 @Override public String explainFailure() { 526 return "expected largest region to be >= 10mb."; 527 } 528 @Override public boolean evaluate() { 529 for (int i = 0; i < generatedRegions.size(); i++) { 530 final RegionInfo regionInfo = generatedRegions.get(i).getRegionInfo(); 531 if (!(getRegionSizeMB(master, regionInfo) >= regionSizesMb[i])) { 532 return false; 533 } 534 } 535 return true; 536 } 537 }); 538 return 4; 539 } 540 541 private static void dropIfExists(final TableName tableName) throws Exception { 542 if (tableName != null && admin.tableExists(tableName)) { 543 if (admin.isTableEnabled(tableName)) { 544 admin.disableTable(tableName); 545 } 546 admin.deleteTable(tableName); 547 } 548 } 549 550 private static int getRegionCount(TableName tableName) throws IOException { 551 try (RegionLocator locator = TEST_UTIL.getConnection().getRegionLocator(tableName)) { 552 return locator.getAllRegionLocations().size(); 553 } 554 } 555 556 private static int getRegionCountUnchecked(final TableName tableName) { 557 try { 558 return getRegionCount(tableName); 559 } catch (IOException e) { 560 throw new RuntimeException(e); 561 } 562 } 563}