001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
003 * agreements. See the NOTICE file distributed with this work for additional information regarding
004 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
005 * "License"); you may not use this file except in compliance with the License. You may obtain a
006 * copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software distributed under the License
011 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
012 * or implied. See the License for the specific language governing permissions and limitations under
013 * the License.
014 */
015package org.apache.hadoop.hbase.master.balancer;
016
017import static junit.framework.TestCase.assertNotNull;
018import static junit.framework.TestCase.assertTrue;
019import static org.junit.Assert.assertNull;
020import java.io.IOException;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Queue;
027import java.util.Random;
028import java.util.TreeMap;
029import java.util.concurrent.ThreadLocalRandom;
030
031import org.apache.hadoop.fs.FileSystem;
032import org.apache.hadoop.hbase.HBaseClassTestRule;
033import org.apache.hadoop.hbase.HBaseTestingUtility;
034import org.apache.hadoop.hbase.HConstants;
035import org.apache.hadoop.hbase.ServerName;
036import org.apache.hadoop.hbase.client.RegionInfo;
037import org.apache.hadoop.hbase.client.RegionReplicaUtil;
038import org.apache.hadoop.hbase.master.RackManager;
039import org.apache.hadoop.hbase.master.RegionPlan;
040import org.apache.hadoop.hbase.testclassification.MasterTests;
041import org.apache.hadoop.hbase.testclassification.MediumTests;
042import org.junit.BeforeClass;
043import org.junit.ClassRule;
044import org.junit.Test;
045import org.junit.experimental.categories.Category;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049@Category({ MasterTests.class, MediumTests.class })
050public class TestStochasticLoadBalancerHeterogeneousCost extends BalancerTestBase {
051  @ClassRule
052  public static final HBaseClassTestRule CLASS_RULE =
053      HBaseClassTestRule.forClass(TestStochasticLoadBalancerHeterogeneousCost.class);
054
055  private static final Logger LOG =
056      LoggerFactory.getLogger(TestStochasticLoadBalancerHeterogeneousCost.class);
057  private static final double ALLOWED_WINDOW = 1.20;
058  private static final HBaseTestingUtility HTU = new HBaseTestingUtility();
059  private static String RULES_FILE;
060
061  @BeforeClass
062  public static void beforeAllTests() throws IOException {
063    BalancerTestBase.conf = HTU.getConfiguration();
064    BalancerTestBase.conf.setFloat("hbase.master.balancer.stochastic.regionCountCost", 0);
065    BalancerTestBase.conf.setFloat("hbase.master.balancer.stochastic.primaryRegionCountCost", 0);
066    BalancerTestBase.conf.setFloat("hbase.master.balancer.stochastic.tableSkewCost", 0);
067    BalancerTestBase.conf.setBoolean("hbase.master.balancer.stochastic.runMaxSteps", true);
068    BalancerTestBase.conf.set(StochasticLoadBalancer.COST_FUNCTIONS_COST_FUNCTIONS_KEY,
069      HeterogeneousRegionCountCostFunction.class.getName());
070    // Need to ensure test dir has been created.
071    assertTrue(FileSystem.get(HTU.getConfiguration()).mkdirs(HTU.getDataTestDir()));
072    RULES_FILE = HTU.getDataTestDir(
073      TestStochasticLoadBalancerHeterogeneousCostRules.DEFAULT_RULES_FILE_NAME).toString();
074    BalancerTestBase.conf.set(
075      HeterogeneousRegionCountCostFunction.HBASE_MASTER_BALANCER_HETEROGENEOUS_RULES_FILE,
076      RULES_FILE);
077    BalancerTestBase.loadBalancer = new StochasticLoadBalancer();
078    BalancerTestBase.loadBalancer.setConf(BalancerTestBase.conf);
079  }
080
081  @Test
082  public void testDefault() throws IOException {
083    final List<String> rules = Collections.emptyList();
084
085    final int numNodes = 2;
086    final int numRegions = 300;
087    final int numRegionsPerServer = 250;
088
089    // Initial state: { rs1:50 , rs0:250 }
090    // Cluster can hold 300/400 regions (75%)
091    // Expected balanced Cluster: { rs0:150 , rs1:150 }
092    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
093  }
094
095  @Test
096  public void testOneGroup() throws IOException {
097    final List<String> rules = Collections.singletonList("rs.* 100");
098
099    final int numNodes = 4;
100    final int numRegions = 300;
101    final int numRegionsPerServer = 30;
102
103    // Initial state: { rs0:30 , rs1:30 , rs2:30 , rs3:210 }.
104    // The cluster can hold 300/400 regions (75%)
105    // Expected balanced Cluster: { rs0:75 , rs1:75 , rs2:75 , rs3:75 }
106    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
107  }
108
109  @Test
110  public void testTwoGroups() throws IOException {
111    final List<String> rules = Arrays.asList("rs[0-4] 200", "rs[5-9] 50");
112
113    final int numNodes = 10;
114    final int numRegions = 500;
115    final int numRegionsPerServer = 50;
116
117    // Initial state: { rs0:50 , rs1:50 , rs2:50 , rs3:50 , rs4:50 , rs5:50 , rs6:50 , rs7:50 ,
118    // rs8:50 , rs9:50 }
119    // the cluster can hold 500/1250 regions (40%)
120    // Expected balanced Cluster: { rs5:20 , rs6:20 , rs7:20 , rs8:20 , rs9:20 , rs0:80 , rs1:80 ,
121    // rs2:80 , rs3:80 , rs4:80 }
122    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
123  }
124
125  @Test
126  public void testFourGroups() throws IOException {
127    final List<String> rules = Arrays.asList("rs[1-3] 200", "rs[4-7] 250", "rs[8-9] 100");
128
129    final int numNodes = 10;
130    final int numRegions = 800;
131    final int numRegionsPerServer = 80;
132
133    // Initial state: { rs0:80 , rs1:80 , rs2:80 , rs3:80 , rs4:80 , rs5:80 , rs6:80 , rs7:80 ,
134    // rs8:80 , rs9:80 }
135    // Cluster can hold 800/2000 regions (40%)
136    // Expected balanced Cluster: { rs8:40 , rs9:40 , rs2:80 , rs3:80 , rs1:82 , rs0:94 , rs4:96 ,
137    // rs5:96 , rs6:96 , rs7:96 }
138    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
139  }
140
141  @Test
142  public void testOverloaded() throws IOException {
143    final List<String> rules = Collections.singletonList("rs[0-1] 50");
144
145    final int numNodes = 2;
146    final int numRegions = 120;
147    final int numRegionsPerServer = 60;
148
149    TestStochasticLoadBalancerHeterogeneousCostRules.createRulesFile(RULES_FILE);
150    final Map<ServerName, List<RegionInfo>> serverMap =
151        this.createServerMap(numNodes, numRegions, numRegionsPerServer, 1, 1);
152    final List<RegionPlan> plans =
153        BalancerTestBase.loadBalancer.balanceTable(HConstants.ENSEMBLE_TABLE_NAME, serverMap);
154    // As we disabled all the other cost functions, balancing only according to
155    // the heterogeneous cost function should return nothing.
156    assertNull(plans);
157  }
158
159  private void testHeterogeneousWithCluster(final int numNodes, final int numRegions,
160      final int numRegionsPerServer, final List<String> rules) throws IOException {
161
162    TestStochasticLoadBalancerHeterogeneousCostRules.createRulesFile(RULES_FILE, rules);
163    final Map<ServerName, List<RegionInfo>> serverMap =
164        this.createServerMap(numNodes, numRegions, numRegionsPerServer, 1, 1);
165    this.testWithCluster(serverMap, null, true, false);
166  }
167
168  protected void testWithCluster(final Map<ServerName, List<RegionInfo>> serverMap,
169      final RackManager rackManager, final boolean assertFullyBalanced,
170      final boolean assertFullyBalancedForReplicas) {
171    final List<ServerAndLoad> list = this.convertToList(serverMap);
172    LOG.info("Mock Cluster : " + this.printMock(list) + " " + this.printStats(list));
173
174    BalancerTestBase.loadBalancer.setRackManager(rackManager);
175
176    // Run the balancer.
177    final List<RegionPlan> plans =
178        BalancerTestBase.loadBalancer.balanceTable(HConstants.ENSEMBLE_TABLE_NAME, serverMap);
179    assertNotNull(plans);
180
181    // Check to see that this actually got to a stable place.
182    if (assertFullyBalanced || assertFullyBalancedForReplicas) {
183      // Apply the plan to the mock cluster.
184      final List<ServerAndLoad> balancedCluster = this.reconcile(list, plans, serverMap);
185
186      // Print out the cluster loads to make debugging easier.
187      LOG.info("Mock Balanced cluster : " + this.printMock(balancedCluster));
188
189      if (assertFullyBalanced) {
190        final List<RegionPlan> secondPlans =
191            BalancerTestBase.loadBalancer.balanceTable(HConstants.ENSEMBLE_TABLE_NAME, serverMap);
192        assertNull(secondPlans);
193
194        // create external cost function to retrieve limit
195        // for each RS
196        final HeterogeneousRegionCountCostFunction cf =
197            new HeterogeneousRegionCountCostFunction(conf);
198        assertNotNull(cf);
199        BaseLoadBalancer.Cluster cluster =
200            new BaseLoadBalancer.Cluster(serverMap, null, null, null);
201        cf.init(cluster);
202
203        // checking that we all hosts have a number of regions below their limit
204        for (final ServerAndLoad serverAndLoad : balancedCluster) {
205          final ServerName sn = serverAndLoad.getServerName();
206          final int numberRegions = serverAndLoad.getLoad();
207          final int limit = cf.findLimitForRS(sn);
208
209          double usage = (double) numberRegions / (double) limit;
210          LOG.debug(
211            sn.getHostname() + ":" + numberRegions + "/" + limit + "(" + (usage * 100) + "%)");
212
213          // as the balancer is stochastic, we cannot check exactly the result of the balancing,
214          // hence the allowedWindow parameter
215          assertTrue("Host " + sn.getHostname() + " should be below "
216              + cf.overallUsage * ALLOWED_WINDOW * 100 + "%; " + cf.overallUsage +
217              ", " + usage + ", " + numberRegions + ", " + limit,
218            usage <= cf.overallUsage * ALLOWED_WINDOW);
219        }
220      }
221
222      if (assertFullyBalancedForReplicas) {
223        this.assertRegionReplicaPlacement(serverMap, rackManager);
224      }
225    }
226  }
227
228  @Override
229  protected Map<ServerName, List<RegionInfo>> createServerMap(int numNodes, int numRegions,
230      int numRegionsPerServer, int replication, int numTables) {
231    // construct a cluster of numNodes, having a total of numRegions. Each RS will hold
232    // numRegionsPerServer many regions except for the last one, which will host all the
233    // remaining regions
234    int[] cluster = new int[numNodes];
235    for (int i = 0; i < numNodes; i++) {
236      cluster[i] = numRegionsPerServer;
237    }
238    cluster[cluster.length - 1] = numRegions - ((cluster.length - 1) * numRegionsPerServer);
239    Map<ServerName, List<RegionInfo>> clusterState = mockClusterServers(cluster, numTables);
240    if (replication > 0) {
241      // replicate the regions to the same servers
242      for (List<RegionInfo> regions : clusterState.values()) {
243        int length = regions.size();
244        for (int i = 0; i < length; i++) {
245          for (int r = 1; r < replication; r++) {
246            regions.add(RegionReplicaUtil.getRegionInfoForReplica(regions.get(i), r));
247          }
248        }
249      }
250    }
251
252    return clusterState;
253  }
254
255  @Override
256  protected TreeMap<ServerName, List<RegionInfo>> mockClusterServers(int[] mockCluster,
257      int numTables) {
258    int numServers = mockCluster.length;
259    TreeMap<ServerName, List<RegionInfo>> servers = new TreeMap<>();
260    for (int i = 0; i < numServers; i++) {
261      int numRegions = mockCluster[i];
262      ServerAndLoad sal = createServer("rs" + i);
263      List<RegionInfo> regions = randomRegions(numRegions, numTables);
264      servers.put(sal.getServerName(), regions);
265    }
266    return servers;
267  }
268
269  private Queue<ServerName> serverQueue = new LinkedList<>();
270
271  private ServerAndLoad createServer(final String host) {
272    if (!this.serverQueue.isEmpty()) {
273      ServerName sn = this.serverQueue.poll();
274      return new ServerAndLoad(sn, 0);
275    }
276    Random rand = ThreadLocalRandom.current();
277    int port = rand.nextInt(60000);
278    long startCode = rand.nextLong();
279    ServerName sn = ServerName.valueOf(host, port, startCode);
280    return new ServerAndLoad(sn, 0);
281  }
282}