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 */
018
019package org.apache.hadoop.hbase.rsgroup;
020
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.TreeMap;
031
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.ClusterMetrics;
034import org.apache.hadoop.hbase.HBaseIOException;
035import org.apache.hadoop.hbase.HConstants;
036import org.apache.hadoop.hbase.ServerName;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.constraint.ConstraintException;
040import org.apache.hadoop.hbase.master.LoadBalancer;
041import org.apache.hadoop.hbase.master.MasterServices;
042import org.apache.hadoop.hbase.master.RegionPlan;
043import org.apache.hadoop.hbase.master.balancer.StochasticLoadBalancer;
044import org.apache.hadoop.hbase.net.Address;
045import org.apache.hadoop.util.ReflectionUtils;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
051import org.apache.hbase.thirdparty.com.google.common.collect.ArrayListMultimap;
052import org.apache.hbase.thirdparty.com.google.common.collect.LinkedListMultimap;
053import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap;
054import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
055import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
056
057/**
058 * GroupBasedLoadBalancer, used when Region Server Grouping is configured (HBase-6721)
059 * It does region balance based on a table's group membership.
060 *
061 * Most assignment methods contain two exclusive code paths: Online - when the group
062 * table is online and Offline - when it is unavailable.
063 *
064 * During Offline, assignments are assigned based on cached information in zookeeper.
065 * If unavailable (ie bootstrap) then regions are assigned randomly.
066 *
067 * Once the GROUP table has been assigned, the balancer switches to Online and will then
068 * start providing appropriate assignments for user tables.
069 *
070 */
071@InterfaceAudience.Private
072public class RSGroupBasedLoadBalancer implements RSGroupableBalancer {
073  private static final Logger LOG = LoggerFactory.getLogger(RSGroupBasedLoadBalancer.class);
074
075  private Configuration config;
076  private ClusterMetrics clusterStatus;
077  private MasterServices masterServices;
078  private volatile RSGroupInfoManager rsGroupInfoManager;
079  private LoadBalancer internalBalancer;
080
081  /**
082   * Used by reflection in {@link org.apache.hadoop.hbase.master.balancer.LoadBalancerFactory}.
083   */
084  @InterfaceAudience.Private
085  public RSGroupBasedLoadBalancer() {}
086
087  @Override
088  public Configuration getConf() {
089    return config;
090  }
091
092  @Override
093  public void setConf(Configuration conf) {
094    this.config = conf;
095  }
096
097  @Override
098  public void setClusterMetrics(ClusterMetrics sm) {
099    this.clusterStatus = sm;
100  }
101
102  @Override
103  public void setMasterServices(MasterServices masterServices) {
104    this.masterServices = masterServices;
105  }
106
107  @Override
108  public List<RegionPlan> balanceCluster(TableName tableName, Map<ServerName, List<RegionInfo>>
109      clusterState) throws HBaseIOException {
110    return balanceCluster(clusterState);
111  }
112
113  @Override
114  public List<RegionPlan> balanceCluster(Map<ServerName, List<RegionInfo>> clusterState)
115      throws HBaseIOException {
116    if (!isOnline()) {
117      throw new ConstraintException(RSGroupInfoManager.RSGROUP_TABLE_NAME +
118          " is not online, unable to perform balance");
119    }
120
121    Map<ServerName,List<RegionInfo>> correctedState = correctAssignments(clusterState);
122    List<RegionPlan> regionPlans = new ArrayList<>();
123
124    List<RegionInfo> misplacedRegions = correctedState.get(LoadBalancer.BOGUS_SERVER_NAME);
125    for (RegionInfo regionInfo : misplacedRegions) {
126      ServerName serverName = findServerForRegion(clusterState, regionInfo);
127      regionPlans.add(new RegionPlan(regionInfo, serverName, null));
128    }
129    try {
130      // Record which region servers have been processed,so as to skip them after processed
131      HashSet<ServerName> processedServers = new HashSet<>();
132
133      // For each rsgroup
134      for (RSGroupInfo rsgroup : rsGroupInfoManager.listRSGroups()) {
135        Map<ServerName, List<RegionInfo>> groupClusterState = new HashMap<>();
136        Map<TableName, Map<ServerName, List<RegionInfo>>> groupClusterLoad = new HashMap<>();
137        for (ServerName server : clusterState.keySet()) { // for each region server
138          if (!processedServers.contains(server) // server is not processed yet
139              && rsgroup.containsServer(server.getAddress())) { // server belongs to this rsgroup
140            List<RegionInfo> regionsOnServer = correctedState.get(server);
141            groupClusterState.put(server, regionsOnServer);
142
143            processedServers.add(server);
144          }
145        }
146
147        groupClusterLoad.put(HConstants.ENSEMBLE_TABLE_NAME, groupClusterState);
148        this.internalBalancer.setClusterLoad(groupClusterLoad);
149        List<RegionPlan> groupPlans = this.internalBalancer
150            .balanceCluster(groupClusterState);
151        if (groupPlans != null) {
152          regionPlans.addAll(groupPlans);
153        }
154      }
155    } catch (IOException exp) {
156      LOG.warn("Exception while balancing cluster.", exp);
157      regionPlans.clear();
158    }
159    return regionPlans;
160  }
161
162  @Override
163  public Map<ServerName, List<RegionInfo>> roundRobinAssignment(
164      List<RegionInfo> regions, List<ServerName> servers) throws HBaseIOException {
165    Map<ServerName, List<RegionInfo>> assignments = Maps.newHashMap();
166    ListMultimap<String,RegionInfo> regionMap = ArrayListMultimap.create();
167    ListMultimap<String,ServerName> serverMap = ArrayListMultimap.create();
168    generateGroupMaps(regions, servers, regionMap, serverMap);
169    for(String groupKey : regionMap.keySet()) {
170      if (regionMap.get(groupKey).size() > 0) {
171        Map<ServerName, List<RegionInfo>> result =
172            this.internalBalancer.roundRobinAssignment(
173                regionMap.get(groupKey),
174                serverMap.get(groupKey));
175        if(result != null) {
176          if(result.containsKey(LoadBalancer.BOGUS_SERVER_NAME) &&
177              assignments.containsKey(LoadBalancer.BOGUS_SERVER_NAME)){
178            assignments.get(LoadBalancer.BOGUS_SERVER_NAME).addAll(
179              result.get(LoadBalancer.BOGUS_SERVER_NAME));
180          } else {
181            assignments.putAll(result);
182          }
183        }
184      }
185    }
186    return assignments;
187  }
188
189  @Override
190  public Map<ServerName, List<RegionInfo>> retainAssignment(
191      Map<RegionInfo, ServerName> regions, List<ServerName> servers) throws HBaseIOException {
192    try {
193      Map<ServerName, List<RegionInfo>> assignments = new TreeMap<>();
194      ListMultimap<String, RegionInfo> groupToRegion = ArrayListMultimap.create();
195      Set<RegionInfo> misplacedRegions = getMisplacedRegions(regions);
196      for (RegionInfo region : regions.keySet()) {
197        if (!misplacedRegions.contains(region)) {
198          String groupName = rsGroupInfoManager.getRSGroupOfTable(region.getTable());
199          if (groupName == null) {
200            LOG.info("Group not found for table " + region.getTable() + ", using default");
201            groupName = RSGroupInfo.DEFAULT_GROUP;
202          }
203          groupToRegion.put(groupName, region);
204        }
205      }
206      // Now the "groupToRegion" map has only the regions which have correct
207      // assignments.
208      for (String key : groupToRegion.keySet()) {
209        Map<RegionInfo, ServerName> currentAssignmentMap = new TreeMap<RegionInfo, ServerName>();
210        List<RegionInfo> regionList = groupToRegion.get(key);
211        RSGroupInfo info = rsGroupInfoManager.getRSGroup(key);
212        List<ServerName> candidateList = filterOfflineServers(info, servers);
213        for (RegionInfo region : regionList) {
214          currentAssignmentMap.put(region, regions.get(region));
215        }
216        if(candidateList.size() > 0) {
217          assignments.putAll(this.internalBalancer.retainAssignment(
218              currentAssignmentMap, candidateList));
219        }
220      }
221
222      for (RegionInfo region : misplacedRegions) {
223        String groupName = rsGroupInfoManager.getRSGroupOfTable(region.getTable());
224        if (groupName == null) {
225          LOG.info("Group not found for table " + region.getTable() + ", using default");
226          groupName = RSGroupInfo.DEFAULT_GROUP;
227        }
228        RSGroupInfo info = rsGroupInfoManager.getRSGroup(groupName);
229        List<ServerName> candidateList = filterOfflineServers(info, servers);
230        ServerName server = this.internalBalancer.randomAssignment(region,
231            candidateList);
232        if (server != null) {
233          if (!assignments.containsKey(server)) {
234            assignments.put(server, new ArrayList<>());
235          }
236          assignments.get(server).add(region);
237        } else {
238          //if not server is available assign to bogus so it ends up in RIT
239          if(!assignments.containsKey(LoadBalancer.BOGUS_SERVER_NAME)) {
240            assignments.put(LoadBalancer.BOGUS_SERVER_NAME, new ArrayList<>());
241          }
242          assignments.get(LoadBalancer.BOGUS_SERVER_NAME).add(region);
243        }
244      }
245      return assignments;
246    } catch (IOException e) {
247      throw new HBaseIOException("Failed to do online retain assignment", e);
248    }
249  }
250
251  @Override
252  public ServerName randomAssignment(RegionInfo region,
253      List<ServerName> servers) throws HBaseIOException {
254    ListMultimap<String,RegionInfo> regionMap = LinkedListMultimap.create();
255    ListMultimap<String,ServerName> serverMap = LinkedListMultimap.create();
256    generateGroupMaps(Lists.newArrayList(region), servers, regionMap, serverMap);
257    List<ServerName> filteredServers = serverMap.get(regionMap.keySet().iterator().next());
258    return this.internalBalancer.randomAssignment(region, filteredServers);
259  }
260
261  private void generateGroupMaps(
262    List<RegionInfo> regions,
263    List<ServerName> servers,
264    ListMultimap<String, RegionInfo> regionMap,
265    ListMultimap<String, ServerName> serverMap) throws HBaseIOException {
266    try {
267      for (RegionInfo region : regions) {
268        String groupName = rsGroupInfoManager.getRSGroupOfTable(region.getTable());
269        if (groupName == null) {
270          LOG.info("Group not found for table " + region.getTable() + ", using default");
271          groupName = RSGroupInfo.DEFAULT_GROUP;
272        }
273        regionMap.put(groupName, region);
274      }
275      for (String groupKey : regionMap.keySet()) {
276        RSGroupInfo info = rsGroupInfoManager.getRSGroup(groupKey);
277        serverMap.putAll(groupKey, filterOfflineServers(info, servers));
278        if(serverMap.get(groupKey).size() < 1) {
279          serverMap.put(groupKey, LoadBalancer.BOGUS_SERVER_NAME);
280        }
281      }
282    } catch(IOException e) {
283      throw new HBaseIOException("Failed to generate group maps", e);
284    }
285  }
286
287  private List<ServerName> filterOfflineServers(RSGroupInfo RSGroupInfo,
288                                                List<ServerName> onlineServers) {
289    if (RSGroupInfo != null) {
290      return filterServers(RSGroupInfo.getServers(), onlineServers);
291    } else {
292      LOG.warn("RSGroup Information found to be null. Some regions might be unassigned.");
293      return Collections.EMPTY_LIST;
294    }
295  }
296
297  /**
298   * Filter servers based on the online servers.
299   *
300   * @param servers
301   *          the servers
302   * @param onlineServers
303   *          List of servers which are online.
304   * @return the list
305   */
306  private List<ServerName> filterServers(Set<Address> servers,
307                                         List<ServerName> onlineServers) {
308    /**
309     * servers is actually a TreeSet (see {@link org.apache.hadoop.hbase.rsgroup.RSGroupInfo}),
310     * having its contains()'s time complexity as O(logn), which is good enough.
311     * TODO: consider using HashSet to pursue O(1) for contains() throughout the calling chain
312     * if needed. */
313    ArrayList<ServerName> finalList = new ArrayList<>();
314    for (ServerName onlineServer : onlineServers) {
315      if (servers.contains(onlineServer.getAddress())) {
316        finalList.add(onlineServer);
317      }
318    }
319
320    return finalList;
321  }
322
323  @VisibleForTesting
324  public Set<RegionInfo> getMisplacedRegions(
325      Map<RegionInfo, ServerName> regions) throws IOException {
326    Set<RegionInfo> misplacedRegions = new HashSet<>();
327    for(Map.Entry<RegionInfo, ServerName> region : regions.entrySet()) {
328      RegionInfo regionInfo = region.getKey();
329      ServerName assignedServer = region.getValue();
330      String groupName = rsGroupInfoManager.getRSGroupOfTable(regionInfo.getTable());
331      if (groupName == null) {
332        LOG.info("Group not found for table " + regionInfo.getTable() + ", using default");
333        groupName = RSGroupInfo.DEFAULT_GROUP;
334      }
335      RSGroupInfo info = rsGroupInfoManager.getRSGroup(groupName);
336      if (assignedServer == null) {
337        LOG.debug("There is no assigned server for {}", region);
338        continue;
339      }
340      RSGroupInfo otherInfo = rsGroupInfoManager.getRSGroupOfServer(assignedServer.getAddress());
341      if (info == null && otherInfo == null) {
342        LOG.warn("Couldn't obtain rs group information for {} on {}", region, assignedServer);
343        continue;
344      }
345      if ((info == null || !info.containsServer(assignedServer.getAddress()))) {
346        LOG.debug("Found misplaced region: " + regionInfo.getRegionNameAsString() +
347            " on server: " + assignedServer +
348            " found in group: " +  otherInfo +
349            " outside of group: " + (info == null ? "UNKNOWN" : info.getName()));
350        misplacedRegions.add(regionInfo);
351      }
352    }
353    return misplacedRegions;
354  }
355
356  private ServerName findServerForRegion(
357      Map<ServerName, List<RegionInfo>> existingAssignments, RegionInfo region) {
358    for (Map.Entry<ServerName, List<RegionInfo>> entry : existingAssignments.entrySet()) {
359      if (entry.getValue().contains(region)) {
360        return entry.getKey();
361      }
362    }
363
364    throw new IllegalStateException("Could not find server for region "
365        + region.getShortNameToLog());
366  }
367
368  private Map<ServerName, List<RegionInfo>> correctAssignments(
369       Map<ServerName, List<RegionInfo>> existingAssignments)
370  throws HBaseIOException{
371    Map<ServerName, List<RegionInfo>> correctAssignments = new TreeMap<>();
372    correctAssignments.put(LoadBalancer.BOGUS_SERVER_NAME, new LinkedList<>());
373    for (Map.Entry<ServerName, List<RegionInfo>> assignments : existingAssignments.entrySet()){
374      ServerName sName = assignments.getKey();
375      correctAssignments.put(sName, new LinkedList<>());
376      List<RegionInfo> regions = assignments.getValue();
377      for (RegionInfo region : regions) {
378        RSGroupInfo targetRSGInfo = null;
379        try {
380          String groupName = rsGroupInfoManager.getRSGroupOfTable(region.getTable());
381          if (groupName == null) {
382            LOG.info("Group not found for table " + region.getTable() + ", using default");
383            groupName = RSGroupInfo.DEFAULT_GROUP;
384          }
385          targetRSGInfo = rsGroupInfoManager.getRSGroup(groupName);
386        } catch (IOException exp) {
387          LOG.debug("RSGroup information null for region of table " + region.getTable(),
388              exp);
389        }
390        if ((targetRSGInfo == null) || (!targetRSGInfo.containsServer(sName.getAddress()))) {
391          correctAssignments.get(LoadBalancer.BOGUS_SERVER_NAME).add(region);
392        } else {
393          correctAssignments.get(sName).add(region);
394        }
395      }
396    }
397    return correctAssignments;
398  }
399
400  @Override
401  public void initialize() throws HBaseIOException {
402    try {
403      if (rsGroupInfoManager == null) {
404        List<RSGroupAdminEndpoint> cps =
405          masterServices.getMasterCoprocessorHost().findCoprocessors(RSGroupAdminEndpoint.class);
406        if (cps.size() != 1) {
407          String msg = "Expected one implementation of GroupAdminEndpoint but found " + cps.size();
408          LOG.error(msg);
409          throw new HBaseIOException(msg);
410        }
411        rsGroupInfoManager = cps.get(0).getGroupInfoManager();
412        if(rsGroupInfoManager == null){
413          String msg = "RSGroupInfoManager hasn't been initialized";
414          LOG.error(msg);
415          throw new HBaseIOException(msg);
416        }
417        rsGroupInfoManager.start();
418      }
419    } catch (IOException e) {
420      throw new HBaseIOException("Failed to initialize GroupInfoManagerImpl", e);
421    }
422
423    // Create the balancer
424    Class<? extends LoadBalancer> balancerKlass = config.getClass(HBASE_RSGROUP_LOADBALANCER_CLASS,
425        StochasticLoadBalancer.class, LoadBalancer.class);
426    internalBalancer = ReflectionUtils.newInstance(balancerKlass, config);
427    internalBalancer.setMasterServices(masterServices);
428    internalBalancer.setClusterMetrics(clusterStatus);
429    internalBalancer.setConf(config);
430    internalBalancer.initialize();
431  }
432
433  public boolean isOnline() {
434    if (this.rsGroupInfoManager == null) {
435      return false;
436    }
437
438    return this.rsGroupInfoManager.isOnline();
439  }
440
441  @Override
442  public void setClusterLoad(Map<TableName, Map<ServerName, List<RegionInfo>>> clusterLoad) {
443  }
444
445  @Override
446  public void regionOnline(RegionInfo regionInfo, ServerName sn) {
447  }
448
449  @Override
450  public void regionOffline(RegionInfo regionInfo) {
451  }
452
453  @Override
454  public void onConfigurationChange(Configuration conf) {
455    //DO nothing for now
456  }
457
458  @Override
459  public void stop(String why) {
460  }
461
462  @Override
463  public boolean isStopped() {
464    return false;
465  }
466
467  @VisibleForTesting
468  public void setRsGroupInfoManager(RSGroupInfoManager rsGroupInfoManager) {
469    this.rsGroupInfoManager = rsGroupInfoManager;
470  }
471
472  @Override
473  public void postMasterStartupInitialize() {
474    this.internalBalancer.postMasterStartupInitialize();
475  }
476
477  public void updateBalancerStatus(boolean status) {
478    internalBalancer.updateBalancerStatus(status);
479  }
480}