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.balancer;
019
020import static org.apache.hadoop.hbase.master.balancer.HeterogeneousCostRulesTestHelper.DEFAULT_RULES_FILE_NAME;
021import static org.apache.hadoop.hbase.master.balancer.HeterogeneousCostRulesTestHelper.createRulesFile;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.util.ArrayDeque;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031import java.util.Queue;
032import java.util.Random;
033import java.util.TreeMap;
034import java.util.concurrent.ThreadLocalRandom;
035import org.apache.hadoop.fs.FileSystem;
036import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
037import org.apache.hadoop.hbase.HConstants;
038import org.apache.hadoop.hbase.ServerName;
039import org.apache.hadoop.hbase.client.RegionInfo;
040import org.apache.hadoop.hbase.client.RegionReplicaUtil;
041import org.apache.hadoop.hbase.master.RegionPlan;
042import org.apache.hadoop.hbase.testclassification.MasterTests;
043import org.apache.hadoop.hbase.testclassification.MediumTests;
044import org.junit.jupiter.api.BeforeAll;
045import org.junit.jupiter.api.Tag;
046import org.junit.jupiter.api.Test;
047
048@Tag(MasterTests.TAG)
049@Tag(MediumTests.TAG)
050public class TestStochasticLoadBalancerHeterogeneousCost extends StochasticBalancerTestBase {
051
052  private static final HBaseCommonTestingUtil HTU = new HBaseCommonTestingUtil();
053  private static String RULES_FILE;
054
055  @BeforeAll
056  public static void beforeAllTests() throws IOException {
057    conf = HTU.getConfiguration();
058    conf.setFloat("hbase.master.balancer.stochastic.regionCountCost", 0);
059    conf.setFloat("hbase.master.balancer.stochastic.primaryRegionCountCost", 0);
060    conf.setFloat("hbase.master.balancer.stochastic.tableSkewCost", 0);
061    conf.set(StochasticLoadBalancer.COST_FUNCTIONS_COST_FUNCTIONS_KEY,
062      HeterogeneousRegionCountCostFunction.class.getName());
063    // Need to ensure test dir has been created.
064    assertTrue(FileSystem.get(HTU.getConfiguration()).mkdirs(HTU.getDataTestDir()));
065    RULES_FILE = HTU.getDataTestDir(DEFAULT_RULES_FILE_NAME).toString();
066    conf.set(HeterogeneousRegionCountCostFunction.HBASE_MASTER_BALANCER_HETEROGENEOUS_RULES_FILE,
067      RULES_FILE);
068    loadBalancer = new StochasticLoadTestBalancer();
069    loadBalancer.setClusterInfoProvider(new DummyClusterInfoProvider(conf));
070    loadBalancer.initialize();
071  }
072
073  @Test
074  public void testDefault() throws IOException {
075    final List<String> rules = Collections.emptyList();
076
077    final int numNodes = 2;
078    final int numRegions = 300;
079    final int numRegionsPerServer = 250;
080
081    // Initial state: { rs1:50 , rs0:250 }
082    // Cluster can hold 300/400 regions (75%)
083    // Expected balanced Cluster: { rs0:150 , rs1:150 }
084    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
085  }
086
087  @Test
088  public void testOneGroup() throws IOException {
089    final List<String> rules = Collections.singletonList("rs.* 100");
090
091    final int numNodes = 4;
092    final int numRegions = 300;
093    final int numRegionsPerServer = 30;
094
095    // Initial state: { rs0:30 , rs1:30 , rs2:30 , rs3:210 }.
096    // The cluster can hold 300/400 regions (75%)
097    // Expected balanced Cluster: { rs0:75 , rs1:75 , rs2:75 , rs3:75 }
098    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
099  }
100
101  @Test
102  public void testTwoGroups() throws IOException {
103    final List<String> rules = Arrays.asList("rs[0-4] 200", "rs[5-9] 50");
104
105    final int numNodes = 10;
106    final int numRegions = 500;
107    final int numRegionsPerServer = 50;
108
109    // Initial state: { rs0:50 , rs1:50 , rs2:50 , rs3:50 , rs4:50 , rs5:50 , rs6:50 , rs7:50 ,
110    // rs8:50 , rs9:50 }
111    // the cluster can hold 500/1250 regions (40%)
112    // Expected balanced Cluster: { rs5:20 , rs6:20 , rs7:20 , rs8:20 , rs9:20 , rs0:80 , rs1:80 ,
113    // rs2:80 , rs3:80 , rs4:80 }
114    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
115  }
116
117  @Test
118  public void testFourGroups() throws IOException {
119    final List<String> rules = Arrays.asList("rs[1-3] 200", "rs[4-7] 250", "rs[8-9] 100");
120
121    final int numNodes = 10;
122    final int numRegions = 800;
123    final int numRegionsPerServer = 80;
124
125    // Initial state: { rs0:80 , rs1:80 , rs2:80 , rs3:80 , rs4:80 , rs5:80 , rs6:80 , rs7:80 ,
126    // rs8:80 , rs9:80 }
127    // Cluster can hold 800/2000 regions (40%)
128    // Expected balanced Cluster: { rs8:40 , rs9:40 , rs2:80 , rs3:80 , rs1:82 , rs0:94 , rs4:96 ,
129    // rs5:96 , rs6:96 , rs7:96 }
130    this.testHeterogeneousWithCluster(numNodes, numRegions, numRegionsPerServer, rules);
131  }
132
133  @Test
134  public void testOverloaded() throws IOException {
135    final int numNodes = 2;
136    final int numRegions = 120;
137    final int numRegionsPerServer = 60;
138
139    createRulesFile(RULES_FILE);
140    final Map<ServerName, List<RegionInfo>> serverMap =
141      this.createServerMap(numNodes, numRegions, numRegionsPerServer, 1, 1);
142    final List<RegionPlan> plans =
143      loadBalancer.balanceTable(HConstants.ENSEMBLE_TABLE_NAME, serverMap);
144    // As we disabled all the other cost functions, balancing only according to
145    // the heterogeneous cost function should return nothing.
146    assertNull(plans);
147  }
148
149  private void testHeterogeneousWithCluster(final int numNodes, final int numRegions,
150    final int numRegionsPerServer, final List<String> rules) throws IOException {
151
152    createRulesFile(RULES_FILE, rules);
153    final Map<ServerName, List<RegionInfo>> serverMap =
154      this.createServerMap(numNodes, numRegions, numRegionsPerServer, 1, 1);
155    this.testWithClusterWithIteration(serverMap, null, true, false);
156  }
157
158  @Override
159  protected Map<ServerName, List<RegionInfo>> createServerMap(int numNodes, int numRegions,
160    int numRegionsPerServer, int replication, int numTables) {
161    // construct a cluster of numNodes, having a total of numRegions. Each RS will hold
162    // numRegionsPerServer many regions except for the last one, which will host all the
163    // remaining regions
164    int[] cluster = new int[numNodes];
165    for (int i = 0; i < numNodes; i++) {
166      cluster[i] = numRegionsPerServer;
167    }
168    cluster[cluster.length - 1] = numRegions - ((cluster.length - 1) * numRegionsPerServer);
169    Map<ServerName, List<RegionInfo>> clusterState = mockClusterServers(cluster, numTables);
170    if (replication > 0) {
171      // replicate the regions to the same servers
172      for (List<RegionInfo> regions : clusterState.values()) {
173        int length = regions.size();
174        for (int i = 0; i < length; i++) {
175          for (int r = 1; r < replication; r++) {
176            regions.add(RegionReplicaUtil.getRegionInfoForReplica(regions.get(i), r));
177          }
178        }
179      }
180    }
181
182    return clusterState;
183  }
184
185  @Override
186  protected TreeMap<ServerName, List<RegionInfo>> mockClusterServers(int[] mockCluster,
187    int numTables) {
188    int numServers = mockCluster.length;
189    TreeMap<ServerName, List<RegionInfo>> servers = new TreeMap<>();
190    for (int i = 0; i < numServers; i++) {
191      int numRegions = mockCluster[i];
192      ServerAndLoad sal = createServer("rs" + i);
193      List<RegionInfo> regions = randomRegions(numRegions, numTables);
194      servers.put(sal.getServerName(), regions);
195    }
196    return servers;
197  }
198
199  private Queue<ServerName> serverQueue = new ArrayDeque<>();
200
201  private ServerAndLoad createServer(final String host) {
202    if (!this.serverQueue.isEmpty()) {
203      ServerName sn = this.serverQueue.poll();
204      return new ServerAndLoad(sn, 0);
205    }
206    Random rand = ThreadLocalRandom.current();
207    int port = rand.nextInt(60000);
208    long startCode = rand.nextLong();
209    ServerName sn = ServerName.valueOf(host, port, startCode);
210    return new ServerAndLoad(sn, 0);
211  }
212
213  static class FairRandomCandidateGenerator extends RandomCandidateGenerator {
214
215    @Override
216    public BalanceAction pickRandomRegions(BalancerClusterState cluster, int thisServer,
217      int otherServer) {
218      if (thisServer < 0 || otherServer < 0) {
219        return BalanceAction.NULL_ACTION;
220      }
221
222      int thisRegion = pickRandomRegion(cluster, thisServer, 0.5);
223      int otherRegion = pickRandomRegion(cluster, otherServer, 0.5);
224
225      return getAction(thisServer, thisRegion, otherServer, otherRegion);
226    }
227
228    @Override
229    BalanceAction generate(BalancerClusterState cluster) {
230      return super.generate(cluster);
231    }
232  }
233
234  static class StochasticLoadTestBalancer extends StochasticLoadBalancer {
235    private FairRandomCandidateGenerator fairRandomCandidateGenerator =
236      new FairRandomCandidateGenerator();
237
238    StochasticLoadTestBalancer() {
239      super(new DummyMetricsStochasticBalancer());
240    }
241
242    @Override
243    protected CandidateGenerator getRandomGenerator(BalancerClusterState cluster) {
244      return fairRandomCandidateGenerator;
245    }
246  }
247}