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