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.rsgroup; 019 020import com.google.errorprone.annotations.RestrictedApi; 021import edu.umd.cs.findbugs.annotations.NonNull; 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.TreeMap; 030import java.util.concurrent.atomic.AtomicBoolean; 031import java.util.concurrent.atomic.AtomicReference; 032import java.util.stream.Collectors; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.hadoop.hbase.ClusterMetrics; 035import org.apache.hadoop.hbase.HBaseIOException; 036import org.apache.hadoop.hbase.HConstants; 037import org.apache.hadoop.hbase.ServerName; 038import org.apache.hadoop.hbase.TableName; 039import org.apache.hadoop.hbase.client.RegionInfo; 040import org.apache.hadoop.hbase.constraint.ConstraintException; 041import org.apache.hadoop.hbase.favored.FavoredNodesManager; 042import org.apache.hadoop.hbase.favored.FavoredNodesPromoter; 043import org.apache.hadoop.hbase.master.LoadBalancer; 044import org.apache.hadoop.hbase.master.MasterServices; 045import org.apache.hadoop.hbase.master.RegionPlan; 046import org.apache.hadoop.hbase.master.balancer.ClusterInfoProvider; 047import org.apache.hadoop.hbase.master.balancer.LoadBalancerFactory; 048import org.apache.hadoop.hbase.master.balancer.MasterClusterInfoProvider; 049import org.apache.hadoop.hbase.net.Address; 050import org.apache.hadoop.hbase.util.Pair; 051import org.apache.hadoop.hbase.util.ReflectionUtils; 052import org.apache.yetus.audience.InterfaceAudience; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056import org.apache.hbase.thirdparty.com.google.common.collect.ArrayListMultimap; 057import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap; 058import org.apache.hbase.thirdparty.com.google.common.collect.Lists; 059import org.apache.hbase.thirdparty.com.google.common.collect.Maps; 060 061/** 062 * GroupBasedLoadBalancer, used when Region Server Grouping is configured (HBase-6721) It does 063 * region balance based on a table's group membership. 064 * <p/> 065 * Most assignment methods contain two exclusive code paths: Online - when the group table is online 066 * and Offline - when it is unavailable. 067 * <p/> 068 * During Offline, assignments are assigned based on cached information in zookeeper. If unavailable 069 * (ie bootstrap) then regions are assigned randomly. 070 * <p/> 071 * Once the GROUP table has been assigned, the balancer switches to Online and will then start 072 * providing appropriate assignments for user tables. 073 */ 074@InterfaceAudience.Private 075public class RSGroupBasedLoadBalancer implements LoadBalancer { 076 private static final Logger LOG = LoggerFactory.getLogger(RSGroupBasedLoadBalancer.class); 077 078 private MasterServices masterServices; 079 private ClusterInfoProvider provider; 080 private FavoredNodesManager favoredNodesManager; 081 private volatile RSGroupInfoManager rsGroupInfoManager; 082 private volatile LoadBalancer internalBalancer; 083 084 /** 085 * Tracks whether a balance run is currently in progress. 086 */ 087 private final AtomicBoolean isBalancing = new AtomicBoolean(false); 088 089 /** 090 * Holds a configuration update that arrived while a balance run was in progress. 091 */ 092 private AtomicReference<Configuration> pendingConfiguration = new AtomicReference<>(); 093 094 /** 095 * Set this key to {@code true} to allow region fallback. Fallback to the default rsgroup first, 096 * then fallback to any group if no online servers in default rsgroup. Please keep balancer switch 097 * on at the same time, which is relied on to correct misplaced regions 098 */ 099 public static final String FALLBACK_GROUP_ENABLE_KEY = "hbase.rsgroup.fallback.enable"; 100 101 private volatile boolean fallbackEnabled = false; 102 103 @RestrictedApi(explanation = "Should only be called in tests", link = "", 104 allowedOnPath = ".*/src/test/.*") 105 public void setFallbackEnabledForTest(boolean enabled) { 106 this.fallbackEnabled = enabled; 107 } 108 109 // Test-only call-tracking flags, set true whenever the corresponding assignment method below is 110 // invoked. Reset via resetAssignmentCallFlagsForTest() before the operation under test. 111 private volatile boolean roundRobinAssignmentInvoked = false; 112 private volatile boolean retainAssignmentInvoked = false; 113 private volatile boolean randomAssignmentInvoked = false; 114 115 @RestrictedApi(explanation = "Should only be called in tests", link = "", 116 allowedOnPath = ".*/src/test/.*") 117 public boolean isRoundRobinAssignmentInvoked() { 118 return roundRobinAssignmentInvoked; 119 } 120 121 @RestrictedApi(explanation = "Should only be called in tests", link = "", 122 allowedOnPath = ".*/src/test/.*") 123 public boolean isRetainAssignmentInvoked() { 124 return retainAssignmentInvoked; 125 } 126 127 @RestrictedApi(explanation = "Should only be called in tests", link = "", 128 allowedOnPath = ".*/src/test/.*") 129 public boolean isRandomAssignmentInvoked() { 130 return randomAssignmentInvoked; 131 } 132 133 @RestrictedApi(explanation = "Should only be called in tests", link = "", 134 allowedOnPath = ".*/src/test/.*") 135 public void resetAssignmentCallFlagsForTest() { 136 roundRobinAssignmentInvoked = false; 137 retainAssignmentInvoked = false; 138 randomAssignmentInvoked = false; 139 } 140 141 /** 142 * Used by reflection in {@link org.apache.hadoop.hbase.master.balancer.LoadBalancerFactory}. 143 */ 144 @InterfaceAudience.Private 145 public RSGroupBasedLoadBalancer() { 146 } 147 148 // must be called after calling initialize 149 @Override 150 public synchronized void updateClusterMetrics(ClusterMetrics sm) { 151 assert internalBalancer != null; 152 internalBalancer.updateClusterMetrics(sm); 153 } 154 155 @Override 156 public synchronized void 157 updateBalancerLoadInfo(Map<TableName, Map<ServerName, List<RegionInfo>>> loadOfAllTable) { 158 internalBalancer.updateBalancerLoadInfo(loadOfAllTable); 159 } 160 161 public void setMasterServices(MasterServices masterServices) { 162 this.masterServices = masterServices; 163 } 164 165 /** 166 * Balance by RSGroup. 167 */ 168 @Override 169 public synchronized List<RegionPlan> balanceCluster( 170 Map<TableName, Map<ServerName, List<RegionInfo>>> loadOfAllTable) throws IOException { 171 if (!isOnline()) { 172 throw new ConstraintException( 173 RSGroupInfoManager.class.getSimpleName() + " is not online, unable to perform balance"); 174 } 175 176 // Calculate correct assignments and a list of RegionPlan for mis-placed regions 177 Pair<Map<TableName, Map<ServerName, List<RegionInfo>>>, 178 List<RegionPlan>> correctedStateAndRegionPlans = correctAssignments(loadOfAllTable); 179 Map<TableName, Map<ServerName, List<RegionInfo>>> correctedLoadOfAllTable = 180 correctedStateAndRegionPlans.getFirst(); 181 List<RegionPlan> regionPlans = correctedStateAndRegionPlans.getSecond(); 182 RSGroupInfo defaultInfo = rsGroupInfoManager.getRSGroup(RSGroupInfo.DEFAULT_GROUP); 183 // Add RegionPlan 184 // for the regions which have been placed according to the region server group assignment 185 // into the movement list 186 try { 187 // For each rsgroup 188 for (RSGroupInfo rsgroup : rsGroupInfoManager.listRSGroups()) { 189 LOG.debug("Balancing RSGroup={}", rsgroup.getName()); 190 Map<TableName, Map<ServerName, List<RegionInfo>>> loadOfTablesInGroup = new HashMap<>(); 191 for (Map.Entry<TableName, Map<ServerName, List<RegionInfo>>> entry : correctedLoadOfAllTable 192 .entrySet()) { 193 TableName tableName = entry.getKey(); 194 RSGroupInfo targetRSGInfo = RSGroupUtil 195 .getRSGroupInfo(masterServices, rsGroupInfoManager, tableName).orElse(defaultInfo); 196 if (targetRSGInfo.getName().equals(rsgroup.getName())) { 197 loadOfTablesInGroup.put(tableName, entry.getValue()); 198 } 199 } 200 List<RegionPlan> groupPlans = null; 201 if (!loadOfTablesInGroup.isEmpty()) { 202 LOG.info("Start Generate Balance plan for group: " + rsgroup.getName()); 203 groupPlans = this.internalBalancer.balanceCluster(loadOfTablesInGroup); 204 } 205 if (groupPlans != null) { 206 regionPlans.addAll(groupPlans); 207 } 208 } 209 } catch (IOException exp) { 210 LOG.warn("Exception while balancing cluster.", exp); 211 regionPlans.clear(); 212 } 213 214 // Return the whole movement list 215 return regionPlans; 216 } 217 218 @Override 219 @NonNull 220 public Map<ServerName, List<RegionInfo>> roundRobinAssignment(List<RegionInfo> regions, 221 List<ServerName> servers) throws IOException { 222 roundRobinAssignmentInvoked = true; 223 Map<ServerName, List<RegionInfo>> assignments = Maps.newHashMap(); 224 List<Pair<List<RegionInfo>, List<ServerName>>> pairs = 225 generateGroupAssignments(regions, servers); 226 for (Pair<List<RegionInfo>, List<ServerName>> pair : pairs) { 227 Map<ServerName, List<RegionInfo>> result = 228 this.internalBalancer.roundRobinAssignment(pair.getFirst(), pair.getSecond()); 229 result.forEach((server, regionInfos) -> assignments 230 .computeIfAbsent(server, s -> Lists.newArrayList()).addAll(regionInfos)); 231 } 232 return assignments; 233 } 234 235 @Override 236 @NonNull 237 public Map<ServerName, List<RegionInfo>> retainAssignment(Map<RegionInfo, ServerName> regions, 238 List<ServerName> servers) throws HBaseIOException { 239 retainAssignmentInvoked = true; 240 try { 241 Map<ServerName, List<RegionInfo>> assignments = new TreeMap<>(); 242 List<Pair<List<RegionInfo>, List<ServerName>>> pairs = 243 generateGroupAssignments(Lists.newArrayList(regions.keySet()), servers); 244 for (Pair<List<RegionInfo>, List<ServerName>> pair : pairs) { 245 List<RegionInfo> regionList = pair.getFirst(); 246 Map<RegionInfo, ServerName> currentAssignmentMap = Maps.newTreeMap(); 247 regionList.forEach(r -> currentAssignmentMap.put(r, regions.get(r))); 248 Map<ServerName, List<RegionInfo>> pairResult = 249 this.internalBalancer.retainAssignment(currentAssignmentMap, pair.getSecond()); 250 pairResult.forEach((server, rs) -> assignments 251 .computeIfAbsent(server, s -> Lists.newArrayList()).addAll(rs)); 252 } 253 return assignments; 254 } catch (IOException e) { 255 throw new HBaseIOException("Failed to do online retain assignment", e); 256 } 257 } 258 259 @Override 260 public ServerName randomAssignment(RegionInfo region, List<ServerName> servers) 261 throws IOException { 262 randomAssignmentInvoked = true; 263 List<Pair<List<RegionInfo>, List<ServerName>>> pairs = 264 generateGroupAssignments(Lists.newArrayList(region), servers); 265 List<ServerName> filteredServers = pairs.iterator().next().getSecond(); 266 return this.internalBalancer.randomAssignment(region, filteredServers); 267 } 268 269 private List<Pair<List<RegionInfo>, List<ServerName>>> generateGroupAssignments( 270 List<RegionInfo> regions, List<ServerName> servers) throws HBaseIOException { 271 try { 272 ListMultimap<String, RegionInfo> regionMap = ArrayListMultimap.create(); 273 ListMultimap<String, ServerName> serverMap = ArrayListMultimap.create(); 274 RSGroupInfo defaultInfo = rsGroupInfoManager.getRSGroup(RSGroupInfo.DEFAULT_GROUP); 275 for (RegionInfo region : regions) { 276 String groupName = 277 RSGroupUtil.getRSGroupInfo(masterServices, rsGroupInfoManager, region.getTable()) 278 .orElse(defaultInfo).getName(); 279 regionMap.put(groupName, region); 280 } 281 for (String groupKey : regionMap.keySet()) { 282 RSGroupInfo info = rsGroupInfoManager.getRSGroup(groupKey); 283 serverMap.putAll(groupKey, filterOfflineServers(info, servers)); 284 } 285 286 List<Pair<List<RegionInfo>, List<ServerName>>> result = Lists.newArrayList(); 287 List<RegionInfo> fallbackRegions = Lists.newArrayList(); 288 for (String groupKey : regionMap.keySet()) { 289 if (serverMap.get(groupKey).isEmpty()) { 290 fallbackRegions.addAll(regionMap.get(groupKey)); 291 } else { 292 result.add(Pair.newPair(regionMap.get(groupKey), serverMap.get(groupKey))); 293 } 294 } 295 if (!fallbackRegions.isEmpty()) { 296 List<ServerName> candidates = null; 297 if (isFallbackEnabled()) { 298 if (LOG.isDebugEnabled()) { 299 LOG.debug("Falling back {} regions to servers outside their RSGroup. Regions: {}", 300 fallbackRegions.size(), fallbackRegions.stream() 301 .map(RegionInfo::getRegionNameAsString).collect(Collectors.toSet())); 302 } 303 candidates = getFallBackCandidates(servers); 304 } 305 candidates = (candidates == null || candidates.isEmpty()) 306 ? Lists.newArrayList(BOGUS_SERVER_NAME) 307 : candidates; 308 result.add(Pair.newPair(fallbackRegions, candidates)); 309 } 310 return result; 311 } catch (IOException e) { 312 throw new HBaseIOException("Failed to generate group assignments", e); 313 } 314 } 315 316 private List<ServerName> filterOfflineServers(RSGroupInfo RSGroupInfo, 317 List<ServerName> onlineServers) { 318 if (RSGroupInfo != null) { 319 return filterServers(RSGroupInfo.getServers(), onlineServers); 320 } else { 321 LOG.warn("RSGroup Information found to be null. Some regions might be unassigned."); 322 return Collections.emptyList(); 323 } 324 } 325 326 /** 327 * Filter servers based on the online servers. 328 * <p/> 329 * servers is actually a TreeSet (see {@link org.apache.hadoop.hbase.rsgroup.RSGroupInfo}), having 330 * its contains()'s time complexity as O(logn), which is good enough. 331 * <p/> 332 * TODO: consider using HashSet to pursue O(1) for contains() throughout the calling chain if 333 * needed. 334 * @param servers the servers 335 * @param onlineServers List of servers which are online. 336 * @return the list 337 */ 338 private List<ServerName> filterServers(Set<Address> servers, List<ServerName> onlineServers) { 339 ArrayList<ServerName> finalList = new ArrayList<>(); 340 for (ServerName onlineServer : onlineServers) { 341 if (servers.contains(onlineServer.getAddress())) { 342 finalList.add(onlineServer); 343 } 344 } 345 return finalList; 346 } 347 348 private Pair<Map<TableName, Map<ServerName, List<RegionInfo>>>, List<RegionPlan>> 349 correctAssignments(Map<TableName, Map<ServerName, List<RegionInfo>>> existingAssignments) 350 throws IOException { 351 // To return 352 Map<TableName, Map<ServerName, List<RegionInfo>>> correctAssignments = new HashMap<>(); 353 List<RegionPlan> regionPlansForMisplacedRegions = new ArrayList<>(); 354 RSGroupInfo defaultInfo = rsGroupInfoManager.getRSGroup(RSGroupInfo.DEFAULT_GROUP); 355 for (Map.Entry<TableName, Map<ServerName, List<RegionInfo>>> assignments : existingAssignments 356 .entrySet()) { 357 TableName tableName = assignments.getKey(); 358 Map<ServerName, List<RegionInfo>> clusterLoad = assignments.getValue(); 359 RSGroupInfo targetRSGInfo = null; 360 Map<ServerName, List<RegionInfo>> correctServerRegion = new TreeMap<>(); 361 try { 362 targetRSGInfo = RSGroupUtil.getRSGroupInfo(masterServices, rsGroupInfoManager, tableName) 363 .orElse(defaultInfo); 364 } catch (IOException exp) { 365 LOG.debug("RSGroup information null for region of table " + tableName, exp); 366 } 367 for (Map.Entry<ServerName, List<RegionInfo>> serverRegionMap : clusterLoad.entrySet()) { 368 ServerName currentHostServer = serverRegionMap.getKey(); 369 List<RegionInfo> regionInfoList = serverRegionMap.getValue(); 370 if ( 371 targetRSGInfo == null || !targetRSGInfo.containsServer(currentHostServer.getAddress()) 372 ) { 373 regionInfoList.forEach(regionInfo -> { 374 regionPlansForMisplacedRegions.add(new RegionPlan(regionInfo, currentHostServer, null)); 375 }); 376 } else { 377 correctServerRegion.put(currentHostServer, regionInfoList); 378 } 379 } 380 correctAssignments.put(tableName, correctServerRegion); 381 } 382 383 // Return correct assignments and region movement plan for mis-placed regions together 384 return new Pair<Map<TableName, Map<ServerName, List<RegionInfo>>>, List<RegionPlan>>( 385 correctAssignments, regionPlansForMisplacedRegions); 386 } 387 388 @Override 389 public void initialize() throws IOException { 390 if (rsGroupInfoManager == null) { 391 rsGroupInfoManager = masterServices.getRSGroupInfoManager(); 392 if (rsGroupInfoManager == null) { 393 String msg = "RSGroupInfoManager hasn't been initialized"; 394 LOG.error(msg); 395 throw new HBaseIOException(msg); 396 } 397 rsGroupInfoManager.start(); 398 } 399 400 // Create the balancer 401 Configuration conf = masterServices.getConfiguration(); 402 Class<? extends LoadBalancer> balancerClass; 403 @SuppressWarnings("deprecation") 404 String balancerClassName = conf.get(HBASE_RSGROUP_LOADBALANCER_CLASS); 405 if (balancerClassName == null) { 406 balancerClass = conf.getClass(HConstants.HBASE_MASTER_LOADBALANCER_CLASS, 407 LoadBalancerFactory.getDefaultLoadBalancerClass(), LoadBalancer.class); 408 } else { 409 try { 410 balancerClass = Class.forName(balancerClassName).asSubclass(LoadBalancer.class); 411 } catch (ClassNotFoundException e) { 412 throw new IOException(e); 413 } 414 } 415 this.provider = new MasterClusterInfoProvider(masterServices); 416 // avoid infinite nesting 417 if (getClass().isAssignableFrom(balancerClass)) { 418 balancerClass = LoadBalancerFactory.getDefaultLoadBalancerClass(); 419 } 420 internalBalancer = ReflectionUtils.newInstance(balancerClass); 421 internalBalancer.setClusterInfoProvider(provider); 422 // special handling for favor node balancers 423 if (internalBalancer instanceof FavoredNodesPromoter) { 424 favoredNodesManager = new FavoredNodesManager(provider); 425 ((FavoredNodesPromoter) internalBalancer).setFavoredNodesManager(favoredNodesManager); 426 } 427 internalBalancer.initialize(); 428 // init fallback groups 429 this.fallbackEnabled = conf.getBoolean(FALLBACK_GROUP_ENABLE_KEY, false); 430 } 431 432 public boolean isOnline() { 433 if (this.rsGroupInfoManager == null) { 434 return false; 435 } 436 437 return this.rsGroupInfoManager.isOnline(); 438 } 439 440 public boolean isFallbackEnabled() { 441 return fallbackEnabled; 442 } 443 444 @Override 445 public void regionOnline(RegionInfo regionInfo, ServerName sn) { 446 } 447 448 @Override 449 public void regionOffline(RegionInfo regionInfo) { 450 } 451 452 @Override 453 public void onConfigurationChange(Configuration conf) { 454 // Refer to HBASE-29933 455 synchronized (this) { 456 // If balance is running, store configuration in pendingConfiguration and return immediately. 457 // Defer the config update. 458 if (isBalancing.get()) { 459 LOG.debug( 460 "Balance is in progress, defer applying configuration change until balance completed."); 461 pendingConfiguration.set(conf); 462 } else { 463 // Apply configuration change immediately. 464 updateConfiguration(conf); 465 } 466 } 467 } 468 469 /** 470 * Applies the given configuration. 471 */ 472 private void updateConfiguration(Configuration conf) { 473 boolean newFallbackEnabled = conf.getBoolean(FALLBACK_GROUP_ENABLE_KEY, false); 474 if (fallbackEnabled != newFallbackEnabled) { 475 LOG.info("Changing the value of {} from {} to {}", FALLBACK_GROUP_ENABLE_KEY, fallbackEnabled, 476 newFallbackEnabled); 477 fallbackEnabled = newFallbackEnabled; 478 } 479 provider.onConfigurationChange(conf); 480 internalBalancer.onConfigurationChange(conf); 481 } 482 483 /** 484 * If a pending configuration was stored during a balance run, apply it and clear the pending 485 * reference. 486 */ 487 public void applyPendingConfiguration() { 488 Configuration toApply = pendingConfiguration.getAndSet(null); 489 if (toApply != null) { 490 LOG.info("Applying pending configuration after balance completed."); 491 updateConfiguration(toApply); 492 } 493 } 494 495 /** 496 * Sets {@link #isBalancing} to {@code true} before a balance run starts. 497 */ 498 @Override 499 public void onBalancingStart() { 500 LOG.debug("setting isBalancing to true as balance is starting"); 501 isBalancing.set(true); 502 } 503 504 /** 505 * Sets {@link #isBalancing} to {@code false} after a balance run completes and applies any 506 * pending configuration that arrived during balancing. 507 */ 508 @Override 509 public void onBalancingComplete() { 510 LOG.debug("setting isBalancing to false as balance is completed"); 511 isBalancing.set(false); 512 applyPendingConfiguration(); 513 } 514 515 @Override 516 public void throttle(RegionPlan plan) throws Exception { 517 long throttlingTime = internalBalancer.getThrottleDurationMs(plan); 518 if (throttlingTime > 0) { 519 try { 520 // Release the monitor while waiting to avoid blocking other threads. 521 wait(throttlingTime); 522 } catch (InterruptedException e) { 523 throw new RuntimeException(e); 524 } 525 } 526 } 527 528 @Override 529 public void stop(String why) { 530 internalBalancer.stop(why); 531 } 532 533 @Override 534 public boolean isStopped() { 535 return internalBalancer.isStopped(); 536 } 537 538 public LoadBalancer getInternalBalancer() { 539 return internalBalancer; 540 } 541 542 public FavoredNodesManager getFavoredNodesManager() { 543 return favoredNodesManager; 544 } 545 546 @Override 547 public synchronized void postMasterStartupInitialize() { 548 this.internalBalancer.postMasterStartupInitialize(); 549 } 550 551 public void updateBalancerStatus(boolean status) { 552 internalBalancer.updateBalancerStatus(status); 553 } 554 555 private List<ServerName> getFallBackCandidates(List<ServerName> servers) { 556 List<ServerName> serverNames = null; 557 try { 558 RSGroupInfo info = rsGroupInfoManager.getRSGroup(RSGroupInfo.DEFAULT_GROUP); 559 serverNames = filterOfflineServers(info, servers); 560 } catch (IOException e) { 561 LOG.error("Failed to get default rsgroup info to fallback", e); 562 } 563 return serverNames == null || serverNames.isEmpty() ? servers : serverNames; 564 } 565 566 @Override 567 public void setClusterInfoProvider(ClusterInfoProvider provider) { 568 throw new UnsupportedOperationException("Just call set master service instead"); 569 } 570}