You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by an...@apache.org on 2015/06/23 05:43:53 UTC

hbase git commit: HBASE-13103 [ergonomics] add region size balancing as a feature of master

Repository: hbase
Updated Branches:
  refs/heads/master d51a18405 -> fd37ccb63


HBASE-13103 [ergonomics] add region size balancing as a feature of master


Project: http://git-wip-us.apache.org/repos/asf/hbase/repo
Commit: http://git-wip-us.apache.org/repos/asf/hbase/commit/fd37ccb6
Tree: http://git-wip-us.apache.org/repos/asf/hbase/tree/fd37ccb6
Diff: http://git-wip-us.apache.org/repos/asf/hbase/diff/fd37ccb6

Branch: refs/heads/master
Commit: fd37ccb63c545850c08c132b2f6470354a6629f9
Parents: d51a184
Author: Mikhail Antonov <an...@apache.org>
Authored: Mon Jun 22 15:52:07 2015 -0700
Committer: Mikhail Antonov <an...@apache.org>
Committed: Mon Jun 22 15:52:07 2015 -0700

----------------------------------------------------------------------
 .../apache/hadoop/hbase/HTableDescriptor.java   |  35 +++
 .../org/apache/hadoop/hbase/HConstants.java     |   8 +
 .../src/main/resources/hbase-default.xml        |  20 ++
 .../org/apache/hadoop/hbase/master/HMaster.java |  65 ++++-
 .../normalizer/EmptyNormalizationPlan.java      |  48 ++++
 .../normalizer/MergeNormalizationPlan.java      |  73 ++++++
 .../master/normalizer/NormalizationPlan.java    |  35 +++
 .../master/normalizer/RegionNormalizer.java     |  51 ++++
 .../normalizer/RegionNormalizerChore.java       |  53 ++++
 .../normalizer/RegionNormalizerFactory.java     |  48 ++++
 .../normalizer/SimpleRegionNormalizer.java      | 176 ++++++++++++++
 .../normalizer/SplitNormalizationPlan.java      |  81 +++++++
 .../normalizer/TestSimpleRegionNormalizer.java  | 240 +++++++++++++++++++
 .../TestSimpleRegionNormalizerOnCluster.java    | 218 +++++++++++++++++
 14 files changed, 1146 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
----------------------------------------------------------------------
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
index 58067ea..9145cdc 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
@@ -185,6 +185,16 @@ public class HTableDescriptor implements Comparable<HTableDescriptor> {
   private static final Bytes REGION_MEMSTORE_REPLICATION_KEY =
       new Bytes(Bytes.toBytes(REGION_MEMSTORE_REPLICATION));
 
+  /**
+   * <em>INTERNAL</em> Used by shell/rest interface to access this metadata
+   * attribute which denotes if the table should be treated by region normalizer.
+   *
+   * @see #isNormalizationEnabled()
+   */
+  public static final String NORMALIZATION_ENABLED = "NORMALIZATION_ENABLED";
+  private static final Bytes NORMALIZATION_ENABLED_KEY =
+    new Bytes(Bytes.toBytes(NORMALIZATION_ENABLED));
+
   /** Default durability for HTD is USE_DEFAULT, which defaults to HBase-global default value */
   private static final Durability DEFAULT_DURABLITY = Durability.USE_DEFAULT;
 
@@ -212,6 +222,11 @@ public class HTableDescriptor implements Comparable<HTableDescriptor> {
   public static final boolean DEFAULT_COMPACTION_ENABLED = true;
 
   /**
+   * Constant that denotes whether the table is normalized by default.
+   */
+  public static final boolean DEFAULT_NORMALIZATION_ENABLED = false;
+
+  /**
    * Constant that denotes the maximum default size of the memstore after which
    * the contents are flushed to the store files
    */
@@ -614,6 +629,26 @@ public class HTableDescriptor implements Comparable<HTableDescriptor> {
   }
 
   /**
+   * Check if normalization enable flag of the table is true. If flag is
+   * false then no region normalizer won't attempt to normalize this table.
+   *
+   * @return true if region normalization is enabled for this table
+   */
+  public boolean isNormalizationEnabled() {
+    return isSomething(NORMALIZATION_ENABLED_KEY, DEFAULT_NORMALIZATION_ENABLED);
+  }
+
+  /**
+   * Setting the table normalization enable flag.
+   *
+   * @param isEnable True if enable normalization.
+   */
+  public HTableDescriptor setNormalizationEnabled(final boolean isEnable) {
+    setValue(NORMALIZATION_ENABLED_KEY, isEnable ? TRUE : FALSE);
+    return this;
+  }
+
+  /**
    * Sets the {@link Durability} setting for the table. This defaults to Durability.USE_DEFAULT.
    * @param durability enum value
    */

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java
----------------------------------------------------------------------
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java
index 89a3f34..c1316a9 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java
@@ -123,6 +123,14 @@ public final class HConstants {
   /** Config for pluggable load balancers */
   public static final String HBASE_MASTER_LOADBALANCER_CLASS = "hbase.master.loadbalancer.class";
 
+  /** Config for pluggable region normalizer */
+  public static final String HBASE_MASTER_NORMALIZER_CLASS =
+    "hbase.master.normalizer.class";
+
+  /** Config for enabling/disabling pluggable region normalizer */
+  public static final String HBASE_NORMALIZER_ENABLED =
+    "hbase.normalizer.enabled";
+
   /** Cluster is standalone or pseudo-distributed */
   public static final boolean CLUSTER_IS_LOCAL = false;
 

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-common/src/main/resources/hbase-default.xml
----------------------------------------------------------------------
diff --git a/hbase-common/src/main/resources/hbase-default.xml b/hbase-common/src/main/resources/hbase-default.xml
index 57d2927..5d5bb10 100644
--- a/hbase-common/src/main/resources/hbase-default.xml
+++ b/hbase-common/src/main/resources/hbase-default.xml
@@ -583,6 +583,17 @@ possible configurations would overwhelm and obscure the important.
     <description>Period at which the region balancer runs in the Master.</description>
   </property>
   <property>
+    <name>hbase.normalizer.enabled</name>
+    <value>false</value>
+    <description>If set to true, Master will try to keep region size
+      within each table approximately the same.</description>
+  </property>
+  <property>
+    <name>hbase.normalizer.period</name>
+    <value>1800000</value>
+    <description>Period at which the region normalizer runs in the Master.</description>
+  </property>
+  <property>
     <name>hbase.regions.slop</name>
     <value>0.2</value>
     <description>Rebalance if any regionserver has average + (average * slop) regions.</description>
@@ -1418,6 +1429,15 @@ possible configurations would overwhelm and obscure the important.
     </description>
   </property>
   <property>
+    <name>hbase.master.normalizer.class</name>
+    <value>org.apache.hadoop.hbase.master.normalizer.SimpleRegionNormalizer</value>
+    <description>
+      Class used to execute the region normalization when the period occurs.
+      See the class comment for more on how it works
+      http://hbase.apache.org/devapidocs/org/apache/hadoop/hbase/master/normalizer/SimpleRegionNormalizer.html
+    </description>
+  </property>
+  <property>
     <name>hbase.security.exec.permission.checks</name>
     <value>false</value>
     <description>

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
index 86bcdae..84f981f 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
@@ -101,6 +101,9 @@ import org.apache.hadoop.hbase.master.procedure.ModifyTableProcedure;
 import org.apache.hadoop.hbase.master.procedure.ProcedurePrepareLatch;
 import org.apache.hadoop.hbase.master.procedure.ProcedureSyncWait;
 import org.apache.hadoop.hbase.master.procedure.TruncateTableProcedure;
+import org.apache.hadoop.hbase.master.normalizer.RegionNormalizer;
+import org.apache.hadoop.hbase.master.normalizer.RegionNormalizerChore;
+import org.apache.hadoop.hbase.master.normalizer.RegionNormalizerFactory;
 import org.apache.hadoop.hbase.master.snapshot.SnapshotManager;
 import org.apache.hadoop.hbase.monitoring.MemoryBoundedLogMessageBuffer;
 import org.apache.hadoop.hbase.monitoring.MonitoredTask;
@@ -268,7 +271,10 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
   private volatile boolean serverCrashProcessingEnabled = false;
 
   LoadBalancer balancer;
+  RegionNormalizer normalizer;
+  private boolean normalizerEnabled = false;
   private BalancerChore balancerChore;
+  private RegionNormalizerChore normalizerChore;
   private ClusterStatusChore clusterStatusChore;
   private ClusterStatusPublisher clusterStatusPublisherChore = null;
 
@@ -546,6 +552,9 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
   void initializeZKBasedSystemTrackers() throws IOException,
       InterruptedException, KeeperException, CoordinatedStateException {
     this.balancer = LoadBalancerFactory.getLoadBalancer(conf);
+    this.normalizer = RegionNormalizerFactory.getRegionNormalizer(conf);
+    this.normalizer.setMasterServices(this);
+    this.normalizerEnabled = conf.getBoolean(HConstants.HBASE_NORMALIZER_ENABLED, false);
     this.loadBalancerTracker = new LoadBalancerTracker(zooKeeper, this);
     this.loadBalancerTracker.start();
     this.assignmentManager = new AssignmentManager(this, serverManager,
@@ -742,6 +751,8 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
     getChoreService().scheduleChore(clusterStatusChore);
     this.balancerChore = new BalancerChore(this);
     getChoreService().scheduleChore(balancerChore);
+    this.normalizerChore = new RegionNormalizerChore(this);
+    getChoreService().scheduleChore(normalizerChore);
     this.catalogJanitorChore = new CatalogJanitor(this, this);
     getChoreService().scheduleChore(catalogJanitorChore);
 
@@ -1119,6 +1130,9 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
     if (this.balancerChore != null) {
       this.balancerChore.cancel(true);
     }
+    if (this.normalizerChore != null) {
+      this.normalizerChore.cancel(true);
+    }
     if (this.clusterStatusChore != null) {
       this.clusterStatusChore.cancel(true);
     }
@@ -1249,6 +1263,47 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
   }
 
   /**
+   * Perform normalization of cluster (invoked by {@link RegionNormalizerChore}).
+   *
+   * @return true if normalization step was performed successfully, false otherwise
+   *   (specifically, if HMaster hasn't been initialized properly or normalization
+   *   is globally disabled)
+   * @throws IOException
+   */
+  public boolean normalizeRegions() throws IOException {
+    if (!this.initialized) {
+      LOG.debug("Master has not been initialized, don't run region normalizer.");
+      return false;
+    }
+
+    if (!this.normalizerEnabled) {
+      LOG.debug("Region normalization is disabled, don't run region normalizer.");
+      return false;
+    }
+
+    synchronized (this.normalizer) {
+      // Don't run the normalizer concurrently
+      List<TableName> allEnabledTables = new ArrayList<>(
+        this.tableStateManager.getTablesInStates(TableState.State.ENABLED));
+
+      Collections.shuffle(allEnabledTables);
+
+      for(TableName table : allEnabledTables) {
+        if (table.isSystemTable() || !getTableDescriptors().getDescriptor(table).
+            getHTableDescriptor().isNormalizationEnabled()) {
+          LOG.debug("Skipping normalization for table: " + table + ", as it's either system"
+            + " table or doesn't have auto normalization turned on");
+          continue;
+        }
+        this.normalizer.computePlanForTable(table).execute(clusterConnection.getAdmin());
+      }
+    }
+    // If Region did not generate any plans, it means the cluster is already balanced.
+    // Return true indicating a success.
+    return true;
+  }
+
+  /**
    * @return Client info for use as prefix on an audit log string; who did an action
    */
   String getClientIdAuditPrefix() {
@@ -1270,7 +1325,7 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
       final HRegionInfo region_b, final boolean forcible) throws IOException {
     checkInitialized();
     this.service.submit(new DispatchMergingRegionHandler(this,
-        this.catalogJanitorChore, region_a, region_b, forcible));
+      this.catalogJanitorChore, region_a, region_b, forcible));
   }
 
   void move(final byte[] encodedRegionName,
@@ -1524,7 +1579,7 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
           HConstants.DEFAULT_ZK_SESSION_TIMEOUT);
         // If we're a backup master, stall until a primary to writes his address
         if (conf.getBoolean(HConstants.MASTER_TYPE_BACKUP,
-            HConstants.DEFAULT_MASTER_TYPE_BACKUP)) {
+          HConstants.DEFAULT_MASTER_TYPE_BACKUP)) {
           LOG.debug("HMaster started in backup mode. "
             + "Stalling until master znode is written.");
           // This will only be a minute or so while the cluster starts up,
@@ -1546,12 +1601,12 @@ public class HMaster extends HRegionServer implements MasterServices, Server {
           LOG.fatal("Failed to become active master", t);
           // HBASE-5680: Likely hadoop23 vs hadoop 20.x/1.x incompatibility
           if (t instanceof NoClassDefFoundError &&
-              t.getMessage()
-                  .contains("org/apache/hadoop/hdfs/protocol/HdfsConstants$SafeModeAction")) {
+            t.getMessage()
+              .contains("org/apache/hadoop/hdfs/protocol/HdfsConstants$SafeModeAction")) {
             // improved error message for this special case
             abort("HBase is having a problem with its Hadoop jars.  You may need to "
               + "recompile HBase against Hadoop version "
-              +  org.apache.hadoop.util.VersionInfo.getVersion()
+              + org.apache.hadoop.util.VersionInfo.getVersion()
               + " or change your hadoop jars to start properly", t);
           } else {
             abort("Unhandled exception. Starting shutdown.", t);

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/EmptyNormalizationPlan.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/EmptyNormalizationPlan.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/EmptyNormalizationPlan.java
new file mode 100644
index 0000000..a36dd07
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/EmptyNormalizationPlan.java
@@ -0,0 +1,48 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+
+/**
+ * Plan which signifies that no normalization is required,
+ * or normalization of this table isn't allowed, this is singleton.
+ */
+@InterfaceAudience.Private
+public final class EmptyNormalizationPlan implements NormalizationPlan {
+  private static final EmptyNormalizationPlan instance = new EmptyNormalizationPlan();
+
+  private EmptyNormalizationPlan() {
+  }
+
+  /**
+   * @return singleton instance
+   */
+  public static EmptyNormalizationPlan getInstance(){
+    return instance;
+  }
+
+  /**
+   * No-op for empty plan.
+   */
+  @Override
+  public void execute(Admin admin) {
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/MergeNormalizationPlan.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/MergeNormalizationPlan.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/MergeNormalizationPlan.java
new file mode 100644
index 0000000..08a58a5
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/MergeNormalizationPlan.java
@@ -0,0 +1,73 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+
+import java.io.IOException;
+
+/**
+ * Normalization plan to merge regions (smallest region in the table with its smallest neighbor).
+ */
+@InterfaceAudience.Private
+public class MergeNormalizationPlan implements NormalizationPlan {
+  private static final Log LOG = LogFactory.getLog(MergeNormalizationPlan.class.getName());
+
+  private final HRegionInfo firstRegion;
+  private final HRegionInfo secondRegion;
+
+  public MergeNormalizationPlan(HRegionInfo firstRegion, HRegionInfo secondRegion) {
+    this.firstRegion = firstRegion;
+    this.secondRegion = secondRegion;
+  }
+
+  HRegionInfo getFirstRegion() {
+    return firstRegion;
+  }
+
+  HRegionInfo getSecondRegion() {
+    return secondRegion;
+  }
+
+  @Override
+  public String toString() {
+    return "MergeNormalizationPlan{" +
+      "firstRegion=" + firstRegion +
+      ", secondRegion=" + secondRegion +
+      '}';
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void execute(Admin admin) {
+    LOG.info("Executing merging normalization plan: " + this);
+    try {
+      admin.mergeRegions(firstRegion.getEncodedNameAsBytes(),
+        secondRegion.getEncodedNameAsBytes(), true);
+    } catch (IOException ex) {
+      LOG.error("Error during region merge: ", ex);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/NormalizationPlan.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/NormalizationPlan.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/NormalizationPlan.java
new file mode 100644
index 0000000..96eed8c
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/NormalizationPlan.java
@@ -0,0 +1,35 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+
+/**
+ * Interface for normalization plan.
+ */
+@InterfaceAudience.Private
+public interface NormalizationPlan {
+
+  /**
+   * Executes normalization plan on cluster (does actual splitting/merging work).
+   * @param admin instance of Admin
+   */
+  void execute(Admin admin);
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizer.java
new file mode 100644
index 0000000..19abcf2
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizer.java
@@ -0,0 +1,51 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.hadoop.hbase.HBaseIOException;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.master.MasterServices;
+
+/**
+ * Performs "normalization" of regions on the cluster, making sure that suboptimal
+ * choice of split keys doesn't leave cluster in a situation when some regions are
+ * substantially larger than others for considerable amount of time.
+ *
+ * Users who want to use this feature could either use default {@link SimpleRegionNormalizer}
+ * or plug in their own implementation. Please note that overly aggressive normalization rules
+ * (attempting to make all regions perfectly equal in size) could potentially lead to
+ * "split/merge storms".
+ */
+@InterfaceAudience.Private
+public interface RegionNormalizer {
+  /**
+   * Set the master service. Must be called before first call to
+   * {@link #computePlanForTable(TableName)}.
+   * @param masterServices master services to use
+   */
+  void setMasterServices(MasterServices masterServices);
+
+  /**
+   * Computes next optimal normalization plan.
+   * @param table table to normalize
+   * @return Next (perhaps most urgent) normalization action to perform
+   */
+  NormalizationPlan computePlanForTable(TableName table) throws HBaseIOException;
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerChore.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerChore.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerChore.java
new file mode 100644
index 0000000..25118c7
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerChore.java
@@ -0,0 +1,53 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.ScheduledChore;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.master.HMaster;
+
+import java.io.IOException;
+
+/**
+ * Chore that will call {@link org.apache.hadoop.hbase.master.HMaster#normalizeRegions()}
+ * when needed.
+ */
+@InterfaceAudience.Private
+public class RegionNormalizerChore extends ScheduledChore {
+  private static final Log LOG = LogFactory.getLog(RegionNormalizerChore.class);
+
+  private final HMaster master;
+
+  public RegionNormalizerChore(HMaster master) {
+    super(master.getServerName() + "-RegionNormalizerChore", master,
+      master.getConfiguration().getInt("hbase.normalizer.period", 1800000));
+    this.master = master;
+  }
+
+  @Override
+  protected void chore() {
+    try {
+      master.normalizeRegions();
+    } catch (IOException e) {
+      LOG.error("Failed to normalize regions.", e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerFactory.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerFactory.java
new file mode 100644
index 0000000..6df0c77
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/RegionNormalizerFactory.java
@@ -0,0 +1,48 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.util.ReflectionUtils;
+
+/**
+ * Factory to create instance of {@link RegionNormalizer} as configured.
+ */
+@InterfaceAudience.Private
+public final class RegionNormalizerFactory {
+
+  private RegionNormalizerFactory() {
+  }
+
+  /**
+   * Create a region normalizer from the given conf.
+   * @param conf configuration
+   * @return {@link RegionNormalizer} implementation
+   */
+  public static RegionNormalizer getRegionNormalizer(Configuration conf) {
+
+    // Create instance of Region Normalizer
+    Class<? extends RegionNormalizer> balancerKlass =
+      conf.getClass(HConstants.HBASE_MASTER_NORMALIZER_CLASS, SimpleRegionNormalizer.class,
+        RegionNormalizer.class);
+    return ReflectionUtils.newInstance(balancerKlass, conf);
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SimpleRegionNormalizer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SimpleRegionNormalizer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SimpleRegionNormalizer.java
new file mode 100644
index 0000000..bf58691
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SimpleRegionNormalizer.java
@@ -0,0 +1,176 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HBaseIOException;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.RegionLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.master.MasterServices;
+import org.apache.hadoop.hbase.util.Pair;
+
+import java.util.List;
+
+/**
+ * Simple implementation of region normalizer.
+ *
+ * Logic in use:
+ *
+ *  - get all regions of a given table
+ *  - get avg size S of each region (by total size of store files reported in RegionLoad)
+ *  - If biggest region is bigger than S * 2, it is kindly requested to split,
+ *    and normalization stops
+ *  - Otherwise, two smallest region R1 and its smallest neighbor R2 are kindly requested
+ *    to merge, if R1 + R1 <  S, and normalization stops
+ *  - Otherwise, no action is performed
+ */
+@InterfaceAudience.Private
+public class SimpleRegionNormalizer implements RegionNormalizer {
+  private static final Log LOG = LogFactory.getLog(SimpleRegionNormalizer.class);
+  private MasterServices masterServices;
+
+  /**
+   * Set the master service.
+   * @param masterServices inject instance of MasterServices
+   */
+  @Override
+  public void setMasterServices(MasterServices masterServices) {
+    this.masterServices = masterServices;
+  }
+
+  /**
+   * Computes next most "urgent" normalization action on the table.
+   * Action may be either a split, or a merge, or no action.
+   *
+   * @param table table to normalize
+   * @return normalization plan to execute
+   */
+  @Override
+  public NormalizationPlan computePlanForTable(TableName table)
+      throws HBaseIOException {
+    if (table == null || table.isSystemTable()) {
+      LOG.debug("Normalization of table " + table + " isn't allowed");
+      return EmptyNormalizationPlan.getInstance();
+    }
+
+    List<HRegionInfo> tableRegions = masterServices.getAssignmentManager().getRegionStates().
+      getRegionsOfTable(table);
+
+    //TODO: should we make min number of regions a config param?
+    if (tableRegions == null || tableRegions.size() < 3) {
+      LOG.debug("Table " + table + " has " + tableRegions.size() + " regions, required min number"
+        + " of regions for normalizer to run is 3, not running normalizer");
+      return EmptyNormalizationPlan.getInstance();
+    }
+
+    LOG.debug("Computing normalization plan for table: " + table +
+      ", number of regions: " + tableRegions.size());
+
+    long totalSizeMb = 0;
+    Pair<HRegionInfo, Long> largestRegion = new Pair<>();
+
+    // A is a smallest region, B is it's smallest neighbor
+    Pair<HRegionInfo, Long> smallestRegion = new Pair<>();
+    Pair<HRegionInfo, Long> smallestNeighborOfSmallestRegion;
+    int smallestRegionIndex = 0;
+
+    for (int i = 0; i < tableRegions.size(); i++) {
+      HRegionInfo hri = tableRegions.get(i);
+      long regionSize = getRegionSize(hri);
+      totalSizeMb += regionSize;
+
+      if (largestRegion.getFirst() == null || regionSize > largestRegion.getSecond()) {
+        largestRegion.setFirst(hri);
+        largestRegion.setSecond(regionSize);
+      }
+
+      if (smallestRegion.getFirst() == null || regionSize < smallestRegion.getSecond()) {
+        smallestRegion.setFirst(hri);
+        smallestRegion.setSecond(regionSize);
+        smallestRegionIndex = i;
+      }
+    }
+
+    // now get smallest neighbor of smallest region
+    long leftNeighborSize = -1;
+    long rightNeighborSize = -1;
+
+    if (smallestRegionIndex > 0) {
+      leftNeighborSize = getRegionSize(tableRegions.get(smallestRegionIndex - 1));
+    }
+
+    if (smallestRegionIndex < tableRegions.size() - 1) {
+      rightNeighborSize = getRegionSize(tableRegions.get(smallestRegionIndex + 1));
+    }
+
+    if (leftNeighborSize == -1) {
+      smallestNeighborOfSmallestRegion =
+        new Pair<>(tableRegions.get(smallestRegionIndex + 1), rightNeighborSize);
+    } else if (rightNeighborSize == -1) {
+      smallestNeighborOfSmallestRegion =
+        new Pair<>(tableRegions.get(smallestRegionIndex - 1), leftNeighborSize);
+    } else {
+      if (leftNeighborSize < rightNeighborSize) {
+        smallestNeighborOfSmallestRegion =
+          new Pair<>(tableRegions.get(smallestRegionIndex - 1), leftNeighborSize);
+      } else {
+        smallestNeighborOfSmallestRegion =
+          new Pair<>(tableRegions.get(smallestRegionIndex + 1), rightNeighborSize);
+      }
+    }
+
+    double avgRegionSize = totalSizeMb / (double) tableRegions.size();
+
+    LOG.debug("Table " + table + ", total aggregated regions size: " + totalSizeMb);
+    LOG.debug("Table " + table + ", average region size: " + avgRegionSize);
+
+    // now; if the largest region is >2 times large than average, we split it, split
+    // is more high priority normalization action than merge.
+    if (largestRegion.getSecond() > 2 * avgRegionSize) {
+      LOG.debug("Table " + table + ", largest region "
+        + largestRegion.getFirst().getRegionName() + " has size "
+        + largestRegion.getSecond() + ", more than 2 times than avg size, splitting");
+      return new SplitNormalizationPlan(largestRegion.getFirst(), null);
+    } else {
+      if ((smallestRegion.getSecond() + smallestNeighborOfSmallestRegion.getSecond()
+          < avgRegionSize)) {
+        LOG.debug("Table " + table + ", smallest region size: " + smallestRegion.getSecond()
+          + " and its smallest neighbor size: " + smallestNeighborOfSmallestRegion.getSecond()
+          + ", less than half the avg size, merging them");
+        return new MergeNormalizationPlan(smallestRegion.getFirst(),
+          smallestNeighborOfSmallestRegion.getFirst());
+      } else {
+        LOG.debug("No normalization needed, regions look good for table: " + table);
+        return EmptyNormalizationPlan.getInstance();
+      }
+    }
+  }
+
+  private long getRegionSize(HRegionInfo hri) {
+    ServerName sn = masterServices.getAssignmentManager().getRegionStates().
+      getRegionServerOfRegion(hri);
+    RegionLoad regionLoad = masterServices.getServerManager().getLoad(sn).
+      getRegionsLoad().get(hri.getRegionName());
+    return regionLoad.getStorefileSizeMB();
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SplitNormalizationPlan.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SplitNormalizationPlan.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SplitNormalizationPlan.java
new file mode 100644
index 0000000..c96988a
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/normalizer/SplitNormalizationPlan.java
@@ -0,0 +1,81 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Normalization plan to split region.
+ */
+@InterfaceAudience.Private
+public class SplitNormalizationPlan implements NormalizationPlan {
+  private static final Log LOG = LogFactory.getLog(SplitNormalizationPlan.class.getName());
+
+  private HRegionInfo regionInfo;
+  private byte[] splitPoint;
+
+  public SplitNormalizationPlan(HRegionInfo regionInfo, byte[] splitPoint) {
+    this.regionInfo = regionInfo;
+    this.splitPoint = splitPoint;
+  }
+
+  public HRegionInfo getRegionInfo() {
+    return regionInfo;
+  }
+
+  public void setRegionInfo(HRegionInfo regionInfo) {
+    this.regionInfo = regionInfo;
+  }
+
+  public byte[] getSplitPoint() {
+    return splitPoint;
+  }
+
+  public void setSplitPoint(byte[] splitPoint) {
+    this.splitPoint = splitPoint;
+  }
+
+  @Override
+  public String toString() {
+    return "SplitNormalizationPlan{" +
+      "regionInfo=" + regionInfo +
+      ", splitPoint=" + Arrays.toString(splitPoint) +
+      '}';
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void execute(Admin admin) {
+    LOG.info("Executing splitting normalization plan: " + this);
+    try {
+      admin.splitRegion(regionInfo.getRegionName());
+    } catch (IOException ex) {
+      LOG.error("Error during region split: ", ex);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizer.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizer.java
new file mode 100644
index 0000000..211911e
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizer.java
@@ -0,0 +1,240 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HBaseIOException;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.RegionLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.master.MasterServices;
+import org.apache.hadoop.hbase.testclassification.MasterTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests logic of {@link SimpleRegionNormalizer}.
+ */
+@Category({MasterTests.class, SmallTests.class})
+public class TestSimpleRegionNormalizer {
+  private static final Log LOG = LogFactory.getLog(TestSimpleRegionNormalizer.class);
+
+  private static RegionNormalizer normalizer;
+
+  // mocks
+  private static MasterServices masterServices;
+
+  @BeforeClass
+  public static void beforeAllTests() throws Exception {
+    normalizer = new SimpleRegionNormalizer();
+  }
+
+  @Test
+  public void testNoNormalizationForMetaTable() throws HBaseIOException {
+    TableName testTable = TableName.META_TABLE_NAME;
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+    assertTrue(plan instanceof EmptyNormalizationPlan);
+  }
+
+  @Test
+  public void testNoNormalizationIfTooFewRegions() throws HBaseIOException {
+    TableName testTable = TableName.valueOf("testSplitOfSmallRegion");
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    HRegionInfo hri1 = new HRegionInfo(testTable, Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
+    hris.add(hri1);
+    regionSizes.put(hri1.getRegionName(), 10);
+
+    HRegionInfo hri2 = new HRegionInfo(testTable, Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
+    hris.add(hri2);
+    regionSizes.put(hri2.getRegionName(), 15);
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+    assertTrue((plan instanceof EmptyNormalizationPlan));
+  }
+
+  @Test
+  public void testNoNormalizationOnNormalizedCluster() throws HBaseIOException {
+    TableName testTable = TableName.valueOf("testSplitOfSmallRegion");
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    HRegionInfo hri1 = new HRegionInfo(testTable, Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
+    hris.add(hri1);
+    regionSizes.put(hri1.getRegionName(), 10);
+
+    HRegionInfo hri2 = new HRegionInfo(testTable, Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
+    hris.add(hri2);
+    regionSizes.put(hri2.getRegionName(), 15);
+
+    HRegionInfo hri3 = new HRegionInfo(testTable, Bytes.toBytes("ccc"), Bytes.toBytes("ddd"));
+    hris.add(hri3);
+    regionSizes.put(hri3.getRegionName(), 8);
+
+    HRegionInfo hri4 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri4.getRegionName(), 10);
+
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+    assertTrue(plan instanceof EmptyNormalizationPlan);
+  }
+
+  @Test
+  public void testMergeOfSmallRegions() throws HBaseIOException {
+    TableName testTable = TableName.valueOf("testMergeOfSmallRegions");
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    HRegionInfo hri1 = new HRegionInfo(testTable, Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
+    hris.add(hri1);
+    regionSizes.put(hri1.getRegionName(), 15);
+
+    HRegionInfo hri2 = new HRegionInfo(testTable, Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
+    hris.add(hri2);
+    regionSizes.put(hri2.getRegionName(), 5);
+
+    HRegionInfo hri3 = new HRegionInfo(testTable, Bytes.toBytes("ccc"), Bytes.toBytes("ddd"));
+    hris.add(hri3);
+    regionSizes.put(hri3.getRegionName(), 5);
+
+    HRegionInfo hri4 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri4.getRegionName(), 15);
+
+    HRegionInfo hri5 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri5.getRegionName(), 16);
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+
+    assertTrue(plan instanceof MergeNormalizationPlan);
+    assertEquals(hri2, ((MergeNormalizationPlan) plan).getFirstRegion());
+    assertEquals(hri3, ((MergeNormalizationPlan) plan).getSecondRegion());
+  }
+
+  @Test
+  public void testMergeOfSmallNonAdjacentRegions() throws HBaseIOException {
+    TableName testTable = TableName.valueOf("testMergeOfSmallRegions");
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    HRegionInfo hri1 = new HRegionInfo(testTable, Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
+    hris.add(hri1);
+    regionSizes.put(hri1.getRegionName(), 15);
+
+    HRegionInfo hri2 = new HRegionInfo(testTable, Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
+    hris.add(hri2);
+    regionSizes.put(hri2.getRegionName(), 5);
+
+    HRegionInfo hri3 = new HRegionInfo(testTable, Bytes.toBytes("ccc"), Bytes.toBytes("ddd"));
+    hris.add(hri3);
+    regionSizes.put(hri3.getRegionName(), 16);
+
+    HRegionInfo hri4 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri4.getRegionName(), 15);
+
+    HRegionInfo hri5 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri5.getRegionName(), 5);
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+
+    assertTrue(plan instanceof EmptyNormalizationPlan);
+  }
+
+  @Test
+  public void testSplitOfLargeRegion() throws HBaseIOException {
+    TableName testTable = TableName.valueOf("testSplitOfLargeRegion");
+    List<HRegionInfo> hris = new ArrayList<>();
+    Map<byte[], Integer> regionSizes = new HashMap<>();
+
+    HRegionInfo hri1 = new HRegionInfo(testTable, Bytes.toBytes("aaa"), Bytes.toBytes("bbb"));
+    hris.add(hri1);
+    regionSizes.put(hri1.getRegionName(), 8);
+
+    HRegionInfo hri2 = new HRegionInfo(testTable, Bytes.toBytes("bbb"), Bytes.toBytes("ccc"));
+    hris.add(hri2);
+    regionSizes.put(hri2.getRegionName(), 6);
+
+    HRegionInfo hri3 = new HRegionInfo(testTable, Bytes.toBytes("ccc"), Bytes.toBytes("ddd"));
+    hris.add(hri3);
+    regionSizes.put(hri3.getRegionName(), 10);
+
+    HRegionInfo hri4 = new HRegionInfo(testTable, Bytes.toBytes("ddd"), Bytes.toBytes("eee"));
+    hris.add(hri4);
+    regionSizes.put(hri4.getRegionName(), 30);
+
+    setupMocksForNormalizer(regionSizes, hris);
+    NormalizationPlan plan = normalizer.computePlanForTable(testTable);
+
+    assertTrue(plan instanceof SplitNormalizationPlan);
+    assertEquals(hri4, ((SplitNormalizationPlan) plan).getRegionInfo());
+  }
+
+  protected void setupMocksForNormalizer(Map<byte[], Integer> regionSizes,
+                                         List<HRegionInfo> hris) {
+    masterServices = Mockito.mock(MasterServices.class, RETURNS_DEEP_STUBS);
+
+    // for simplicity all regions are assumed to be on one server; doesn't matter to us
+    ServerName sn = ServerName.valueOf("localhost", -1, 1L);
+    when(masterServices.getAssignmentManager().getRegionStates().
+      getRegionsOfTable(any(TableName.class))).thenReturn(hris);
+    when(masterServices.getAssignmentManager().getRegionStates().
+      getRegionServerOfRegion(any(HRegionInfo.class))).thenReturn(sn);
+
+    for (Map.Entry<byte[], Integer> region : regionSizes.entrySet()) {
+      RegionLoad regionLoad = Mockito.mock(RegionLoad.class);
+      when(regionLoad.getName()).thenReturn(region.getKey());
+      when(regionLoad.getStorefileSizeMB()).thenReturn(region.getValue());
+
+      when(masterServices.getServerManager().getLoad(sn).
+        getRegionsLoad().get(region.getKey())).thenReturn(regionLoad);
+    }
+
+    normalizer.setMasterServices(masterServices);
+  }
+}

http://git-wip-us.apache.org/repos/asf/hbase/blob/fd37ccb6/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizerOnCluster.java
----------------------------------------------------------------------
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizerOnCluster.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizerOnCluster.java
new file mode 100644
index 0000000..2cf26c0
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/normalizer/TestSimpleRegionNormalizerOnCluster.java
@@ -0,0 +1,218 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.master.normalizer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.HTableDescriptor;
+import org.apache.hadoop.hbase.MetaTableAccessor;
+import org.apache.hadoop.hbase.MiniHBaseCluster;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.HTable;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.regionserver.Region;
+import org.apache.hadoop.hbase.testclassification.MasterTests;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.test.LoadTestKVGenerator;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Testing {@link SimpleRegionNormalizer} on minicluster.
+ */
+@Category({MasterTests.class, MediumTests.class})
+public class TestSimpleRegionNormalizerOnCluster {
+  private static final Log LOG = LogFactory.getLog(TestSimpleRegionNormalizerOnCluster.class);
+  private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
+  private static final byte[] FAMILYNAME = Bytes.toBytes("fam");
+  private static Admin admin;
+
+  @BeforeClass
+  public static void beforeAllTests() throws Exception {
+    // we will retry operations when PleaseHoldException is thrown
+    TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 3);
+    TEST_UTIL.getConfiguration().setBoolean(HConstants.HBASE_NORMALIZER_ENABLED, true);
+
+    // Start a cluster of two regionservers.
+    TEST_UTIL.startMiniCluster(1);
+    admin = TEST_UTIL.getHBaseAdmin();
+  }
+
+  @AfterClass
+  public static void afterAllTests() throws Exception {
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  @Test(timeout = 60000)
+  @SuppressWarnings("deprecation")
+  public void testRegionNormalizationSplitOnCluster() throws Exception {
+    final TableName TABLENAME =
+      TableName.valueOf("testRegionNormalizationSplitOnCluster");
+    MiniHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
+    HMaster m = cluster.getMaster();
+
+    try (HTable ht = TEST_UTIL.createMultiRegionTable(TABLENAME, FAMILYNAME, 5)) {
+      // Need to get sorted list of regions here
+      List<HRegion> generatedRegions = TEST_UTIL.getHBaseCluster().getRegions(TABLENAME);
+      Collections.sort(generatedRegions, new Comparator<HRegion>() {
+        @Override
+        public int compare(HRegion o1, HRegion o2) {
+          return o1.getRegionInfo().compareTo(o2.getRegionInfo());
+        }
+      });
+
+      HRegion region = generatedRegions.get(0);
+      generateTestData(region, 1);
+      region.flush(true);
+
+      region = generatedRegions.get(1);
+      generateTestData(region, 1);
+      region.flush(true);
+
+      region = generatedRegions.get(2);
+      generateTestData(region, 2);
+      region.flush(true);
+
+      region = generatedRegions.get(3);
+      generateTestData(region, 2);
+      region.flush(true);
+
+      region = generatedRegions.get(4);
+      generateTestData(region, 5);
+      region.flush(true);
+    }
+
+    HTableDescriptor htd = admin.getTableDescriptor(TABLENAME);
+    htd.setNormalizationEnabled(true);
+    admin.modifyTable(TABLENAME, htd);
+
+    admin.flush(TABLENAME);
+
+    System.out.println(admin.getTableDescriptor(TABLENAME));
+
+    assertEquals(5, MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME));
+
+    // Now trigger a split and stop when the split is in progress
+    Thread.sleep(5000); // to let region load to update
+    m.normalizeRegions();
+
+    while (MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME) < 6) {
+      LOG.info("Waiting for normalization split to complete");
+      Thread.sleep(100);
+    }
+
+    assertEquals(6, MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME));
+
+    admin.disableTable(TABLENAME);
+    admin.deleteTable(TABLENAME);
+  }
+
+  @Test(timeout = 60000)
+  @SuppressWarnings("deprecation")
+  public void testRegionNormalizationMergeOnCluster() throws Exception {
+    final TableName TABLENAME =
+      TableName.valueOf("testRegionNormalizationMergeOnCluster");
+    MiniHBaseCluster cluster = TEST_UTIL.getHBaseCluster();
+    HMaster m = cluster.getMaster();
+
+    // create 5 regions with sizes to trigger merge of small regions
+    try (HTable ht = TEST_UTIL.createMultiRegionTable(TABLENAME, FAMILYNAME, 5)) {
+      // Need to get sorted list of regions here
+      List<HRegion> generatedRegions = TEST_UTIL.getHBaseCluster().getRegions(TABLENAME);
+      Collections.sort(generatedRegions, new Comparator<HRegion>() {
+        @Override
+        public int compare(HRegion o1, HRegion o2) {
+          return o1.getRegionInfo().compareTo(o2.getRegionInfo());
+        }
+      });
+
+      HRegion region = generatedRegions.get(0);
+      generateTestData(region, 1);
+      region.flush(true);
+
+      region = generatedRegions.get(1);
+      generateTestData(region, 1);
+      region.flush(true);
+
+      region = generatedRegions.get(2);
+      generateTestData(region, 3);
+      region.flush(true);
+
+      region = generatedRegions.get(3);
+      generateTestData(region, 3);
+      region.flush(true);
+
+      region = generatedRegions.get(4);
+      generateTestData(region, 5);
+      region.flush(true);
+    }
+
+    HTableDescriptor htd = admin.getTableDescriptor(TABLENAME);
+    htd.setNormalizationEnabled(true);
+    admin.modifyTable(TABLENAME, htd);
+
+    admin.flush(TABLENAME);
+
+    assertEquals(5, MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME));
+
+    // Now trigger a merge and stop when the merge is in progress
+    Thread.sleep(5000); // to let region load to update
+    m.normalizeRegions();
+
+    while (MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME) > 4) {
+      LOG.info("Waiting for normalization merge to complete");
+      Thread.sleep(100);
+    }
+
+    assertEquals(4, MetaTableAccessor.getRegionCount(TEST_UTIL.getConnection(), TABLENAME));
+
+    admin.disableTable(TABLENAME);
+    admin.deleteTable(TABLENAME);
+  }
+
+  private void generateTestData(Region region, int numRows) throws IOException {
+    // generating 1Mb values
+    LoadTestKVGenerator dataGenerator = new LoadTestKVGenerator(1024 * 1024, 1024 * 1024);
+    for (int i = 0; i < numRows; ++i) {
+      byte[] key = Bytes.add(region.getRegionInfo().getStartKey(), Bytes.toBytes(i));
+      for (int j = 0; j < 1; ++j) {
+        Put put = new Put(key);
+        byte[] col = Bytes.toBytes(String.valueOf(j));
+        byte[] value = dataGenerator.generateRandomSizeValue(key, col);
+        put.add(FAMILYNAME, col, value);
+        region.put(put);
+      }
+    }
+  }
+}