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}