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