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}