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.mapreduce;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.HBaseConfiguration;
029import org.apache.hadoop.hbase.HConstants;
030import org.apache.hadoop.mapreduce.InputSplit;
031import org.apache.hadoop.mapreduce.JobContext;
032import org.apache.hadoop.mapreduce.JobID;
033import org.apache.hadoop.mapreduce.task.JobContextImpl;
034import org.apache.yetus.audience.InterfaceAudience;
035
036/**
037 * Process the return from super-class {@link TableInputFormat} (TIF) so as to undo any clumping of
038 * {@link InputSplit}s around RegionServers. Spread splits broadly to distribute read-load over
039 * RegionServers in the cluster. The super-class TIF returns splits in hbase:meta table order.
040 * Adjacent or near-adjacent hbase:meta Regions can be hosted on the same RegionServer -- nothing
041 * prevents this. This hbase:maeta ordering of InputSplit placement can be lumpy making it so some
042 * RegionServers end up hosting lots of InputSplit scans while contemporaneously other RegionServers
043 * host few or none. This class does a pass over the return from the super-class to better spread
044 * the load. See the below helpful Flipkart blog post for a description and from where the base of
045 * this code comes from (with permission).
046 * @see <a href=
047 *      "https://tech.flipkart.com/is-data-locality-always-out-of-the-box-in-hadoop-not-really-2ae9c95163cb">
048 *      Is data locality always out of the box in Hadoop? Not really</a>
049 */
050@InterfaceAudience.Public
051public class RoundRobinTableInputFormat extends TableInputFormat {
052  private Boolean hbaseRegionsizecalculatorEnableOriginalValue = null;
053  /**
054   * Boolean config for whether superclass should produce InputSplits with 'lengths'. If true, TIF
055   * will query every RegionServer to get the 'size' of all involved Regions and this 'size' will be
056   * used the the InputSplit length. If false, we skip this query and the super-classes returned
057   * InputSplits will have lenghths of zero. This override will set the flag to false. All returned
058   * lengths will be zero. Makes it so sorting on 'length' becomes a noop. The sort returned by this
059   * override will prevail. Thats what we want.
060   */
061  static String HBASE_REGIONSIZECALCULATOR_ENABLE = "hbase.regionsizecalculator.enable";
062
063  @Override
064  public List<InputSplit> getSplits(JobContext context) throws IOException {
065    try {
066      // Do a round robin on what we get back from the super-class.
067      configure();
068      return roundRobin(getSuperSplits(context));
069    } finally {
070      unconfigure();
071    }
072  }
073
074  /**
075   * Call super-classes' getSplits. Have it out here as its own method so can be overridden.
076   */
077  List<InputSplit> getSuperSplits(JobContext context) throws IOException {
078    return super.getSplits(context);
079  }
080
081  /**
082   * Spread the splits list so as to avoid clumping on RegionServers. Order splits so every server
083   * gets one split before a server gets a second, and so on; i.e. round-robin the splits amongst
084   * the servers in the cluster.
085   */
086  List<InputSplit> roundRobin(List<InputSplit> inputs) throws IOException {
087    if ((inputs == null) || inputs.isEmpty()) {
088      return inputs;
089    }
090    List<InputSplit> result = new ArrayList<>(inputs.size());
091    // Prepare a hashmap with each region server as key and list of Input Splits as value
092    Map<String, List<InputSplit>> regionServerSplits = new HashMap<>();
093    for (InputSplit is : inputs) {
094      if (is instanceof TableSplit) {
095        String regionServer = ((TableSplit) is).getRegionLocation();
096        if (regionServer != null && !StringUtils.isBlank(regionServer)) {
097          regionServerSplits.computeIfAbsent(regionServer, k -> new ArrayList<>()).add(is);
098          continue;
099        }
100      }
101      // If TableSplit or region server not found, add it anyways.
102      result.add(is);
103    }
104    // Write out splits in a manner that spreads splits for a RegionServer to avoid 'clumping'.
105    while (!regionServerSplits.isEmpty()) {
106      Iterator<List<InputSplit>> iter = regionServerSplits.values().iterator();
107      while (iter.hasNext()) {
108        List<InputSplit> inputSplitListForRegion = iter.next();
109        if (!inputSplitListForRegion.isEmpty()) {
110          result.add(inputSplitListForRegion.remove(0));
111        }
112        if (inputSplitListForRegion.isEmpty()) {
113          iter.remove();
114        }
115      }
116    }
117    return result;
118  }
119
120  /**
121   * Adds a configuration to the Context disabling remote rpc'ing to figure Region size when
122   * calculating InputSplits. See up in super-class TIF where we rpc to every server to find the
123   * size of all involved Regions. Here we disable this super-class action. This means InputSplits
124   * will have a length of zero. If all InputSplits have zero-length InputSplits, the ordering done
125   * in here will 'pass-through' Hadoop's length-first sort. The superclass TIF will ask every node
126   * for the current size of each of the participating Table Regions. It does this because it wants
127   * to schedule the biggest Regions first (This fixation comes of hadoop itself -- see JobSubmitter
128   * where it sorts inputs by size). This extra diligence takes time and is of no utility in this
129   * RRTIF where spread is of more import than size-first. Also, if a rolling restart is happening
130   * when we go to launch the job, the job launch may fail because the request for Region size fails
131   * -- even after retries -- because rolled RegionServer may take a while to come online: e.g. it
132   * takes java 90 seconds to allocate a 160G. RegionServer is offline during this time. The job
133   * launch will fail with 'Connection rejected'. So, we set 'hbase.regionsizecalculator.enable' to
134   * false here in RRTIF.
135   * @see #unconfigure()
136   */
137  void configure() {
138    if (getConf().get(HBASE_REGIONSIZECALCULATOR_ENABLE) != null) {
139      this.hbaseRegionsizecalculatorEnableOriginalValue =
140        getConf().getBoolean(HBASE_REGIONSIZECALCULATOR_ENABLE, true);
141    }
142    getConf().setBoolean(HBASE_REGIONSIZECALCULATOR_ENABLE, false);
143  }
144
145  /**
146   * @see #configure()
147   */
148  void unconfigure() {
149    if (this.hbaseRegionsizecalculatorEnableOriginalValue == null) {
150      getConf().unset(HBASE_REGIONSIZECALCULATOR_ENABLE);
151    } else {
152      getConf().setBoolean(HBASE_REGIONSIZECALCULATOR_ENABLE,
153        this.hbaseRegionsizecalculatorEnableOriginalValue);
154    }
155  }
156
157  /**
158   * Pass table name as argument. Set the zk ensemble to use with the System property
159   * 'hbase.zookeeper.quorum'
160   */
161  public static void main(String[] args) throws IOException {
162    TableInputFormat tif = new RoundRobinTableInputFormat();
163    final Configuration configuration = HBaseConfiguration.create();
164    configuration.setBoolean("hbase.regionsizecalculator.enable", false);
165    configuration.set(HConstants.ZOOKEEPER_QUORUM,
166      System.getProperty(HConstants.ZOOKEEPER_QUORUM, "localhost"));
167    configuration.set(TableInputFormat.INPUT_TABLE, args[0]);
168    tif.setConf(configuration);
169    List<InputSplit> splits = tif.getSplits(new JobContextImpl(configuration, new JobID()));
170    for (InputSplit split : splits) {
171      System.out.println(split);
172    }
173  }
174}