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