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}