View Javadoc

1   /**
2    * Copyright The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase.master.balancer;
21  
22  import static org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertFalse;
24  import static org.junit.Assert.assertTrue;
25  
26  import com.google.common.collect.ArrayListMultimap;
27  import com.google.common.collect.Lists;
28  
29  import java.io.FileNotFoundException;
30  import java.io.IOException;
31  import java.security.SecureRandom;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.TreeMap;
38  import java.util.TreeSet;
39  
40  import org.apache.commons.lang.StringUtils;
41  import org.apache.hadoop.hbase.HRegionInfo;
42  import org.apache.hadoop.hbase.HTableDescriptor;
43  import org.apache.hadoop.hbase.ServerName;
44  import org.apache.hadoop.hbase.TableDescriptors;
45  import org.apache.hadoop.hbase.TableName;
46  import org.apache.hadoop.hbase.master.AssignmentManager;
47  import org.apache.hadoop.hbase.master.HMaster;
48  import org.apache.hadoop.hbase.master.MasterServices;
49  import org.apache.hadoop.hbase.master.RegionPlan;
50  import org.apache.hadoop.hbase.net.Address;
51  import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
52  import org.apache.hadoop.hbase.rsgroup.RSGroupInfoManager;
53  import org.apache.hadoop.hbase.util.Bytes;
54  import org.mockito.Mockito;
55  import org.mockito.invocation.InvocationOnMock;
56  import org.mockito.stubbing.Answer;
57  
58  public class RSGroupableBalancerTestBase {
59  
60    static SecureRandom rand = new SecureRandom();
61    static String[] groups = new String[] { RSGroupInfo.DEFAULT_GROUP, "dg2", "dg3", "dg4" };
62    static TableName table0 = TableName.valueOf("dt0");
63    static TableName[] tables =
64        new TableName[] { TableName.valueOf("dt1"),
65            TableName.valueOf("dt2"),
66            TableName.valueOf("dt3"),
67            TableName.valueOf("dt4")};
68    static List<ServerName> servers;
69    static Map<String, RSGroupInfo> groupMap;
70    static Map<TableName, String> tableMap = new HashMap<>();
71    static List<HTableDescriptor> tableDescs;
72    int[] regionAssignment = new int[] { 2, 5, 7, 10, 4, 3, 1 };
73    static int regionId = 0;
74  
75    /**
76     * Invariant is that all servers of a group have load between floor(avg) and
77     * ceiling(avg) number of regions.
78     */
79    protected void assertClusterAsBalanced(
80        ArrayListMultimap<String, ServerAndLoad> groupLoadMap) {
81      for (String gName : groupLoadMap.keySet()) {
82        List<ServerAndLoad> groupLoad = groupLoadMap.get(gName);
83        int numServers = groupLoad.size();
84        int numRegions = 0;
85        int maxRegions = 0;
86        int minRegions = Integer.MAX_VALUE;
87        for (ServerAndLoad server : groupLoad) {
88          int nr = server.getLoad();
89          if (nr > maxRegions) {
90            maxRegions = nr;
91          }
92          if (nr < minRegions) {
93            minRegions = nr;
94          }
95          numRegions += nr;
96        }
97        if (maxRegions - minRegions < 2) {
98          // less than 2 between max and min, can't balance
99          return;
100       }
101       int min = numRegions / numServers;
102       int max = numRegions % numServers == 0 ? min : min + 1;
103 
104       for (ServerAndLoad server : groupLoad) {
105         assertTrue(server.getLoad() <= max);
106         assertTrue(server.getLoad() >= min);
107       }
108     }
109   }
110 
111   /**
112    * Asserts a valid retained assignment plan.
113    * <p>
114    * Must meet the following conditions:
115    * <ul>
116    * <li>Every input region has an assignment, and to an online server
117    * <li>If a region had an existing assignment to a server with the same
118    * address a a currently online server, it will be assigned to it
119    * </ul>
120    */
121   protected void assertRetainedAssignment(
122       Map<HRegionInfo, ServerName> existing, List<ServerName> servers,
123       Map<ServerName, List<HRegionInfo>> assignment)
124       throws FileNotFoundException, IOException {
125     // Verify condition 1, every region assigned, and to online server
126     Set<ServerName> onlineServerSet = new TreeSet<ServerName>(servers);
127     Set<HRegionInfo> assignedRegions = new TreeSet<HRegionInfo>();
128     for (Map.Entry<ServerName, List<HRegionInfo>> a : assignment.entrySet()) {
129       assertTrue(
130           "Region assigned to server that was not listed as online",
131           onlineServerSet.contains(a.getKey()));
132       for (HRegionInfo r : a.getValue()) {
133         assignedRegions.add(r);
134       }
135     }
136     assertEquals(existing.size(), assignedRegions.size());
137 
138     // Verify condition 2, every region must be assigned to correct server.
139     Set<String> onlineHostNames = new TreeSet<String>();
140     for (ServerName s : servers) {
141       onlineHostNames.add(s.getHostname());
142     }
143 
144     for (Map.Entry<ServerName, List<HRegionInfo>> a : assignment.entrySet()) {
145       ServerName currentServer = a.getKey();
146       for (HRegionInfo r : a.getValue()) {
147         ServerName oldAssignedServer = existing.get(r);
148         TableName tableName = r.getTable();
149         String groupName =
150             getMockedGroupInfoManager().getRSGroupOfTable(tableName);
151         assertTrue(StringUtils.isNotEmpty(groupName));
152         RSGroupInfo gInfo = getMockedGroupInfoManager().getRSGroup(
153             groupName);
154         assertTrue(
155             "Region is not correctly assigned to group servers.",
156             gInfo.containsServer(currentServer.getAddress()));
157         if (oldAssignedServer != null
158             && onlineHostNames.contains(oldAssignedServer
159             .getHostname())) {
160           // this region was previously assigned somewhere, and that
161           // host is still around, then the host must have been is a
162           // different group.
163           if (!oldAssignedServer.getAddress().equals(currentServer.getAddress())) {
164             assertFalse(gInfo.containsServer(oldAssignedServer.getAddress()));
165           }
166         }
167       }
168     }
169   }
170 
171   protected String printStats(
172       ArrayListMultimap<String, ServerAndLoad> groupBasedLoad) {
173     StringBuffer sb = new StringBuffer();
174     sb.append("\n");
175     for (String groupName : groupBasedLoad.keySet()) {
176       sb.append("Stats for group: " + groupName);
177       sb.append("\n");
178       sb.append(groupMap.get(groupName).getServers());
179       sb.append("\n");
180       List<ServerAndLoad> groupLoad = groupBasedLoad.get(groupName);
181       int numServers = groupLoad.size();
182       int totalRegions = 0;
183       sb.append("Per Server Load: \n");
184       for (ServerAndLoad sLoad : groupLoad) {
185         sb.append("Server :" + sLoad.getServerName() + " Load : "
186             + sLoad.getLoad() + "\n");
187         totalRegions += sLoad.getLoad();
188       }
189       sb.append(" Group Statistics : \n");
190       float average = (float) totalRegions / numServers;
191       int max = (int) Math.ceil(average);
192       int min = (int) Math.floor(average);
193       sb.append("[srvr=" + numServers + " rgns=" + totalRegions + " avg="
194           + average + " max=" + max + " min=" + min + "]");
195       sb.append("\n");
196       sb.append("===============================");
197       sb.append("\n");
198     }
199     return sb.toString();
200   }
201 
202   protected ArrayListMultimap<String, ServerAndLoad> convertToGroupBasedMap(
203       final Map<ServerName, List<HRegionInfo>> serversMap) throws IOException {
204     ArrayListMultimap<String, ServerAndLoad> loadMap = ArrayListMultimap
205         .create();
206     for (RSGroupInfo gInfo : getMockedGroupInfoManager().listRSGroups()) {
207       Set<Address> groupServers = gInfo.getServers();
208       for (Address server : groupServers) {
209         ServerName actual = null;
210         for(ServerName entry: servers) {
211           if(entry.getAddress().equals(server)) {
212             actual = entry;
213             break;
214           }
215         }
216         List<HRegionInfo> regions = serversMap.get(actual);
217         assertTrue("No load for " + actual, regions != null);
218         loadMap.put(gInfo.getName(),
219             new ServerAndLoad(actual, regions.size()));
220       }
221     }
222     return loadMap;
223   }
224 
225   protected ArrayListMultimap<String, ServerAndLoad> reconcile(
226       ArrayListMultimap<String, ServerAndLoad> previousLoad,
227       List<RegionPlan> plans) {
228     ArrayListMultimap<String, ServerAndLoad> result = ArrayListMultimap
229         .create();
230     result.putAll(previousLoad);
231     if (plans != null) {
232       for (RegionPlan plan : plans) {
233         ServerName source = plan.getSource();
234         updateLoad(result, source, -1);
235         ServerName destination = plan.getDestination();
236         updateLoad(result, destination, +1);
237       }
238     }
239     return result;
240   }
241 
242   protected void updateLoad(
243       ArrayListMultimap<String, ServerAndLoad> previousLoad,
244       final ServerName sn, final int diff) {
245     for (String groupName : previousLoad.keySet()) {
246       ServerAndLoad newSAL = null;
247       ServerAndLoad oldSAL = null;
248       for (ServerAndLoad sal : previousLoad.get(groupName)) {
249         if (ServerName.isSameAddress(sn, sal.getServerName())) {
250           oldSAL = sal;
251           newSAL = new ServerAndLoad(sn, sal.getLoad() + diff);
252           break;
253         }
254       }
255       if (newSAL != null) {
256         previousLoad.remove(groupName, oldSAL);
257         previousLoad.put(groupName, newSAL);
258         break;
259       }
260     }
261   }
262 
263   protected Map<ServerName, List<HRegionInfo>> mockClusterServers() throws IOException {
264     assertTrue(servers.size() == regionAssignment.length);
265     Map<ServerName, List<HRegionInfo>> assignment = new TreeMap<ServerName, List<HRegionInfo>>();
266     for (int i = 0; i < servers.size(); i++) {
267       int numRegions = regionAssignment[i];
268       List<HRegionInfo> regions = assignedRegions(numRegions, servers.get(i));
269       assignment.put(servers.get(i), regions);
270     }
271     return assignment;
272   }
273 
274   /**
275    * Generate a list of regions evenly distributed between the tables.
276    *
277    * @param numRegions The number of regions to be generated.
278    * @return List of HRegionInfo.
279    */
280   protected List<HRegionInfo> randomRegions(int numRegions) {
281     List<HRegionInfo> regions = new ArrayList<HRegionInfo>(numRegions);
282     byte[] start = new byte[16];
283     byte[] end = new byte[16];
284     rand.nextBytes(start);
285     rand.nextBytes(end);
286     int regionIdx = rand.nextInt(tables.length);
287     for (int i = 0; i < numRegions; i++) {
288       Bytes.putInt(start, 0, numRegions << 1);
289       Bytes.putInt(end, 0, (numRegions << 1) + 1);
290       int tableIndex = (i + regionIdx) % tables.length;
291       HRegionInfo hri = new HRegionInfo(
292           tables[tableIndex], start, end, false, regionId++);
293       regions.add(hri);
294     }
295     return regions;
296   }
297 
298   /**
299    * Generate assigned regions to a given server using group information.
300    *
301    * @param numRegions the num regions to generate
302    * @param sn the servername
303    * @return the list of regions
304    * @throws java.io.IOException Signals that an I/O exception has occurred.
305    */
306   protected List<HRegionInfo> assignedRegions(int numRegions, ServerName sn) throws IOException {
307     List<HRegionInfo> regions = new ArrayList<HRegionInfo>(numRegions);
308     byte[] start = new byte[16];
309     byte[] end = new byte[16];
310     Bytes.putInt(start, 0, numRegions << 1);
311     Bytes.putInt(end, 0, (numRegions << 1) + 1);
312     for (int i = 0; i < numRegions; i++) {
313       TableName tableName = getTableName(sn);
314       HRegionInfo hri = new HRegionInfo(
315           tableName, start, end, false,
316           regionId++);
317       regions.add(hri);
318     }
319     return regions;
320   }
321 
322   protected static List<ServerName> generateServers(int numServers) {
323     List<ServerName> servers = new ArrayList<ServerName>(numServers);
324     for (int i = 0; i < numServers; i++) {
325       String host = "server" + rand.nextInt(100000);
326       int port = rand.nextInt(60000);
327       servers.add(ServerName.valueOf(host, port, -1));
328     }
329     return servers;
330   }
331 
332   /**
333    * Construct group info, with each group having at least one server.
334    *
335    * @param servers the servers
336    * @param groups the groups
337    * @return the map
338    */
339   protected static Map<String, RSGroupInfo> constructGroupInfo(
340       List<ServerName> servers, String[] groups) {
341     assertTrue(servers != null);
342     assertTrue(servers.size() >= groups.length);
343     int index = 0;
344     Map<String, RSGroupInfo> groupMap = new HashMap<String, RSGroupInfo>();
345     for (String grpName : groups) {
346       RSGroupInfo RSGroupInfo = new RSGroupInfo(grpName);
347       RSGroupInfo.addServer(servers.get(index).getAddress());
348       groupMap.put(grpName, RSGroupInfo);
349       index++;
350     }
351     while (index < servers.size()) {
352       int grpIndex = rand.nextInt(groups.length);
353       groupMap.get(groups[grpIndex]).addServer(servers.get(index).getAddress());
354       index++;
355     }
356     return groupMap;
357   }
358 
359   /**
360    * Construct table descriptors evenly distributed between the groups.
361    *
362    * @return the list
363    */
364   protected static List<HTableDescriptor> constructTableDesc(boolean hasBogusTable) {
365     List<HTableDescriptor> tds = Lists.newArrayList();
366     int index = rand.nextInt(groups.length);
367     for (int i = 0; i < tables.length; i++) {
368       HTableDescriptor htd = new HTableDescriptor(tables[i]);
369       int grpIndex = (i + index) % groups.length ;
370       String groupName = groups[grpIndex];
371       tableMap.put(tables[i], groupName);
372       tds.add(htd);
373     }
374     if (hasBogusTable) {
375       tableMap.put(table0, "");
376       tds.add(new HTableDescriptor(table0));
377     }
378     return tds;
379   }
380 
381   protected static MasterServices getMockedMaster() throws IOException {
382     TableDescriptors tds = Mockito.mock(TableDescriptors.class);
383     Mockito.when(tds.get(tables[0])).thenReturn(tableDescs.get(0));
384     Mockito.when(tds.get(tables[1])).thenReturn(tableDescs.get(1));
385     Mockito.when(tds.get(tables[2])).thenReturn(tableDescs.get(2));
386     Mockito.when(tds.get(tables[3])).thenReturn(tableDescs.get(3));
387     MasterServices services = Mockito.mock(HMaster.class);
388     Mockito.when(services.getTableDescriptors()).thenReturn(tds);
389     AssignmentManager am = Mockito.mock(AssignmentManager.class);
390     Mockito.when(services.getAssignmentManager()).thenReturn(am);
391     return services;
392   }
393 
394   protected static RSGroupInfoManager getMockedGroupInfoManager() throws IOException {
395     RSGroupInfoManager gm = Mockito.mock(RSGroupInfoManager.class);
396     Mockito.when(gm.getRSGroup(Mockito.anyString())).thenAnswer(new Answer<RSGroupInfo>() {
397       @Override
398       public RSGroupInfo answer(InvocationOnMock invocation) throws Throwable {
399         return groupMap.get(invocation.getArguments()[0]);
400       }
401     });
402     Mockito.when(gm.listRSGroups()).thenReturn(
403         Lists.newLinkedList(groupMap.values()));
404     Mockito.when(gm.isOnline()).thenReturn(true);
405     Mockito.when(gm.getRSGroupOfTable(Mockito.any(TableName.class)))
406         .thenAnswer(new Answer<String>() {
407           @Override
408           public String answer(InvocationOnMock invocation) throws Throwable {
409             return tableMap.get(invocation.getArguments()[0]);
410           }
411         });
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(HTableDescriptor desc : tableDescs){
427       if(gm.getRSGroupOfTable(desc.getTableName()).endsWith(groupOfServer.getName())){
428         tableName = desc.getTableName();
429       }
430     }
431     return tableName;
432   }
433 }