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 020/** An implementation of the {@link org.apache.hadoop.hbase.master.LoadBalancer} that assigns regions 021 * based on the amount they are cached on a given server. A region can move across the region 022 * servers whenever a region server shuts down or crashes. The region server preserves the cache 023 * periodically and restores the cache when it is restarted. This balancer implements a mechanism 024 * where it maintains the amount by which a region is cached on a region server. During balancer 025 * run, a region plan is generated that takes into account this cache information and tries to 026 * move the regions so that the cache is minimally impacted. 027 */ 028 029import static org.apache.hadoop.hbase.HConstants.BUCKET_CACHE_PERSISTENT_PATH_KEY; 030import static org.apache.hadoop.hbase.HConstants.BUCKET_CACHE_SIZE_KEY; 031 032import java.math.BigDecimal; 033import java.text.DecimalFormat; 034import java.util.ArrayDeque; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Deque; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Map; 041import java.util.Optional; 042import java.util.concurrent.ThreadLocalRandom; 043import org.apache.hadoop.conf.Configuration; 044import org.apache.hadoop.hbase.ClusterMetrics; 045import org.apache.hadoop.hbase.RegionMetrics; 046import org.apache.hadoop.hbase.ServerMetrics; 047import org.apache.hadoop.hbase.ServerName; 048import org.apache.hadoop.hbase.Size; 049import org.apache.hadoop.hbase.TableName; 050import org.apache.hadoop.hbase.client.RegionInfo; 051import org.apache.hadoop.hbase.master.RackManager; 052import org.apache.hadoop.hbase.master.RegionPlan; 053import org.apache.hadoop.hbase.util.Pair; 054import org.apache.yetus.audience.InterfaceAudience; 055import org.slf4j.Logger; 056import org.slf4j.LoggerFactory; 057 058@InterfaceAudience.Private 059public class CacheAwareLoadBalancer extends StochasticLoadBalancer { 060 private static final Logger LOG = LoggerFactory.getLogger(CacheAwareLoadBalancer.class); 061 062 public static final String CACHE_RATIO_THRESHOLD = 063 "hbase.master.balancer.stochastic.throttling.cacheRatio"; 064 public static final float CACHE_RATIO_THRESHOLD_DEFAULT = 0.8f; 065 066 /** 067 * Below this cache ratio on the current host, a move may be considered for the free-space 068 * heuristic. 069 */ 070 public static final String LOW_CACHE_RATIO_FOR_RELOCATION_KEY = 071 "hbase.master.balancer.cacheaware.lowCacheRatioThreshold"; 072 public static final float LOW_CACHE_RATIO_FOR_RELOCATION_DEFAULT = 0.35f; 073 074 /** 075 * Optimistic region cache ratio assumed for cost purposes when a better host has free cache space 076 * (actual warmup is not modeled). 077 */ 078 public static final String POTENTIAL_CACHE_RATIO_AFTER_MOVE_KEY = 079 "hbase.master.balancer.cacheaware.potentialCacheRatioAfterMove"; 080 public static final float POTENTIAL_CACHE_RATIO_AFTER_MOVE_DEFAULT = 0.95f; 081 082 /** 083 * Minimum free block cache on a target server, as a multiple of the region's on-disk size in 084 * bytes, required to count that server as a relocation opportunity. 085 */ 086 public static final String MIN_FREE_CACHE_SPACE_FACTOR_KEY = 087 "hbase.master.balancer.cacheaware.minFreeCacheSpaceFactor"; 088 public static final float MIN_FREE_CACHE_SPACE_FACTOR_DEFAULT = 1.0f; 089 090 public Float ratioThreshold; 091 092 private Long sleepTime; 093 private Configuration configuration; 094 095 private float lowCacheRatioThreshold; 096 private float potentialCacheRatioAfterMove; 097 private float minFreeCacheSpaceFactor; 098 private long cachePrefetchOverheadBytes; 099 private boolean cacheSpaceTrackingEnabled; 100 101 private BigDecimal simulatedRatio = BigDecimal.ZERO; 102 103 @Override 104 public void loadConf(Configuration configuration) { 105 this.configuration = configuration; 106 this.costFunctions = new ArrayList<>(); 107 super.loadConf(configuration); 108 ratioThreshold = 109 this.configuration.getFloat(CACHE_RATIO_THRESHOLD, CACHE_RATIO_THRESHOLD_DEFAULT); 110 sleepTime = configuration.getLong(MOVE_THROTTLING, MOVE_THROTTLING_DEFAULT.toMillis()); 111 lowCacheRatioThreshold = configuration.getFloat(LOW_CACHE_RATIO_FOR_RELOCATION_KEY, 112 LOW_CACHE_RATIO_FOR_RELOCATION_DEFAULT); 113 potentialCacheRatioAfterMove = configuration.getFloat(POTENTIAL_CACHE_RATIO_AFTER_MOVE_KEY, 114 POTENTIAL_CACHE_RATIO_AFTER_MOVE_DEFAULT); 115 minFreeCacheSpaceFactor = 116 configuration.getFloat(MIN_FREE_CACHE_SPACE_FACTOR_KEY, MIN_FREE_CACHE_SPACE_FACTOR_DEFAULT); 117 float bucketCacheSizeMB = configuration.getFloat(BUCKET_CACHE_SIZE_KEY, 0F); 118 float acceptableFactor = configuration.getFloat("hbase.bucketcache.acceptfactor", 0.95f); 119 cachePrefetchOverheadBytes = 120 (long) (bucketCacheSizeMB * 1024L * 1024L * (1 - acceptableFactor)); 121 cacheSpaceTrackingEnabled = bucketCacheSizeMB > 0; 122 if (!cacheSpaceTrackingEnabled) { 123 LOG.warn("{} is not configured on the master. The free-space relocation heuristic cannot " 124 + "account for the prefetch threshold and may move regions to servers where prefetch " 125 + "will be blocked.", BUCKET_CACHE_SIZE_KEY); 126 } 127 } 128 129 @Override 130 protected Map<Class<? extends CandidateGenerator>, CandidateGenerator> 131 createCandidateGenerators(Configuration conf) { 132 Map<Class<? extends CandidateGenerator>, CandidateGenerator> candidateGenerators = 133 new HashMap<>(2); 134 candidateGenerators.put(CacheAwareSkewnessCandidateGenerator.class, 135 new CacheAwareSkewnessCandidateGenerator()); 136 candidateGenerators.put(CacheAwareCandidateGenerator.class, new CacheAwareCandidateGenerator()); 137 return candidateGenerators; 138 } 139 140 @Override 141 protected List<CostFunction> createCostFunctions(Configuration configuration) { 142 List<CostFunction> costFunctions = new ArrayList<>(); 143 addCostFunction(costFunctions, new CacheAwareRegionSkewnessCostFunction(configuration)); 144 addCostFunction(costFunctions, new CacheAwareCostFunction(configuration)); 145 return costFunctions; 146 } 147 148 private void addCostFunction(List<CostFunction> costFunctions, CostFunction costFunction) { 149 if (costFunction.getMultiplier() > 0) { 150 costFunctions.add(costFunction); 151 } 152 } 153 154 @Override 155 public void updateClusterMetrics(ClusterMetrics clusterMetrics) { 156 this.clusterStatus = clusterMetrics; 157 updateRegionLoad(); 158 } 159 160 protected Map<ServerName, Long> getServerBlockCacheFreeBytes() { 161 if (clusterStatus == null || !cacheSpaceTrackingEnabled) { 162 return null; 163 } 164 Map<ServerName, Long> map = new HashMap<>(); 165 clusterStatus.getLiveServerMetrics().forEach((sn, sm) -> { 166 long effectiveFree = Math.max(0, sm.getCacheFreeSize() - cachePrefetchOverheadBytes); 167 map.put(sn, effectiveFree); 168 }); 169 return map; 170 } 171 172 @Override 173 protected BalancerClusterState createState(Map<ServerName, List<RegionInfo>> clusterState, 174 Map<String, Deque<BalancerRegionLoad>> loads, RegionHDFSBlockLocationFinder finder, 175 RackManager rackManager) { 176 return new BalancerClusterState(clusterState, loads, finder, rackManager, 177 regionCacheRatioOnOldServerMap, getServerBlockCacheFreeBytes()); 178 } 179 180 /** 181 * Collect the amount of region cached for all the regions from all the active region servers. 182 */ 183 private void updateRegionLoad() { 184 loads = new HashMap<>(); 185 regionCacheRatioOnOldServerMap = new HashMap<>(); 186 Map<String, Pair<ServerName, Integer>> regionCacheRatioOnCurrentServerMap = new HashMap<>(); 187 188 // Build current region cache statistics 189 clusterStatus.getLiveServerMetrics().forEach((ServerName sn, ServerMetrics sm) -> { 190 // Create a map of region and the server where it is currently hosted 191 sm.getRegionMetrics().forEach((byte[] regionName, RegionMetrics rm) -> { 192 String regionEncodedName = RegionInfo.encodeRegionName(regionName); 193 194 Deque<BalancerRegionLoad> rload = new ArrayDeque<>(); 195 196 // Get the total size of the hFiles in this region 197 int regionSizeMB = (int) rm.getRegionSizeMB().get(Size.Unit.MEGABYTE); 198 199 rload.add(new BalancerRegionLoad(rm)); 200 // Maintain a map of region and its total size. This is needed to calculate the cache 201 // ratios for the regions cached on old region servers 202 regionCacheRatioOnCurrentServerMap.put(regionEncodedName, new Pair<>(sn, regionSizeMB)); 203 loads.put(regionEncodedName, rload); 204 }); 205 }); 206 207 // Build cache statistics for the regions hosted previously on old region servers 208 clusterStatus.getLiveServerMetrics().forEach((ServerName sn, ServerMetrics sm) -> { 209 // Find if a region was previously hosted on a server other than the one it is currently 210 // hosted on. 211 sm.getRegionCachedInfo().forEach((String regionEncodedName, Integer regionSizeInCache) -> { 212 // If the region is found in regionCacheRatioOnCurrentServerMap, it is currently hosted on 213 // this server 214 if (regionCacheRatioOnCurrentServerMap.containsKey(regionEncodedName)) { 215 ServerName currentServer = 216 regionCacheRatioOnCurrentServerMap.get(regionEncodedName).getFirst(); 217 if (!ServerName.isSameAddress(currentServer, sn)) { 218 int regionSizeMB = 219 regionCacheRatioOnCurrentServerMap.get(regionEncodedName).getSecond(); 220 // The coldDataSize accounts for data size classified as "cold" by DataTieringManager, 221 // which should be kept out of cache. We calculate cache ratio on old server based 222 // only on the hot data size for the region (regionSizeMB - coldDataSize), as we 223 // don't want to move regions with low cache ratio due to data classified as cold. 224 int coldDataSize = sm.getRegionColdDataSize().getOrDefault(regionEncodedName, 0); 225 float regionCacheRatioOnOldServer = (regionSizeMB - coldDataSize) <= 0 226 ? 0.0f 227 : (float) regionSizeInCache / (regionSizeMB - coldDataSize); 228 regionCacheRatioOnOldServerMap.put(regionEncodedName, 229 new Pair<>(sn, regionCacheRatioOnOldServer)); 230 } 231 } 232 }); 233 }); 234 } 235 236 private RegionInfo getRegionInfoByEncodedName(BalancerClusterState cluster, String regionName) { 237 Optional<RegionInfo> regionInfoOptional = 238 Arrays.stream(cluster.regions).filter((RegionInfo ri) -> { 239 return regionName.equals(ri.getEncodedName()); 240 }).findFirst(); 241 242 if (regionInfoOptional.isPresent()) { 243 return regionInfoOptional.get(); 244 } 245 return null; 246 } 247 248 private boolean serverHasCacheSpaceForRegion(BalancerClusterState cluster, int region, 249 int server) { 250 if (cluster.serverBlockCacheFreeSize == null) { 251 return true; 252 } 253 int regionSizeMb = cluster.getRegionSizeMinusColdDataMB(region); 254 if (regionSizeMb <= 0) { 255 return true; 256 } 257 long bytesNeeded = (long) regionSizeMb * 1024L * 1024L; 258 return cluster.serverBlockCacheFreeSize[server] >= bytesNeeded; 259 } 260 261 @Override 262 public long getThrottleDurationMs(RegionPlan plan) { 263 Pair<ServerName, Float> rsRatio = this.regionCacheRatioOnOldServerMap.get(plan.getRegionName()); 264 if ( 265 rsRatio != null && plan.getDestination().equals(rsRatio.getFirst()) 266 && rsRatio.getSecond() >= ratioThreshold 267 ) { 268 LOG.debug("Moving region {} to server {} with cache ratio {}. No throttling needed.", 269 plan.getRegionInfo().getEncodedName(), plan.getDestination(), rsRatio.getSecond()); 270 return 0L; 271 } 272 // Skip throttling for regions with low cache ratio on their source server — there is 273 // negligible cached data to lose, so no warm-up delay is needed on the destination. 274 float cacheRatioOnSource = getRegionCacheRatioOnSource(plan); 275 if (cacheRatioOnSource < lowCacheRatioThreshold) { 276 LOG.debug( 277 "Moving region {} to server {} with low cache ratio {} on source. No throttling needed.", 278 plan.getRegionInfo().getEncodedName(), plan.getDestination(), cacheRatioOnSource); 279 return 0L; 280 } 281 282 if (rsRatio != null) { 283 LOG.debug("Moving region {} to server {} with cache ratio: {}. Throttling move for {}ms.", 284 plan.getRegionInfo().getEncodedName(), plan.getDestination(), 285 plan.getDestination().equals(rsRatio.getFirst()) ? rsRatio.getSecond() : "unknown", 286 sleepTime); 287 } else { 288 LOG.debug( 289 "Moving region {} to server {} with no cache ratio info for the region. " 290 + "Throttling move for {}ms.", 291 plan.getRegionInfo().getEncodedName(), plan.getDestination(), sleepTime); 292 } 293 return sleepTime; 294 } 295 296 private float getRegionCacheRatioOnSource(RegionPlan plan) { 297 Deque<BalancerRegionLoad> regionLoad = loads.get(plan.getRegionName()); 298 if (regionLoad != null && !regionLoad.isEmpty()) { 299 return regionLoad.getFirst().getCurrentRegionCacheRatio(); 300 } 301 // Unknown cache ratio — assume it may be cached and require throttling 302 return 1.0f; 303 } 304 305 @Override 306 protected List<RegionPlan> balanceTable(TableName tableName, 307 Map<ServerName, List<RegionInfo>> loadOfOneTable) { 308 final Map<String, Pair<ServerName, Float>> snapshot = new HashMap<>(); 309 snapshot.putAll(this.regionCacheRatioOnOldServerMap); 310 List<RegionPlan> plans = super.balanceTable(tableName, loadOfOneTable); 311 if (plans == null) { 312 return plans; 313 } 314 plans.sort((p1, p2) -> { 315 Pair<ServerName, Float> pair1 = snapshot.get(p1.getRegionName()); 316 Float ratio1 = 317 pair1 == null ? 0 : pair1.getFirst().equals(p1.getDestination()) ? pair1.getSecond() : 0f; 318 Pair<ServerName, Float> pair2 = snapshot.get(p2.getRegionName()); 319 Float ratio2 = 320 pair2 == null ? 0 : pair2.getFirst().equals(p2.getDestination()) ? pair2.getSecond() : 0f; 321 return ratio1.compareTo(ratio2) * (-1); 322 }); 323 return plans; 324 } 325 326 private class CacheAwareCandidateGenerator extends CandidateGenerator { 327 @Override 328 protected BalanceAction generate(BalancerClusterState cluster) { 329 simulatedRatio = BigDecimal.ZERO; 330 // Move the regions to the servers they were previously hosted on based on the cache ratio 331 if ( 332 !regionCacheRatioOnOldServerMap.isEmpty() 333 && regionCacheRatioOnOldServerMap.entrySet().iterator().hasNext() 334 ) { 335 Map.Entry<String, Pair<ServerName, Float>> regionCacheRatioServerMap = 336 regionCacheRatioOnOldServerMap.entrySet().iterator().next(); 337 // Get the server where this region was previously hosted 338 String regionEncodedName = regionCacheRatioServerMap.getKey(); 339 RegionInfo regionInfo = getRegionInfoByEncodedName(cluster, regionEncodedName); 340 if (regionInfo == null) { 341 LOG.warn("Region {} not found", regionEncodedName); 342 regionCacheRatioOnOldServerMap.remove(regionEncodedName); 343 return BalanceAction.NULL_ACTION; 344 } 345 if (regionInfo.isMetaRegion() || regionInfo.getTable().isSystemTable()) { 346 regionCacheRatioOnOldServerMap.remove(regionEncodedName); 347 return BalanceAction.NULL_ACTION; 348 } 349 int regionIndex = cluster.regionsToIndex.get(regionInfo); 350 int oldServerIndex = cluster.serversToIndex 351 .get(regionCacheRatioOnOldServerMap.get(regionEncodedName).getFirst().getAddress()); 352 if (oldServerIndex < 0) { 353 LOG.warn("Server previously hosting region {} not found", regionEncodedName); 354 regionCacheRatioOnOldServerMap.remove(regionEncodedName); 355 return BalanceAction.NULL_ACTION; 356 } 357 358 float oldRegionCacheRatio = 359 cluster.getOrComputeRegionCacheRatio(regionIndex, oldServerIndex); 360 int currentServerIndex = cluster.regionIndexToServerIndex[regionIndex]; 361 float currentRegionCacheRatio = 362 cluster.getOrComputeRegionCacheRatio(regionIndex, currentServerIndex); 363 364 BalanceAction action = generatePlan(cluster, regionIndex, currentServerIndex, 365 currentRegionCacheRatio, oldServerIndex, oldRegionCacheRatio); 366 regionCacheRatioOnOldServerMap.remove(regionEncodedName); 367 return action; 368 } 369 return generatePlanForFreeCacheSpace(cluster); 370 } 371 372 private BalanceAction generatePlanForFreeCacheSpace(BalancerClusterState cluster) { 373 if (cluster.serverBlockCacheFreeSize == null) { 374 return BalanceAction.NULL_ACTION; 375 } 376 List<BalanceAction> possibleActions = new ArrayList<>(); 377 Map<Integer, Long> serverFreeCacheAfterAction = new HashMap<>(); 378 for (int region = 0; region < cluster.numRegions; region++) { 379 RegionInfo regionInfo = cluster.regions[region]; 380 if (regionInfo.isMetaRegion() || regionInfo.getTable().isSystemTable()) { 381 continue; 382 } 383 int currentServer = cluster.regionIndexToServerIndex[region]; 384 float ratio = cluster.getSumRegionCacheAndColdDataRatio(region); 385 if (ratio >= lowCacheRatioThreshold) { 386 continue; 387 } 388 int regionSizeMb = cluster.getRegionSizeMinusColdDataMB(region); 389 if (regionSizeMb <= 0) { 390 continue; 391 } 392 long bytesNeeded = (long) (regionSizeMb * 1024L * 1024L * minFreeCacheSpaceFactor); 393 for (int server = 0; server < cluster.numServers; server++) { 394 // Skips current server for region, as we can't generate a move to same server 395 if (server == currentServer) { 396 continue; 397 } 398 serverFreeCacheAfterAction.putIfAbsent(server, cluster.serverBlockCacheFreeSize[server]); 399 if (serverFreeCacheAfterAction.get(server) >= bytesNeeded) { 400 serverFreeCacheAfterAction.compute(server, (s, freeCache) -> freeCache - bytesNeeded); 401 possibleActions.add(getAction(currentServer, region, server, -1)); 402 } 403 } 404 } 405 if (!possibleActions.isEmpty()) { 406 BalanceAction action = 407 possibleActions.get(ThreadLocalRandom.current().nextInt(possibleActions.size())); 408 LOG.debug("region {} had sum ratio {}", 409 cluster.regions[((MoveRegionAction) action).getRegion()].getEncodedName(), 410 cluster.getSumRegionCacheAndColdDataRatio(((MoveRegionAction) action).getRegion())); 411 return action; 412 } 413 return BalanceAction.NULL_ACTION; 414 } 415 416 private BalanceAction generatePlan(BalancerClusterState cluster, int regionIndex, 417 int currentServerIndex, float cacheRatioOnCurrentServer, int oldServerIndex, 418 float cacheRatioOnOldServer) { 419 return moveRegionToOldServer(cluster, regionIndex, currentServerIndex, 420 cacheRatioOnCurrentServer, oldServerIndex, cacheRatioOnOldServer) 421 ? getAction(currentServerIndex, regionIndex, oldServerIndex, -1) 422 : generatePlanForFreeCacheSpace(cluster); 423 } 424 425 private boolean moveRegionToOldServer(BalancerClusterState cluster, int regionIndex, 426 int currentServerIndex, float cacheRatioOnCurrentServer, int oldServerIndex, 427 float cacheRatioOnOldServer) { 428 // Find if the region has already moved by comparing the current server index with the 429 // current server index. This can happen when other candidate generator has moved the region 430 if (currentServerIndex < 0 || oldServerIndex < 0) { 431 return false; 432 } 433 434 // If the region is already well-cached on its current server, don't disrupt it. 435 // The old server's historical cache data may be stale, and moving a hot region 436 // causes unnecessary cache churn. 437 if (cacheRatioOnCurrentServer >= ratioThreshold) { 438 if (LOG.isDebugEnabled()) { 439 LOG.debug( 440 "Region {} not moved from {} to {} as it is already well-cached ({}) on current server", 441 cluster.regions[regionIndex].getEncodedName(), cluster.servers[currentServerIndex], 442 cluster.servers[oldServerIndex], cacheRatioOnCurrentServer); 443 } 444 return false; 445 } 446 447 if (!serverHasCacheSpaceForRegion(cluster, regionIndex, oldServerIndex)) { 448 if (LOG.isDebugEnabled()) { 449 LOG.debug("Region {} not moved from {} to {} as destination server lacks cache space", 450 cluster.regions[regionIndex].getEncodedName(), cluster.servers[currentServerIndex], 451 cluster.servers[oldServerIndex]); 452 } 453 return false; 454 } 455 456 DecimalFormat df = new DecimalFormat("#"); 457 df.setMaximumFractionDigits(4); 458 459 float cacheRatioDiffThreshold = 0.6f; 460 461 // Conditions for moving the region 462 463 // If the region is fully cached on the old server, move the region back 464 if (cacheRatioOnOldServer == 1.0f) { 465 if (LOG.isDebugEnabled()) { 466 LOG.debug("Region {} moved to the old server {} as it is fully cached there", 467 cluster.regions[regionIndex].getEncodedName(), cluster.servers[oldServerIndex]); 468 } 469 return true; 470 } 471 472 // Move the region back to the old server if it is cached equally on both the servers 473 if (cacheRatioOnCurrentServer == cacheRatioOnOldServer) { 474 if (LOG.isDebugEnabled()) { 475 LOG.debug( 476 "Region {} moved from {} to {} as the region is cached {} equally on both servers", 477 cluster.regions[regionIndex].getEncodedName(), cluster.servers[currentServerIndex], 478 cluster.servers[oldServerIndex], df.format(cacheRatioOnCurrentServer)); 479 } 480 return true; 481 } 482 483 // If the region is not fully cached on either of the servers, move the region back to the 484 // old server if the region cache ratio on the current server is still much less than the old 485 // server 486 if ( 487 cacheRatioOnOldServer > 0.0f 488 && cacheRatioOnCurrentServer / cacheRatioOnOldServer < cacheRatioDiffThreshold 489 ) { 490 if (LOG.isDebugEnabled()) { 491 LOG.debug( 492 "Region {} moved from {} to {} as region cache ratio {} is better than the current " 493 + "cache ratio {}", 494 cluster.regions[regionIndex].getEncodedName(), cluster.servers[currentServerIndex], 495 cluster.servers[oldServerIndex], cacheRatioOnOldServer, cacheRatioOnCurrentServer); 496 } 497 return true; 498 } 499 500 if (LOG.isDebugEnabled()) { 501 LOG.debug( 502 "Region {} not moved from {} to {} with current cache ratio {} and old cache ratio {}", 503 cluster.regions[regionIndex], cluster.servers[currentServerIndex], 504 cluster.servers[oldServerIndex], cacheRatioOnCurrentServer, cacheRatioOnOldServer); 505 } 506 return false; 507 } 508 } 509 510 private class CacheAwareSkewnessCandidateGenerator extends LoadCandidateGenerator { 511 @Override 512 BalanceAction pickRandomRegions(BalancerClusterState cluster, int thisServer, int otherServer) { 513 simulatedRatio = BigDecimal.ZERO; 514 515 if (thisServer < 0 || otherServer < 0) { 516 return BalanceAction.NULL_ACTION; 517 } 518 519 int regionIndexToMove = pickLeastCachedRegion(cluster, thisServer); 520 if (regionIndexToMove < 0) { 521 if (LOG.isDebugEnabled()) { 522 LOG.debug("CacheAwareSkewnessCandidateGenerator: No region found for movement"); 523 } 524 return BalanceAction.NULL_ACTION; 525 } 526 if (LOG.isDebugEnabled()) { 527 LOG.debug( 528 "CacheAwareSkewnessCandidateGenerator: Region {} moved from {} to {} as it is " 529 + "least cached on current server", 530 cluster.regions[regionIndexToMove].getEncodedName(), 531 cluster.servers[thisServer].getHostname(), cluster.servers[otherServer].getHostname()); 532 } 533 return getAction(thisServer, regionIndexToMove, otherServer, -1); 534 } 535 536 private int pickLeastCachedRegion(BalancerClusterState cluster, int thisServer) { 537 float minCacheRatio = Float.MAX_VALUE; 538 int leastCachedRegion = -1; 539 for (int i = 0; i < cluster.regionsPerServer[thisServer].length; i++) { 540 int regionIndex = cluster.regionsPerServer[thisServer][i]; 541 542 float cacheRatioOnCurrentServer = 543 cluster.getOrComputeRegionCacheRatio(regionIndex, thisServer); 544 if (cacheRatioOnCurrentServer < minCacheRatio) { 545 minCacheRatio = cacheRatioOnCurrentServer; 546 leastCachedRegion = regionIndex; 547 } 548 } 549 return leastCachedRegion; 550 } 551 } 552 553 static class CacheAwareRegionSkewnessCostFunction extends CostFunction { 554 static final String REGION_COUNT_SKEW_COST_KEY = 555 "hbase.master.balancer.stochastic.regionCountCost"; 556 static final float DEFAULT_REGION_COUNT_SKEW_COST = 20; 557 private final DoubleArrayCost cost = new DoubleArrayCost(); 558 559 CacheAwareRegionSkewnessCostFunction(Configuration conf) { 560 // Load multiplier should be the greatest as it is the most general way to balance data. 561 this.setMultiplier(conf.getFloat(REGION_COUNT_SKEW_COST_KEY, DEFAULT_REGION_COUNT_SKEW_COST)); 562 } 563 564 @Override 565 void prepare(BalancerClusterState cluster) { 566 super.prepare(cluster); 567 cost.prepare(cluster.numServers); 568 cost.applyCostsChange(costs -> { 569 for (int i = 0; i < cluster.numServers; i++) { 570 costs[i] = cluster.regionsPerServer[i].length; 571 } 572 }); 573 } 574 575 @Override 576 protected double cost() { 577 return cost.cost(); 578 } 579 580 @Override 581 protected void regionMoved(int region, int oldServer, int newServer) { 582 cost.applyCostsChange(costs -> { 583 costs[oldServer] = cluster.regionsPerServer[oldServer].length; 584 costs[newServer] = cluster.regionsPerServer[newServer].length; 585 }); 586 } 587 588 @Override 589 public final void updateWeight(Map<Class<? extends CandidateGenerator>, Double> weights) { 590 weights.merge(CacheAwareSkewnessCandidateGenerator.class, cost(), Double::sum); 591 } 592 } 593 594 class CacheAwareCostFunction extends CostFunction { 595 private static final String CACHE_COST_KEY = "hbase.master.balancer.stochastic.cacheCost"; 596 private double cacheRatio; 597 private double bestCacheRatio; 598 private final float lowCacheRatioThreshold; 599 private final float potentialCacheRatioAfterMove; 600 private final float minFreeCacheSpaceFactor; 601 602 private static final float DEFAULT_CACHE_COST = 20; 603 604 CacheAwareCostFunction(Configuration conf) { 605 boolean isPersistentCache = conf.get(BUCKET_CACHE_PERSISTENT_PATH_KEY) != null; 606 // Disable the CacheAwareCostFunction if the cached file list persistence is not enabled 607 this.setMultiplier( 608 !isPersistentCache ? 0.0f : conf.getFloat(CACHE_COST_KEY, DEFAULT_CACHE_COST)); 609 bestCacheRatio = 0.0; 610 cacheRatio = 0.0; 611 lowCacheRatioThreshold = 612 conf.getFloat(LOW_CACHE_RATIO_FOR_RELOCATION_KEY, LOW_CACHE_RATIO_FOR_RELOCATION_DEFAULT); 613 potentialCacheRatioAfterMove = Math.min(1.0f, conf 614 .getFloat(POTENTIAL_CACHE_RATIO_AFTER_MOVE_KEY, POTENTIAL_CACHE_RATIO_AFTER_MOVE_DEFAULT)); 615 minFreeCacheSpaceFactor = 616 conf.getFloat(MIN_FREE_CACHE_SPACE_FACTOR_KEY, MIN_FREE_CACHE_SPACE_FACTOR_DEFAULT); 617 } 618 619 @Override 620 void prepare(BalancerClusterState cluster) { 621 super.prepare(cluster); 622 recomputeCacheRatio(cluster); 623 if (LOG.isDebugEnabled()) { 624 LOG.debug("CacheAwareCostFunction: Cost: {}", 1 - cacheRatio); 625 } 626 } 627 628 private void recomputeCacheRatio(BalancerClusterState cluster) { 629 double[] currentWeighted = computeCurrentWeightedContributions(cluster); 630 double currentSum = 0.0; 631 double bestCacheSum = 0.0; 632 for (int region = 0; region < cluster.numRegions; region++) { 633 currentSum += currentWeighted[region]; 634 // here we only get the server index where this region cache ratio is the highest 635 int serverIndexBestCache = cluster.getOrComputeServerWithBestRegionCachedRatio()[region]; 636 // get the highest cacheRatio for this region on the current state of allocations 637 double currentHighestCache = 638 cluster.getOrComputeWeightedRegionCacheRatio(region, serverIndexBestCache); 639 // Get a hypothetical best cache ratio for this region if any server has enough free cache 640 // to host it. 641 double potentialHighestCache = potentialBestWeightedFromFreeCache(cluster, region); 642 bestCacheSum += Math.max(currentHighestCache, potentialHighestCache); 643 } 644 bestCacheRatio = bestCacheSum; 645 if (bestCacheSum <= 0.0) { 646 cacheRatio = cluster.numRegions == 0 ? 1.0 : 0.0; 647 } else { 648 cacheRatio = Math.min(1.0, currentSum / bestCacheSum); 649 } 650 } 651 652 private double[] computeCurrentWeightedContributions(BalancerClusterState cluster) { 653 int totalRegions = cluster.numRegions; 654 double[] contrib = new double[totalRegions]; 655 for (int r = 0; r < totalRegions; r++) { 656 int s = cluster.regionIndexToServerIndex[r]; 657 int sizeMb = cluster.getRegionSizeMinusColdDataMB(r); 658 if (sizeMb <= 0) { 659 contrib[r] = 0.0; 660 continue; 661 } 662 boolean movedInSimulation = cluster.initialRegionIndexToServerIndex[r] != s; 663 if ( 664 cluster.serverBlockCacheFreeSize != null && movedInSimulation 665 && cluster.getSumRegionCacheAndColdDataRatio(r) < lowCacheRatioThreshold 666 ) { 667 LOG.debug("Region {} is simulated moved to new server {}", 668 cluster.regions[r].getEncodedName(), cluster.servers[s].getHostname()); 669 long bytesNeeded = (long) (sizeMb * 1024L * 1024L * minFreeCacheSpaceFactor); 670 if (cluster.serverBlockCacheFreeSize[s] >= bytesNeeded) { 671 contrib[r] = sizeMb * potentialCacheRatioAfterMove; 672 continue; 673 } 674 } 675 contrib[r] = cluster.getOrComputeWeightedRegionCacheRatio(r, s); 676 } 677 return contrib; 678 } 679 680 /* 681 * If this region is cold in metrics and at least one RS (including its current host) reports 682 * enough free block cache to hold it, return an optimistic weighted cache score ({@link 683 * #potentialCacheRatioAfterMove} * region MB) so placement is not considered optimal solely 684 * from low ratios when capacity exists somewhere in the cluster. 685 */ 686 private double potentialBestWeightedFromFreeCache(BalancerClusterState cluster, int region) { 687 if (cluster.serverBlockCacheFreeSize == null) { 688 return 0.0; 689 } 690 float observedRatio = cluster.getSumRegionCacheAndColdDataRatio(region); 691 if (observedRatio >= lowCacheRatioThreshold) { 692 return 0.0; 693 } 694 int regionSizeMb = cluster.getRegionSizeMinusColdDataMB(region); 695 if (regionSizeMb <= 0) { 696 return 0.0; 697 } 698 long regionSizeBytes = (long) regionSizeMb * 1024L * 1024L; 699 long requiredFree = (long) (regionSizeBytes * minFreeCacheSpaceFactor); 700 for (int s = 0; s < cluster.numServers; s++) { 701 if (cluster.serverBlockCacheFreeSize[s] >= requiredFree) { 702 return regionSizeMb * potentialCacheRatioAfterMove; 703 } 704 } 705 return 0.0; 706 } 707 708 @Override 709 protected double cost() { 710 return scale(0, 1, 1 - cacheRatio); 711 } 712 713 @Override 714 protected void regionMoved(int region, int oldServer, int newServer) { 715 double regionCacheRatioOnOldServer = 716 cluster.getOrComputeWeightedRegionCacheRatio(region, oldServer); 717 if (simulatedRatio.equals(BigDecimal.ZERO)) { 718 double potentialCachedSizeOnNewServer = 719 cluster.getRegionSizeMinusColdDataMB(region) * potentialCacheRatioAfterMove; 720 long potentialCachedBytesOnNewServer = 721 (long) (potentialCachedSizeOnNewServer * 1024L * 1024L); 722 boolean simulateCacheBasedOnFreeSpace = cluster.serverBlockCacheFreeSize != null 723 && cluster.getOrComputeRegionCacheRatio(region, oldServer) < lowCacheRatioThreshold 724 && cluster.serverBlockCacheFreeSize[newServer] >= potentialCachedBytesOnNewServer; 725 double regionCacheRatioOnNewServer = simulateCacheBasedOnFreeSpace 726 ? potentialCachedSizeOnNewServer 727 : cluster.getOrComputeWeightedRegionCacheRatio(region, newServer); 728 double cacheRatioDiff = regionCacheRatioOnNewServer - regionCacheRatioOnOldServer; 729 double normalizedDelta = bestCacheRatio == 0.0 ? 0.0 : cacheRatioDiff / bestCacheRatio; 730 LOG.debug( 731 "simulating moving region {} using simulateCacheBasedOnFreeSpace={} " 732 + "got a normalized delta of {} to be added to cacheRatio: {}", 733 cluster.regions[region].getEncodedName(), simulateCacheBasedOnFreeSpace, normalizedDelta, 734 cacheRatio); 735 simulatedRatio = BigDecimal.valueOf(normalizedDelta); 736 cacheRatio += normalizedDelta; 737 if (cacheRatio < 0.0 || cacheRatio > 1.0) { 738 LOG.info( 739 "Recomputing cacheRatio after calculating impact of region move: \n " 740 + "CacheAwareCostFunction:regionMoved:region:{}:from:{}:to:{}:" 741 + "regionCacheRatioOnOldServer:{}:regionCacheRatioOnNewServer:{}:" 742 + "bestRegionCacheRatio:{}:cacheRatio:{}", 743 cluster.regions[region].getEncodedName(), cluster.servers[oldServer].getHostname(), 744 cluster.servers[newServer].getHostname(), regionCacheRatioOnOldServer, 745 regionCacheRatioOnNewServer, bestCacheRatio, cacheRatio); 746 recomputeCacheRatio(cluster); 747 } 748 } else { 749 // This means we are in an undoAction call and need to reverse the cache delta applied in 750 // the region move simulation 751 cacheRatio -= simulatedRatio.doubleValue(); 752 } 753 } 754 755 private int getServerWithBestCacheRatioForRegion(int region) { 756 return cluster.getOrComputeServerWithBestRegionCachedRatio()[region]; 757 } 758 759 @Override 760 public void updateWeight(Map<Class<? extends CandidateGenerator>, Double> weights) { 761 weights.merge(CacheAwareCandidateGenerator.class, cost(), Double::sum); 762 } 763 } 764}