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}