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}