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}