我们在MapReduce中TextInputFormat分片和读取分片数据源码级分析 这篇中以TextInputFormat为例讲解了InputFormat的分片过程以及RecordReader读取分片数据的过程。接下来咱们分析TableInputFormat的分片信息和数据读取过程。

  TableInputFormat这是专门处理基于HBase的MapReduce的输入数据的格式类。我们可以看看继承结构:(1)public class TableInputFormat extends TableInputFormatBase implements Configurable;(2)public abstract class TableInputFormatBase extends InputFormat<ImmutableBytesWritable, Result>。其中InputFormat是输入格式的基类。

  TableInputFormat类主要是构造HTable对象和Scan对象,主要在方法setConf(Configuration configuration)构造,代码如下:

 /**
* Sets the configuration. This is used to set the details for the table to
* be scanned.
*
* @param configuration The configuration to set.
* @see org.apache.hadoop.conf.Configurable#setConf(
* org.apache.hadoop.conf.Configuration)
*/
@Override
public void setConf(Configuration configuration) {
this.conf = configuration;
String tableName = conf.get(INPUT_TABLE);
try {
setHTable(new HTable(new Configuration(conf), tableName));
} catch (Exception e) {
LOG.error(StringUtils.stringifyException(e));
} Scan scan = null; if (conf.get(SCAN) != null) {
try {
scan = TableMapReduceUtil.convertStringToScan(conf.get(SCAN));
} catch (IOException e) {
LOG.error("An error occurred.", e);
}
} else {
try {
scan = new Scan(); if (conf.get(SCAN_ROW_START) != null) {
scan.setStartRow(Bytes.toBytes(conf.get(SCAN_ROW_START)));
} if (conf.get(SCAN_ROW_STOP) != null) {
scan.setStopRow(Bytes.toBytes(conf.get(SCAN_ROW_STOP)));
} if (conf.get(SCAN_COLUMNS) != null) {
addColumns(scan, conf.get(SCAN_COLUMNS));
} if (conf.get(SCAN_COLUMN_FAMILY) != null) {
scan.addFamily(Bytes.toBytes(conf.get(SCAN_COLUMN_FAMILY)));
} if (conf.get(SCAN_TIMESTAMP) != null) {
scan.setTimeStamp(Long.parseLong(conf.get(SCAN_TIMESTAMP)));
} if (conf.get(SCAN_TIMERANGE_START) != null && conf.get(SCAN_TIMERANGE_END) != null) {
scan.setTimeRange(
Long.parseLong(conf.get(SCAN_TIMERANGE_START)),
Long.parseLong(conf.get(SCAN_TIMERANGE_END)));
} if (conf.get(SCAN_MAXVERSIONS) != null) {
scan.setMaxVersions(Integer.parseInt(conf.get(SCAN_MAXVERSIONS)));
} if (conf.get(SCAN_CACHEDROWS) != null) {
scan.setCaching(Integer.parseInt(conf.get(SCAN_CACHEDROWS)));
} // false by default, full table scans generate too much BC churn
scan.setCacheBlocks((conf.getBoolean(SCAN_CACHEBLOCKS, false)));
} catch (Exception e) {
LOG.error(StringUtils.stringifyException(e));
}
} setScan(scan);
}

  首先会通过配置信息获取HBase表名,然后构造一个HTable对象;如果用户自己的作业中有配置Scan的话,就会解码Scan字符串转换为Scan对象;如果用户没配置Scan就会创建一个默认的Scan,进行一些基本配置。

  关于TableInputFormatBase,我们重点还是讲两个方法:RecordReader<ImmutableBytesWritable, Result> createRecordReader(InputSplit split, TaskAttemptContext context)方法和List<InputSplit> getSplits(JobContext context)方法,前者是读取分片信息所指的数据供TableMapper处理,后者是构造HBase表的分片信息。这里的分片信息是TableSplit extends InputSplit implements Writable, Comparable,这个TableSpli维护4个字段,HBase表名:byte [] tableName、scan起始rowkey:byte [] startRow、scan结束rowkey:byte [] endRow、以及该region所在节点:String regionLocation。

  一、先看getSplits方法吧,这里一个split一般对应一个完整region,除非用户设定的开始和结束rowkey不是region的边界,代码如下:

   /**
* Calculates the splits that will serve as input for the map tasks. The
* number of splits matches the number of regions in a table.
*
* @param context The current job context.
* @return The list of input splits.
* @throws IOException When creating the list of splits fails.
* @see org.apache.hadoop.mapreduce.InputFormat#getSplits(
* org.apache.hadoop.mapreduce.JobContext)
*/
@Override
public List<InputSplit> getSplits(JobContext context) throws IOException {
if (table == null) {
throw new IOException("No table was provided.");
}
// Get the name server address and the default value is null.
this.nameServer =
context.getConfiguration().get("hbase.nameserver.address", null); Pair<byte[][], byte[][]> keys = table.getStartEndKeys();//获取所有Region的开始rowkey和结束rowkey
if (keys == null || keys.getFirst() == null ||
keys.getFirst().length == 0) { //
//table的第一个region的startKey必须是EMPTY_BYTE_ARRAY,否则输出FIRST_REGION_STARTKEY_NOT_EMPTY信息
HRegionLocation regLoc = table.getRegionLocation(
HConstants.EMPTY_BYTE_ARRAY, false);
if (null == regLoc) { //一个region也没有
throw new IOException("Expecting at least one region.");
}
List<InputSplit> splits = new ArrayList<InputSplit>(1);
//构造一个TableSplit,起始rowkey和结束rowkey都是EMPTY_BYTE_ARRAY
InputSplit split = new TableSplit(table.getTableName(),
HConstants.EMPTY_BYTE_ARRAY, HConstants.EMPTY_BYTE_ARRAY, regLoc
.getHostnamePort().split(Addressing.HOSTNAME_PORT_SEPARATOR)[0]);
splits.add(split);
return splits; //返回分片信息
}
List<InputSplit> splits = new ArrayList<InputSplit>(keys.getFirst().length);
for (int i = 0; i < keys.getFirst().length; i++) { //有多个region
if ( !includeRegionInSplit(keys.getFirst()[i], keys.getSecond()[i])) { //这个方法一直返回true
continue;
}
HServerAddress regionServerAddress =
table.getRegionLocation(keys.getFirst()[i]).getServerAddress();
InetAddress regionAddress =
regionServerAddress.getInetSocketAddress().getAddress();//获取region所在地址
String regionLocation;
try {
regionLocation = reverseDNS(regionAddress);//将地址转换为字符串的主机名
} catch (NamingException e) {
LOG.error("Cannot resolve the host name for " + regionAddress +
" because of " + e);
regionLocation = regionServerAddress.getHostname();
} byte[] startRow = scan.getStartRow(); //获取scan的开始rowkey
byte[] stopRow = scan.getStopRow(); //获取scan的结束rowkey
// determine if the given start an stop key fall into the region
//比较用户设定的rowkey范围在那些region之中
if ((startRow.length == 0 || keys.getSecond()[i].length == 0 ||
Bytes.compareTo(startRow, keys.getSecond()[i]) < 0) &&
(stopRow.length == 0 ||
Bytes.compareTo(stopRow, keys.getFirst()[i]) > 0)) {
byte[] splitStart = startRow.length == 0 ||
Bytes.compareTo(keys.getFirst()[i], startRow) >= 0 ?
keys.getFirst()[i] : startRow;
byte[] splitStop = (stopRow.length == 0 ||
Bytes.compareTo(keys.getSecond()[i], stopRow) <= 0) &&
keys.getSecond()[i].length > 0 ?
keys.getSecond()[i] : stopRow;
InputSplit split = new TableSplit(table.getTableName(),
splitStart, splitStop, regionLocation); //构造TableSplit
splits.add(split);
if (LOG.isDebugEnabled()) {
LOG.debug("getSplits: split -> " + i + " -> " + split);
}
}
}
return splits;
}

  Pair这个结构由两部分组成:start和end,都是byte[][],这是一个二维byte数组,其中一维是region的顺序,二维是start或者end的字符序列。通过getFirst方法就可以获取所有region的start rowkey,通过getSecond可以获得所有的end rowkey。

  这里还有一个要注意的就是每个HBase表的第一个region是没有start rowkey,最后一个region是没有end  rowkey,这里的“没有”是指的是table.getStartEndKeys()这个方法获取的结果,另外大伙也可以从WEB UI中查看指定的HBase表的region信息也可以看到第一个region的start和最后一个region的end并没有显示。还有就是如果这个HBase表只有一个region的话,getFirst方法返回是没有数据的;getSecond也没数据。

  (1)、getSplits方法中的第一个if语句段是上面说的只有一个region的情况。每个HBase表的第一个region的start rowkey都是EMPTY_BYTE_ARRAY,这是一个长度为0的byte数组。table.getRegionLocation方法会找指定的rowkey所在的region所在信息HRegionLocation。如果只有一个region的话,就先找EMPTY_BYTE_ARRAY所在region信息,如果没有这样的信息就是出现了错误;如果如果有这样的信息的话,就构建一个长度为1的InputSplit列表splits,构造一个TableSplit:设定HBase表名,scan起始和结束rowkey都是EMPTY_BYTE_ARRAY,再加上region所在节点,把这个TableSplit加入splits,返回这个splits。

  (2)、接下来就是HBase表有多个region的情况,构建长度为keys.getFirst().length的InputSplit列表,然后遍历keys.getFirst()获取每个region的位置信息并将其转换成String类型(reverseDNS方法从reverseDNSCacheMap中获取,reverseDNSCacheMap是存储IPAddress => HostName的映射);然后获取用户设定的起始和结束rowkey,并和当前的region的起始和比较结束,如果有rowkey包含这个region就会将这region当做一个InputSplit放入列表中,最后待遍历完之后返回split列表。判断当前region是否应该加入InputSplit列表的条件就是循环中的最后一个if语句段,条件是:((startRow.length == 0 || keys.getSecond()[i].length == 0 ||Bytes.compareTo (startRow, keys.getSecond()[i]) < 0) && (stopRow.length == 0 || Bytes.compareTo(stopRow, keys.getFirst()[i]) > 0)),分解这个条件为两部分:A、(startRow.length == 0 || keys.getSecond()[i].length == 0 ||Bytes.compareTo (startRow, keys.getSecond()[i]) < 0)这个是要确定用户设定的start rowkey是否小于当前region的结束rowkey;B、(stopRow.length == 0 || Bytes.compareTo(stopRow, keys.getFirst()[i]) > 0)这个是要确定用户设定的end rowkey是否大于当前region的开始rowkey,这俩条件必须同时满足才可以,一旦满足就要确定这个TableSplit的开始rowkey和结束rowkey了:A、startRow.length == 0 || Bytes.compareTo(keys.getFirst()[i], startRow) >= 0如果为true的话,说明用户设定的起始rowkey可能是从表的开头开始或者是当前region的起始rowkey大于用户设定的则应该将当前region的起始rowkey作为TableSplit的起始rowkey,如果表达式为false的话将用户设定的起始rowkey作为TableSplit的起始rowkey;B、(stopRow.length == 0 || Bytes.compareTo(keys.getSecond()[i], stopRow) <= 0) && keys.getSecond()[i].length > 0如果为true的话,说明首先要确定是否设定的结束rowkey或者当前region的结束rowkey小于用户设定的结束rowkey,且要保证当前region不是最后一个(keys.getSecond()[i].length > 0),这样的的TableSplit的结束rowkey就是当前region的结束rowkey,如果为false则将用户设定的结束rowkey为TableSplit的结束rowkey,为什么要不是最后一个region呢?因为最后一个region的end rowkey的长度始终为0,比较之下会将最后一个region的end rowkey设置给TableSplit,显然这是不对,能到这里说明这个region应该被分配给一个TableSplit,如果是最后一个region的话,那么这个TableSplit的结束rowkey应该是用户设定的而非这个region自己的。获得这个tableSplit的开始和结束rowkey之后就可以封装这个TableSplit了,并放入InputSplit列表。最终待所有的region遍历结束之后返回这个InputSplit列表。

  这样getSplits方法就结束了。

  二、createRecordReader方法,这个方法代码如下:

 /**
* Builds a TableRecordReader. If no TableRecordReader was provided, uses
* the default.
*
* @param split The split to work with.
* @param context The current context.
* @return The newly created record reader.
* @throws IOException When creating the reader fails.
* @see org.apache.hadoop.mapreduce.InputFormat#createRecordReader(
* org.apache.hadoop.mapreduce.InputSplit,
* org.apache.hadoop.mapreduce.TaskAttemptContext)
*/
@Override
public RecordReader<ImmutableBytesWritable, Result> createRecordReader(
InputSplit split, TaskAttemptContext context)
throws IOException {
if (table == null) {
throw new IOException("Cannot create a record reader because of a" +
" previous error. Please look at the previous logs lines from" +
" the task's full log for more details.");
}
TableSplit tSplit = (TableSplit) split;
TableRecordReader trr = this.tableRecordReader;
// if no table record reader was provided use default
if (trr == null) {
trr = new TableRecordReader();
}
Scan sc = new Scan(this.scan);
sc.setStartRow(tSplit.getStartRow());
sc.setStopRow(tSplit.getEndRow());
trr.setScan(sc);
trr.setHTable(table);
try {
trr.initialize(tSplit, context);
} catch (InterruptedException e) {
throw new InterruptedIOException(e.getMessage());
}
return trr;
}

  这个方法主要是获取TableSplit,然后构造一个Scan,设定开始和结束rowkey;设定HTablePool;将Scan和HTable传递给一个TableRecordReader对象,然后调用initialize(tSplit, context)初始化,最后返回这个TableRecordReader。可以看出TableRecordReader这个是读取key/value的。TableRecordReader中实际操作数据的是TableRecordReaderImpl,TableRecordReader的nextKeyValue()、getCurrentValue()、initialize、getCurrentKey()方法会调用TableRecordReaderImpl的相应方法。

  TableRecordReaderImpl的initialize方法主要是重新创建一个新的Scan,并将createRecordReader传过来的赋值给这个新的currentScan,并获取对应的ResultScanner。

  TableRecordReaderImpl的nextKeyValue()会先创建一个key = new ImmutableBytesWritable()和value = new Result(),这就是我们继承TableMapper中map方法中的参数类型,然后每次调用该方法通过执行value = this.scanner.next()方法来获取HBase中的一行数据赋值给value,这里表明如果scanner.next()运行无异常的话key中是没有数据的(出现异常之后会存储value对应行的rowkey),只有value有数据。执行了这个方法就可以通过getCurrentValue()、getCurrentKey()方法来获取value和key了。

  上面这些讲解了在HBase上使用MapReduce时的分片过程及如何读取这些分片上的数据的。

TableInputFormat分片及分片数据读取源码级分析的更多相关文章

  1. MapReduce的MapTask任务的运行源码级分析

    TaskTracker任务初始化及启动task源码级分析 这篇文章中分析了任务的启动,每个task都会使用一个进程占用一个JVM来执行,org.apache.hadoop.mapred.Child方法 ...

  2. MapReduce的ReduceTask任务的运行源码级分析

    MapReduce的MapTask任务的运行源码级分析 这篇文章好不容易恢复了...谢天谢地...这篇文章讲了MapTask的执行流程.咱们这一节讲解ReduceTask的执行流程.ReduceTas ...

  3. TaskTracker任务初始化及启动task源码级分析

    在监听器初始化Job.JobTracker相应TaskTracker心跳.调度器分配task源码级分析中我们分析的Tasktracker发送心跳的机制,这一节我们分析TaskTracker接受JobT ...

  4. 监听器初始化Job、JobTracker相应TaskTracker心跳、调度器分配task源码级分析

    JobTracker和TaskTracker分别启动之后(JobTracker启动流程源码级分析,TaskTracker启动过程源码级分析),taskTracker会通过心跳与JobTracker通信 ...

  5. MapReduce job在JobTracker初始化源码级分析

    mapreduce job提交流程源码级分析(三)中已经说明用户最终调用JobTracker.submitJob方法来向JobTracker提交作业.而这个方法的核心提交方法是JobTracker.a ...

  6. Flume-NG内置计数器(监控)源码级分析

    Flume的内置监控怎么整?这个问题有很多人问.目前了解到的信息是可以使用Cloudera Manager.Ganglia有图形的监控工具,以及从浏览器获取json串,或者自定义向其他监控系统汇报信息 ...

  7. Flume-NG(1.5版本)中SpillableMemoryChannel源码级分析

    SpillableMemoryChannel是1.5版本新增的一个channel.这个channel优先将evnet放在内存中,一旦内存达到设定的容量就使用file channel写入磁盘.然后读的时 ...

  8. Shell主要逻辑源码级分析 (2)——SHELL作业控制

    版权声明:本文由李航原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/110 来源:腾云阁 https://www.qclou ...

  9. Shell主要逻辑源码级分析(1)——SHELL运行流程

    版权声明:本文由李航原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/109 来源:腾云阁 https://www.qclou ...

随机推荐

  1. lumia 520无法开机

    拿出尘封已久的lumia 520,发现其开机困难,现象如下: 1.拿掉电池再放回去有几率开机 2.轻轻地用手机砸向桌面时手机会重启 因为手机在更新WP8.1之后就出问题了,所以先得定位问题,在黑屏的时 ...

  2. Android空闲教室查询-资料

    这是去年某课程的一个称作“研究型学习”的东西的报告的展示PPT,有点失败的是这个APP的名字起得不太好……PPT上的功能都实现了,其他功能都没有.一年了,程序都忘差不多了,也暂时没有时间分享.就先把P ...

  3. HTML5 IE兼容问题

    最近,为公司做产品的时候用到了HTML5,用IE11打开的时候出现了界面乱或者加载js错误的问题. IE10 or IE11 :Browser Mode is IE10 .Document Mode: ...

  4. ASP.NET MVC系列 框架搭建(一)之仓储层的搭建

    大神勿喷,小神默默学. 会了就是不值一提的东西,不会就是绝对的高大上. 最后上传源码.希望能给读者带来一些新的认识及知识. 还没上过头条..各位大神,请点支持一下小弟. 陆续更新.更新到你会为止!! ...

  5. (旧)子数涵数·Flash——路径补间

    一.打开flash软件(图为flash8) 二.创建新项目->Flash文档 三.使用椭圆工具,绘制一个圆形图像(快捷键为O,很形象吧) 四.在后面若干帧中插入关键帧,并移动刚刚绘制好的图像的位 ...

  6. 序列化类型为“System.Data.Entity.DynamicProxies.ActionInfo_”的对象时检测到循环引用。

    解决方案: 加上 db.Configuration.ProxyCreationEnabled = false;这句话搞定~

  7. 【Moqui业务逻辑翻译系列】Story of Online Retail Company 在线零售公司的故事

    h1. Story of Online Retail Company 在线零售公司的故事 Someone decides to sell a product. [Product Marketer Ma ...

  8. iOS--SDAutolayout宽度自适应

    #pragma mark - UIScrollView 内容竖向自适应.内容横向自适应方法 @interface UIScrollView (SDAutoContentSize) /** 设置scro ...

  9. 深入理解web项目的配置文件

    1.启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点. 2.紧急着,容创建一个Servl ...

  10. Asp.Net MVC 中实现跨域访问

    在ASP.Net webapi中可以使用  Microsoft.AspNet.WebApi.Cors  来实现: public static class WebApiConfig { public s ...