不得不说阅读源码的过程,极其痛苦 。Dream Car 镇楼 ~ !

虽说整个MapReduce过程也就只有Map阶段和Reduce阶段,但是仔细想想,在Map阶段要做哪些事情?这一阶段具体应该包含数据输入(input),数据计算(map),数据输出(output),这三个步骤的划分是非常符合思维习惯的。

从大数据开发的hello world案例入手,如下是一个word count 案例的map程序

public class WcMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

    private Text k = new Text();
private IntWritable v = new IntWritable(1); protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //1. 获取一行数据
String str = value.toString();
//2. 按照空格分开单词
String[] words = str.split(" ");
//3. 遍历集合,拼装成(word,one)形式
for (String word : words) {
this.k.set(word);
context.write(k, v);
}
}
}

自定义的WcMapper类继承了Mapper类,重写了map()方法,在这个方法里我们按照需求,编写了相应的业务逻辑。进入Mapper类中查看。

这个类包含的方法并不多,并且比较符合见名知义的思维规律,可以根据方法辅助注释大概了解其具体功能。在这个类的头上还包括一段对类的描述性注释,大致意思就是map阶段到底干了什么,尝试简单翻译一下核心内容

  • 将输入键/值对映射到一组中间键/值对。
  • 映射是将输入记录转换为中间记录的单个任务。 转换后的中间记录不需要与输入记录的类型相同。 一个给定的输入对可以映射到零个或多个输出对。
  • Hadoop Map-Reduce 框架为InputFormat为作业生成的每个InputSplit生成一个映射任务。 Mapper实现可以通过JobContext.getConfiguration()访问作业的Configuration 。
  • 框架首先调用setup(Mapper.Context) ,然后为InputSplit中的每个键/值对调用map(Object, Object, Mapper.Context) 。 最后调用cleanup(Mapper.Context) 。
  • 与给定输出键关联的所有中间值随后由框架分组,并传递给Reducer以确定最终输出。 用户可以通过指定两个关键的RawComparator类来控制排序和分组。
  • Mapper输出按Reducer进行分区。 用户可以通过实现自定义Partitioner来控制哪些键(以及记录)去哪个Reducer 。
  • 用户可以选择通过Job.setCombinerClass(Class)指定combiner来执行中间输出的本地聚合,这有助于减少从Mapper传输到Reducer的数据量。
  • 应用程序可以指定是否以及如何压缩中间输出,以及通过Configuration使用哪些CompressionCodec 。

    如果作业有零减少,则Mapper的输出将直接写入OutputFormat而不按键排序。
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

  public abstract class Context implements MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {}

  protected void setup(Context context ) throws IOException, InterruptedException {}

  protected void map(KEYIN key, VALUEIN value,Context context) throws IOException, InterruptedException
{context.write((KEYOUT) key, (VALUEOUT) value);} protected void cleanup(Context context) throws IOException, InterruptedException {} public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
}

看到run(Context context) 这个方法就比较有框架的感觉了,这个方法里面调用了一次setup(context)cleanup(context),而对map方法则为输入拆分中的每个键/值对调用一次。

这个类看到这也就算结束了,其它的也看不出啥东西了。进入MapTask类,包含了大量的核心业务逻辑方法。这个类会被Yarn反射调用run方法,实例化MapTask。直接进run方法,删除了部分非核心代码,清清爽爽。

@Override
public void run(final JobConf job, final TaskUmbilicalProtocol umbilical){
this.umbilical = umbilical;
if (isMapTask()) {
// reduce的个数为 0,所以整个任务只有map阶段
if (conf.getNumReduceTasks() == 0) {
mapPhase = getProgress().addPhase("map", 1.0f);
} else {
// 如果有reduce阶段,将进行进度分配
mapPhase = getProgress().addPhase("map", 0.667f);
// 排序环节让后续的reduce环节变得更轻松完成,只需拉取一次文件,减少I/O
sortPhase = getProgress().addPhase("sort", 0.333f);
}
}
TaskReporter reporter = startReporter(umbilical); boolean useNewApi = job.getUseNewMapper();
initialize(job, getJobID(), reporter, useNewApi); // check if it is a cleanupJobTask
.........
if (useNewApi) { // 新旧API的选择
// 进这个方法
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
done(umbilical, reporter);
}

继续进入runNewMapper(job, splitMetaInfo, umbilical, reporter) 方法。里边有点长啊,一下不好找到重点。小常识:重要的东西放在try-catch中!! 所以首先看try-catch块。

private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runNewMapper(final JobConf job,final TaskSplitIndex splitIndex,final TaskUmbilicalProtocol umbilical,TaskReporter reporter) { ............先删了,略过不看............
// 用人类的思维过一遍方法名
try {
// 1、初始化输入流
input.initialize(split, mapperContext);
// 2、直觉调用这个run()方法,最终会调用到自定义的map方法
mapper.run(mapperContext);
// 3、完成map计算阶段
mapPhase.complete();
// 4、排序阶段走起
setPhase(TaskStatus.Phase.SORT);
// 5、状态信息更新或者传递(猜的)
statusUpdate(umbilical);
// 6、关闭输入流
input.close();
input = null;
// 7、进入到out阶段,输出map数据
output.close(mapperContext);
output = null;
} finally {
// Quietly,默默的做一些事情 ...
closeQuietly(input);
closeQuietly(output, mapperContext);
}
}

这样一来整个思路就就很丝滑顺畅了,回过头来看删除掉的代码片段 ,原注释信息也蛮好懂的。

   // 1、make a task context so we can get the classes  封装任务的上下文,job里有configuration
// 常识:在框架中上下文对象是不可缺少的,有些信息在业务线来回穿梭,封装进上下文可以随时获取
// 回忆:客户端上传任务到资源层,其中包括Jar包,配置文件,切片三个文件,container拿到可以实例化job
org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =
new org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl(job, getTaskID(),reporter); // 2、make a mapper:根据taskContext + job,实例化出来一个mapper对象
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
(org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
// 就是自己写的WCMapper对象,也就对应了下边的 mapper.run(mapperContext)。丝滑~!
ReflectionUtils.newInstance(taskContext.getMapperClass(), job); // 3、make the input format:输入格式化,为啥需要这个玩意?split是一片数据,那读一条数据就要这玩意了
org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
(org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
// 在写job配置的时候,其实是可以指定InputFormat哒,默认是TextInputFormat
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job); // 4、rebuild the input split,每个map都要确定自己往哪个split移动
org.apache.hadoop.mapreduce.InputSplit split = null;
// 每个mapper都要搞搞清楚自己要读取哪个split 【计算向数据移动】
split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset()); // 5、input = split + inputFormat (父类是RecordReader)
org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
new NewTrackingRecordReader<INKEY,INVALUE>
// 所以input有能力在split读取出来一条条的记录
(split, inputFormat, reporter, taskContext);
// 小总结:3、4、5 三步要做的就是——想个办法在Split中读取一条数据 //--------------------NewTrackingRecordReader() begin-------------------------------
private final org.apache.hadoop.mapreduce.RecordReader<K,V> real;
NewTrackingRecordReader(...){
.....
// 调用TextInputFormat的createRecordReader,返回一个LineRecordReader对象
// 所以input就是一个LineRecordReader对象
this.real = inputFormat.createRecordReader(split, taskContext);
.....
}
//--------------------NewTrackingRecordReader() end-------------------------------- ...........先略过输出这一部分........... // 6、上面是任务上下文,这里是map上下文,包含了input、output、split
org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE>
mapContext =
new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(),
input, output,
committer,
reporter, split);
// 7、又对map上下文包装了一层mapperContext,包含了input、output、split
// 这不就是Mapper类中的run(Context context)的入参嘛 ~!!
org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context
mapperContext =
new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
mapContext); //-------------Mapper::run(Context context) begin ----------------------------------
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
// 从mapper的上下文里判断有无下一条数据
while (context.nextKeyValue()) {
// 取出切片中的下一条数据进行计算
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
// 从map的上下文信息中是如何获取到一条数据的?LineRecordReader返回的~~ 层层封装真绝了
//-------------Mapper::run(Context context) end ----------------------------------

现在可以回头看try块中的 input.initialize(split, mapperContext)的方法,进去看方法实现的细节。数据在HDFS层会被切割开,那么它能被计算正确是如何实现的? 在这就有相应的实现代码不复杂,但是有小亮点。

只保留核心业务逻辑,还是该删的删,清清爽爽,开开心心阅读源码 ~

// 记住这是Recordreader的初始化方法
public void initialize(InputSplit genericSplit,TaskAttemptContext context) { // map任务计算是面向切片的,先拿到切片,再拿到切片的始端
start = split.getStart();
// 始端 + 切片大小,得到末端
end = start + split.getLength();
// 从切片中拿到文件路径
final Path file = split.getPath(); // open the file and seek to the start of the split
// 获取到文件系统的一个对象
final FileSystem fs = file.getFileSystem(job);
//打开文件,会得到一个面向文件的输入流
// 各个map并行执行,所以不会都是从文件头开始读,所以它要搭配一个seek()方法
fileIn = fs.open(file); if (...) {
......
} else {
// 每个map 都会seek到自己切片偏移量的位置开始读取数据
fileIn.seek(start);
// SplitLineReader:切片里的行记录读取器。这名字一看就很面向对象
in = new SplitLineReader(fileIn, job, this.recordDelimiterBytes);
filePosition = fileIn;
}
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
// 如果这不是第一次拆分,我们总是丢弃第一条记录。
// 因为我们总是(除了最后一次拆分)在 next() 方法中读取额外的一行。
// 这就防止了 hello 被拆成了 he llo 导致计算错误
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}

in.readLine(new Text(), 0, maxBytesToConsume(start)) 这个方法把读到的一行数据交给一个Text对象持有,返回值是一个int类型的数值,表示读到了多少个字节。

注意到方法传参new Text()对象,当方法执行完是时候,这个对象会因为没有引用被GC回收。那么既然没有引用,它在干嘛?

回忆:切片是一个逻辑切分,默认的大小是一个block块的大小。假如一个split小于block ,这个block就会被切成多个部分。如果就是尼玛那么寸, hello 两个切片被拆成了 he llo 两部分,就会导致计算错误。这时候向下多读一行,哎,这个问题就解决啦。

再回头说:计算向数据移动。被多读的一行如果在其它的节点怎么办?答:把这一行数据传过来,不必移动计算。

其实看到这里也就可以明白了,在整个Map的input环节,真正干读取数据活的是LineRecordReaderkey就是面向行的字节偏移量。下边这段代码已经出现多次了

  public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
// 从mapper的上下文里判断有无下一条数据
while (context.nextKeyValue()) {
// 取出切片中的下一条数据进行计算
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}

通过阅读上边的源码我们已经知道此处传参Context实际上就是一个MapContextImpl对象,context.nextKeyValue()方法也就是在调用LineRecordReader::nextKeyValue()方法。这个方法内部:会对key-value进行赋值,返回boolean值,代表是否赋值成功。总体下来可以说是感觉非常的丝滑~

总结:(我自己能看懂就行了~)

MapTask:input -> map -> output

intput:(Split + format)来自于输入格式化类返回记录读取器对象

TextInputFormat - > LineRecordReader:

Split三个维度:file , offset , length

init():in = fs.open(file).seek。除了第一个切片,都会往下多读一行。

nextKeyValue():

1、读取数据中的一条记录对应的key,value 赋值;

2、返回布尔值;

getCurrentKey()

getCurrentValue()

MapReduce —— MapTask阶段源码分析(Input环节)的更多相关文章

  1. MapReduce —— MapTask阶段源码分析(Output环节)

    Dream car 镇楼 ~ ! 接上一节Input环节,接下来分析 output环节.代码在runNewMapper()方法中: private <INKEY,INVALUE,OUTKEY,O ...

  2. MapReduce 切片机制源码分析

    总体来说大概有以下2个大的步骤 1.连接集群(yarnrunner或者是localjobrunner) 2.submitter.submitJobInternal()在该方法中会创建提交路径,计算切片 ...

  3. YARN(MapReduce 2)运行MapReduce的过程-源码分析

    这是我的分析,当然查阅书籍和网络.如有什么不对的,请各位批评指正.以下的类有的并不完全,只列出重要的方法. 如要转载,请注上作者以及出处. 一.源码阅读环境 需要安装jdk1.7.0版本及其以上版本, ...

  4. MapReduce任务提交源码分析

    为了测试MapReduce提交的详细流程.需要在提交这一步打上断点: F7进入方法: 进入submit方法: 注意这个connect方法,它在连接谁呢?我们知道,Driver是作为客户端存在的,那么客 ...

  5. 【spring源码分析】IOC容器初始化(一)

    前言:spring主要就是对bean进行管理,因此IOC容器的初始化过程非常重要,搞清楚其原理不管在实际生产或面试过程中都十分的有用.在[spring源码分析]准备工作中已经搭建好spring的环境, ...

  6. React事件杂记及源码分析

    前提 最近通过阅读React官方文档的事件模块,发现了其主要提到了以下三个点  调用方法时需要手动绑定this  React事件是一种合成事件SyntheticEvent,什么是合成事件?  事件属性 ...

  7. MapReduce源码分析之JobSubmitter(一)

    JobSubmitter,顾名思义,它是MapReduce中作业提交者,而实际上JobSubmitter除了构造方法外,对外提供的唯一一个非private成员变量或方法就是submitJobInter ...

  8. Hadoop2源码分析-MapReduce篇

    1.概述 前面我们已经对Hadoop有了一个初步认识,接下来我们开始学习Hadoop的一些核心的功能,其中包含mapreduce,fs,hdfs,ipc,io,yarn,今天为大家分享的是mapred ...

  9. MapReduce之提交job源码分析 FileInputFormat源码解析

    MapReduce之提交job源码分析 job 提交流程源码详解 //runner 类中提交job waitForCompletion() submit(); // 1 建立连接 connect(); ...

随机推荐

  1. Linux-鸟菜-6-文件搜索

    Linux-鸟菜-6-文件搜索 which(寻找[执行档]) alian ..............这个后面显示的是别名 没有找到history是因为which是根据PATH环境变阿玲去搜索执行文件 ...

  2. Redis6.x学习笔记(二)持久化之RDB

    前言 最近学习Redis6.x,特做笔记以备忘,与大家共学.课程是从私塾在线下载的,他们把架构师课程都放出来了,大家可以去下载学习,不要钱的,地址是http://t.hk.uy/eK7,课程很不错,值 ...

  3. nginx日志文件按天记录定时清理循环记录

    问题 nginx日志默认记录在一个文件access.log中,时间长了会导致日志文件特别大,甚至磁盘占满. 解决方案 使用以下方法,将access.log文件每天一个,然后清过15天以前的文件. 方法 ...

  4. 论文笔记:RankIQA

    0.Abstract 本文提出了一种从排名中学习的无参考图像质量评估方法(RankIQA).为了解决IQA数据集大小有限的问题,本文训练了一个孪生网络,通过使用合成的已知相对图像质量排名的数据集来训练 ...

  5. 微信小程序组件设计规范

    微信小程序组件设计规范 组件化开发的思想贯穿着我开发设计过程的始终.在过去很长一段时间里,我都受益于这种思想. 组件可复用 - 减少了重复代码量 组件做为抽离的功能单元 - 方便维护 组件作为temp ...

  6. 一看就懂的MySQL的聚簇索引,以及聚簇索引是如何长高的

    这一篇笔记我们简述一下 MySQL的B+Tree索引到底是咋回事? 聚簇索引索引到底是如何长高的. 一点一点看,其实蛮好理解的. 如果你看过了我之前的笔记,你肯定知道了MySQL进行CRUD是在内存中 ...

  7. [ML] 高德软件的路径规划原理

    路径规划 Dijkstra s:起点:S:已知到起点最短路径的点:U:未知到起点最短路径的点 Step 1:S中只有起点s,从U中找出路径最短的 Step 2:更新U中的顶点和顶点对应的路径 重复St ...

  8. MySQL配置HeartBeat实现心跳监控和浮动IP

    1. 初始化环境配置 /sbin/chkconfig --add mysqld /sbin/chkconfig mysqld on ln -s /usr/local/mysql/bin/mysql / ...

  9. jmeter 通过CSV Data Set Config控件参数化

    CSV Data Set Config控件配置如下: 被导入的.csv 文件内容如下 用excel打开如下 设置中url2对应:cn.toursforfun.com 和 www.163.com url ...

  10. 053.Python前端Django框架模板层

    模板层 一 模板语法之变量 在 Django 模板中遍历复杂数据结构的关键是句点字符, 语法: {{ var_name }} [root@node10 mysite]# cat app01/urls. ...