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.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Random;
031import java.util.Set;
032import java.util.TreeMap;
033import java.util.TreeSet;
034import java.util.concurrent.ThreadLocalRandom;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.HBaseConfiguration;
038import org.apache.hadoop.hbase.ServerName;
039import org.apache.hadoop.hbase.TableDescriptors;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.RegionInfo;
042import org.apache.hadoop.hbase.client.RegionInfoBuilder;
043import org.apache.hadoop.hbase.client.TableDescriptor;
044import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
045import org.apache.hadoop.hbase.master.HMaster;
046import org.apache.hadoop.hbase.master.MasterServices;
047import org.apache.hadoop.hbase.master.RegionPlan;
048import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
049import org.apache.hadoop.hbase.net.Address;
050import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
051import org.apache.hadoop.hbase.rsgroup.RSGroupInfoManager;
052import org.apache.hadoop.hbase.util.Bytes;
053import org.mockito.Mockito;
054import org.mockito.invocation.InvocationOnMock;
055import org.mockito.stubbing.Answer;
056
057import org.apache.hbase.thirdparty.com.google.common.collect.ArrayListMultimap;
058import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
059
060/**
061 * Base UT of RSGroupableBalancer.
062 */
063public class RSGroupableBalancerTestBase extends BalancerTestBase {
064
065  static String[] groups = new String[] { RSGroupInfo.DEFAULT_GROUP, "dg2", "dg3", "dg4" };
066  static TableName table0 = TableName.valueOf("dt0");
067  static TableName[] tables = new TableName[] { TableName.valueOf("dt1"), TableName.valueOf("dt2"),
068    TableName.valueOf("dt3"), TableName.valueOf("dt4") };
069  static List<ServerName> servers;
070  static Map<String, RSGroupInfo> groupMap;
071  static Map<TableName, String> tableMap = new HashMap<>();
072  static List<TableDescriptor> tableDescs;
073  int[] regionAssignment = new int[] { 2, 5, 7, 10, 4, 3, 1 };
074  static int regionId = 0;
075  static Configuration conf = HBaseConfiguration.create();
076
077  /**
078   * Invariant is that all servers of a group have load between floor(avg) and ceiling(avg) number
079   * of regions.
080   */
081  protected void assertClusterAsBalanced(ArrayListMultimap<String, ServerAndLoad> groupLoadMap) {
082    for (String gName : groupLoadMap.keySet()) {
083      List<ServerAndLoad> groupLoad = groupLoadMap.get(gName);
084      int numServers = groupLoad.size();
085      int numRegions = 0;
086      int maxRegions = 0;
087      int minRegions = Integer.MAX_VALUE;
088      for (ServerAndLoad server : groupLoad) {
089        int nr = server.getLoad();
090        if (nr > maxRegions) {
091          maxRegions = nr;
092        }
093        if (nr < minRegions) {
094          minRegions = nr;
095        }
096        numRegions += nr;
097      }
098      if (maxRegions - minRegions < 2) {
099        // less than 2 between max and min, can't balance
100        return;
101      }
102      int min = numRegions / numServers;
103      int max = numRegions % numServers == 0 ? min : min + 1;
104
105      for (ServerAndLoad server : groupLoad) {
106        assertTrue(server.getLoad() <= max);
107        assertTrue(server.getLoad() >= min);
108      }
109    }
110  }
111
112  /**
113   * All regions have an assignment.
114   */
115  protected void assertImmediateAssignment(List<RegionInfo> regions, List<ServerName> servers,
116    Map<RegionInfo, ServerName> assignments) throws IOException {
117    for (RegionInfo region : regions) {
118      assertTrue(assignments.containsKey(region));
119      ServerName server = assignments.get(region);
120      TableName tableName = region.getTable();
121
122      String groupName = getMockedGroupInfoManager().getRSGroupOfTable(tableName);
123      assertTrue(StringUtils.isNotEmpty(groupName));
124      RSGroupInfo gInfo = getMockedGroupInfoManager().getRSGroup(groupName);
125      assertTrue("Region is not correctly assigned to group servers.",
126        gInfo.containsServer(server.getAddress()));
127    }
128  }
129
130  /**
131   * Asserts a valid retained assignment plan.
132   * <p>
133   * Must meet the following conditions:
134   * <ul>
135   * <li>Every input region has an assignment, and to an online server
136   * <li>If a region had an existing assignment to a server with the same address a a currently
137   * online server, it will be assigned to it
138   * </ul>
139   */
140  protected void assertRetainedAssignment(Map<RegionInfo, ServerName> existing,
141    List<ServerName> servers, Map<ServerName, List<RegionInfo>> assignment)
142    throws FileNotFoundException, IOException {
143    // Verify condition 1, every region assigned, and to online server
144    Set<ServerName> onlineServerSet = new TreeSet<>(servers);
145    Set<RegionInfo> assignedRegions = new TreeSet<>(RegionInfo.COMPARATOR);
146    for (Map.Entry<ServerName, List<RegionInfo>> a : assignment.entrySet()) {
147      assertTrue("Region assigned to server that was not listed as online",
148        onlineServerSet.contains(a.getKey()));
149      for (RegionInfo r : a.getValue()) {
150        assignedRegions.add(r);
151      }
152    }
153    assertEquals(existing.size(), assignedRegions.size());
154
155    // Verify condition 2, every region must be assigned to correct server.
156    Set<String> onlineHostNames = new TreeSet<>();
157    for (ServerName s : servers) {
158      onlineHostNames.add(s.getHostname());
159    }
160
161    for (Map.Entry<ServerName, List<RegionInfo>> a : assignment.entrySet()) {
162      ServerName currentServer = a.getKey();
163      for (RegionInfo r : a.getValue()) {
164        ServerName oldAssignedServer = existing.get(r);
165        TableName tableName = r.getTable();
166        String groupName = getMockedGroupInfoManager().getRSGroupOfTable(tableName);
167        assertTrue(StringUtils.isNotEmpty(groupName));
168        RSGroupInfo gInfo = getMockedGroupInfoManager().getRSGroup(groupName);
169        assertTrue("Region is not correctly assigned to group servers.",
170          gInfo.containsServer(currentServer.getAddress()));
171        if (
172          oldAssignedServer != null && onlineHostNames.contains(oldAssignedServer.getHostname())
173        ) {
174          // this region was previously assigned somewhere, and that
175          // host is still around, then the host must have been is a
176          // different group.
177          if (!oldAssignedServer.getAddress().equals(currentServer.getAddress())) {
178            assertFalse(gInfo.containsServer(oldAssignedServer.getAddress()));
179          }
180        }
181      }
182    }
183  }
184
185  protected String printStats(ArrayListMultimap<String, ServerAndLoad> groupBasedLoad) {
186    StringBuilder sb = new StringBuilder();
187    sb.append("\n");
188    for (String groupName : groupBasedLoad.keySet()) {
189      sb.append("Stats for group: " + groupName);
190      sb.append("\n");
191      sb.append(groupMap.get(groupName).getServers());
192      sb.append("\n");
193      List<ServerAndLoad> groupLoad = groupBasedLoad.get(groupName);
194      int numServers = groupLoad.size();
195      int totalRegions = 0;
196      sb.append("Per Server Load: \n");
197      for (ServerAndLoad sLoad : groupLoad) {
198        sb.append("Server :" + sLoad.getServerName() + " Load : " + sLoad.getLoad() + "\n");
199        totalRegions += sLoad.getLoad();
200      }
201      sb.append(" Group Statistics : \n");
202      float average = (float) totalRegions / numServers;
203      int max = (int) Math.ceil(average);
204      int min = (int) Math.floor(average);
205      sb.append("[srvr=" + numServers + " rgns=" + totalRegions + " avg=" + average + " max=" + max
206        + " min=" + min + "]");
207      sb.append("\n");
208      sb.append("===============================");
209      sb.append("\n");
210    }
211    return sb.toString();
212  }
213
214  protected ArrayListMultimap<String, ServerAndLoad>
215    convertToGroupBasedMap(final Map<ServerName, List<RegionInfo>> serversMap) throws IOException {
216    ArrayListMultimap<String, ServerAndLoad> loadMap = ArrayListMultimap.create();
217    for (RSGroupInfo gInfo : getMockedGroupInfoManager().listRSGroups()) {
218      Set<Address> groupServers = gInfo.getServers();
219      for (Address hostPort : groupServers) {
220        ServerName actual = null;
221        for (ServerName entry : servers) {
222          if (entry.getAddress().equals(hostPort)) {
223            actual = entry;
224            break;
225          }
226        }
227        List<RegionInfo> regions = serversMap.get(actual);
228        assertTrue("No load for " + actual, regions != null);
229        loadMap.put(gInfo.getName(), new ServerAndLoad(actual, regions.size()));
230      }
231    }
232    return loadMap;
233  }
234
235  protected ArrayListMultimap<String, ServerAndLoad>
236    reconcile(ArrayListMultimap<String, ServerAndLoad> previousLoad, List<RegionPlan> plans) {
237    ArrayListMultimap<String, ServerAndLoad> result = ArrayListMultimap.create();
238    result.putAll(previousLoad);
239    if (plans != null) {
240      for (RegionPlan plan : plans) {
241        ServerName source = plan.getSource();
242        updateLoad(result, source, -1);
243        ServerName destination = plan.getDestination();
244        updateLoad(result, destination, +1);
245      }
246    }
247    return result;
248  }
249
250  protected void updateLoad(ArrayListMultimap<String, ServerAndLoad> previousLoad,
251    final ServerName sn, final int diff) {
252    for (String groupName : previousLoad.keySet()) {
253      ServerAndLoad newSAL = null;
254      ServerAndLoad oldSAL = null;
255      for (ServerAndLoad sal : previousLoad.get(groupName)) {
256        if (ServerName.isSameAddress(sn, sal.getServerName())) {
257          oldSAL = sal;
258          newSAL = new ServerAndLoad(sn, sal.getLoad() + diff);
259          break;
260        }
261      }
262      if (newSAL != null) {
263        previousLoad.remove(groupName, oldSAL);
264        previousLoad.put(groupName, newSAL);
265        break;
266      }
267    }
268  }
269
270  protected Map<ServerName, List<RegionInfo>> mockClusterServers() throws IOException {
271    assertTrue(servers.size() == regionAssignment.length);
272    Map<ServerName, List<RegionInfo>> assignment = new TreeMap<>();
273    for (int i = 0; i < servers.size(); i++) {
274      int numRegions = regionAssignment[i];
275      List<RegionInfo> regions = assignedRegions(numRegions, servers.get(i));
276      assignment.put(servers.get(i), regions);
277    }
278    return assignment;
279  }
280
281  /**
282   * Generate a list of regions evenly distributed between the tables.
283   * @param numRegions The number of regions to be generated.
284   * @return List of RegionInfo.
285   */
286  protected List<RegionInfo> randomRegions(int numRegions) {
287    List<RegionInfo> regions = new ArrayList<>(numRegions);
288    byte[] start = new byte[16];
289    Bytes.random(start);
290    byte[] end = new byte[16];
291    Bytes.random(end);
292    int regionIdx = ThreadLocalRandom.current().nextInt(tables.length);
293    for (int i = 0; i < numRegions; i++) {
294      Bytes.putInt(start, 0, numRegions << 1);
295      Bytes.putInt(end, 0, (numRegions << 1) + 1);
296      int tableIndex = (i + regionIdx) % tables.length;
297      regions.add(RegionInfoBuilder.newBuilder(tables[tableIndex]).setStartKey(start).setEndKey(end)
298        .setSplit(false).setRegionId(regionId++).build());
299    }
300    return regions;
301  }
302
303  /**
304   * Generate assigned regions to a given server using group information.
305   * @param numRegions the num regions to generate
306   * @param sn         the servername
307   * @return the list of regions
308   * @throws java.io.IOException Signals that an I/O exception has occurred.
309   */
310  protected List<RegionInfo> assignedRegions(int numRegions, ServerName sn) throws IOException {
311    List<RegionInfo> regions = new ArrayList<>(numRegions);
312    byte[] start = new byte[16];
313    byte[] end = new byte[16];
314    Bytes.putInt(start, 0, numRegions << 1);
315    Bytes.putInt(end, 0, (numRegions << 1) + 1);
316    for (int i = 0; i < numRegions; i++) {
317      TableName tableName = getTableName(sn);
318      regions.add(RegionInfoBuilder.newBuilder(tableName).setStartKey(start).setEndKey(end)
319        .setSplit(false).setRegionId(regionId++).build());
320    }
321    return regions;
322  }
323
324  protected static List<ServerName> generateServers(int numServers) {
325    List<ServerName> servers = new ArrayList<>(numServers);
326    Random rand = ThreadLocalRandom.current();
327    for (int i = 0; i < numServers; i++) {
328      String host = "server" + rand.nextInt(100000);
329      int port = rand.nextInt(60000);
330      servers.add(ServerName.valueOf(host, port, -1));
331    }
332    return servers;
333  }
334
335  /**
336   * Construct group info, with each group having at least one server.
337   * @param servers the servers
338   * @param groups  the groups
339   * @return the map
340   */
341  protected static Map<String, RSGroupInfo> constructGroupInfo(List<ServerName> servers,
342    String[] groups) {
343    assertTrue(servers != null);
344    assertTrue(servers.size() >= groups.length);
345    int index = 0;
346    Map<String, RSGroupInfo> groupMap = new HashMap<>();
347    for (String grpName : groups) {
348      RSGroupInfo RSGroupInfo = new RSGroupInfo(grpName);
349      RSGroupInfo.addServer(servers.get(index).getAddress());
350      groupMap.put(grpName, RSGroupInfo);
351      index++;
352    }
353    Random rand = ThreadLocalRandom.current();
354    while (index < servers.size()) {
355      int grpIndex = rand.nextInt(groups.length);
356      groupMap.get(groups[grpIndex]).addServer(servers.get(index).getAddress());
357      index++;
358    }
359    return groupMap;
360  }
361
362  /**
363   * Construct table descriptors evenly distributed between the groups.
364   * @param hasBogusTable there is a table that does not determine the group
365   * @return the list of table descriptors
366   */
367  protected static List<TableDescriptor> constructTableDesc(boolean hasBogusTable) {
368    List<TableDescriptor> tds = Lists.newArrayList();
369    Random rand = ThreadLocalRandom.current();
370    int index = rand.nextInt(groups.length);
371    for (int i = 0; i < tables.length; i++) {
372      TableDescriptor htd = TableDescriptorBuilder.newBuilder(tables[i]).build();
373      int grpIndex = (i + index) % groups.length;
374      String groupName = groups[grpIndex];
375      tableMap.put(tables[i], groupName);
376      tds.add(htd);
377    }
378    if (hasBogusTable) {
379      tableMap.put(table0, "");
380      tds.add(TableDescriptorBuilder.newBuilder(table0).build());
381    }
382    return tds;
383  }
384
385  protected static MasterServices getMockedMaster() throws IOException {
386    TableDescriptors tds = Mockito.mock(TableDescriptors.class);
387    Mockito.when(tds.get(tables[0])).thenReturn(tableDescs.get(0));
388    Mockito.when(tds.get(tables[1])).thenReturn(tableDescs.get(1));
389    Mockito.when(tds.get(tables[2])).thenReturn(tableDescs.get(2));
390    Mockito.when(tds.get(tables[3])).thenReturn(tableDescs.get(3));
391    MasterServices services = Mockito.mock(HMaster.class);
392    Mockito.when(services.getTableDescriptors()).thenReturn(tds);
393    AssignmentManager am = Mockito.mock(AssignmentManager.class);
394    Mockito.when(services.getAssignmentManager()).thenReturn(am);
395    Mockito.when(services.getConfiguration()).thenReturn(conf);
396    return services;
397  }
398
399  protected static RSGroupInfoManager getMockedGroupInfoManager() throws IOException {
400    RSGroupInfoManager gm = Mockito.mock(RSGroupInfoManager.class);
401    Mockito.when(gm.getRSGroup(Mockito.any())).thenAnswer(new Answer<RSGroupInfo>() {
402      @Override
403      public RSGroupInfo answer(InvocationOnMock invocation) throws Throwable {
404        return groupMap.get(invocation.getArgument(0));
405      }
406    });
407    Mockito.when(gm.listRSGroups()).thenReturn(Lists.newLinkedList(groupMap.values()));
408    Mockito.when(gm.isOnline()).thenReturn(true);
409    Mockito.when(gm.getRSGroupOfTable(Mockito.any())).thenAnswer(new Answer<String>() {
410      @Override
411      public String answer(InvocationOnMock invocation) throws Throwable {
412        return tableMap.get(invocation.getArgument(0));
413      }
414    });
415    return gm;
416  }
417
418  protected TableName getTableName(ServerName sn) throws IOException {
419    TableName tableName = null;
420    RSGroupInfoManager gm = getMockedGroupInfoManager();
421    RSGroupInfo groupOfServer = null;
422    for (RSGroupInfo gInfo : gm.listRSGroups()) {
423      if (gInfo.containsServer(sn.getAddress())) {
424        groupOfServer = gInfo;
425        break;
426      }
427    }
428
429    for (TableDescriptor desc : tableDescs) {
430      if (gm.getRSGroupOfTable(desc.getTableName()).endsWith(groupOfServer.getName())) {
431        tableName = desc.getTableName();
432      }
433    }
434    return tableName;
435  }
436}