2021SC@SDUSC
研究内容简略介绍
上周我们分析了类Partitioner以及其代表子类HashPartitioner,并对字定义Partitioner做了一些尝试。随后又分析了QueueAclsInfo和RecordReader,同时对RecordReader的方法及几种常见RecordReader做了分析。本次我们将要继续分析与RecordReader紧密相连的类org.apache.hadoop.mapreduce.RecordWriter<K,V>
。
org.apache.hadoop.mapreduce.RecordWriter<K,V>源码分析
首先看看官方给出的注释:
RecordWriter 继承自Object类。RecordWriter 将输出 <key, value> 对写入输出文件并且实现将作业输出写入 FileSystem.。
简单来说就是负责将task的key/value结果写入内存或者磁盘。
一 、方法分析
1.1 write:写key/value键值对
1.2 close: 关闭RecordWriter
二、RecordWriter运行流程分析
2.1 Map Task Record Writer运行流程分析
Map Task#runNewMapper会根据是否当前程序需要运行Reduce来创建不同的RecordWriter:没有Reduce任务,则创建NewDirectOutput
Collector对象;否则需要创建NewOutputCollector对象
每一次调用map方法结束都会调用context.wirte方法,将key/value写入内存或者磁盘
write方法又会调用NewOutputCollector#collect方法,collect方法开始将key/value写入内存,如果达到阀值则将内存的数据溢写到磁盘
程序运行结束,根据情况看是否需要合并产生的溢写文件,如果太多,是有必要进行一次merge的
2.2 Reduce Task Record Writer运行流程分析
Reduce Task#runNewReducer会构造NewTrackingRecordWriter对象
#这个对象会调用OutputFormat#getRecordWriter对象,默认我们用的是TextOutputFormat,然后它对应的RecordWriter就是LineRecord
Writer对象
#然后每次reduce方法执行完毕,都会调用context.write(key,value)
这时候LineRecordWriter就会把key/value写入输出文件
三、常见的RecordWriter
3.1 DBRecordWriter: 将reduce结果写入sql表中
3.2 LineRecordWriter: 将key/value写入输出文件一行中
3.3 NewDirectOutputCollector: 它默认调用的也是LineRecordWriter,输出结果写入输出文件
3.4 NewOutputCollector: 将key/value写入内存,内存满了写入磁盘,一般情况在Map阶段
org.apache.hadoop.mapreduce.Reducer源码分析
接下来我们分析mapreduce中的重要类Reducer
其源码如下:
package org.apache.hadoop.mapreduce;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapreduce.task.annotation.Checkpointable;
import java.util.Iterator;
@Checkpointable
@InterfaceAudience.Public
@InterfaceStability.Stable
public class Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
/**
* The <code>Context</code> passed on to the {@link Reducer} implementations.
*/
public abstract class Context
implements ReduceContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
}
/**
* Called once at the start of the task.
*/
protected void setup(Context context
) throws IOException, InterruptedException {
// NOTHING
}
/**
* This method is called once for each key. Most applications will define
* their reduce class by overriding this method. The default implementation
* is an identity function.
*/
@SuppressWarnings("unchecked")
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context
) throws IOException, InterruptedException {
for(VALUEIN value: values) {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}
/**
* Called once at the end of the task.
*/
protected void cleanup(Context context
) throws IOException, InterruptedException {
// NOTHING
}
/**
* Advanced application writers can use the
* {@link #run(org.apache.hadoop.mapreduce.Reducer.Context)} method to
* control how the reduce task works.
*/
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
while (context.nextKey()) {
reduce(context.getCurrentKey(), context.getValues(), context);
// If a back up store is used, reset it
Iterator<VALUEIN> iter = context.getValues().iterator();
if(iter instanceof ReduceContext.ValueIterator) {
((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();
}
}
} finally {
cleanup(context);
}
}
}
reducer的任务是将一组共享一个键的中间值减少到一小组值。
用户通过JobConf#setNumReducerTask(int)方法来设置job的Reducer的数目。Reducer的实现类通过JobConfigurable#configure(JobConf)方法来获取job,并初始化它们。类似的,可通过Closeable#close()方法来消耗初始化。
Reducer有是3个主要阶段:
第一阶段:洗牌,Reducer的输入是Mapper的分组输出。在这个阶段,每个Reducer通过http获取所有Mapper的相关分区的输出。
第二阶段:排序,在这个阶段,框架根据键(因不同的Mapper可能产生相同的Key)将Reducer进行分组。洗牌和排序阶段是同步发生的,例如:当取出输出时,将合并它们。
二次排序,若分组中间值等价的键规则和reduce之前键分组的规则不同时,那么其中之一可以通过JobConf#setOutputValueGroupingComparator(Class)来指定一个Comparator。
JobConf#setOutputKeyComparatorClass(Class)可以用来控制中间键分组,可以用在模拟二次排序的值连接中。
示例:若你想找出重复的web网页,并将他们全部标记为“最佳”网址的示例。你可以这样创建job:
Map输入的键:url
Map输入的值:document
Map输出的键:document checksum,url pagerank
Map输出的值:url
分区:通过checksum
输出键比较器:通过checksum,然后是pagerank降序。
输出值分组比较器:通过checksum
Reduce
在此阶段,为在分组书中的每个<key,value数组>对调用reduce(Object, Iterator, OutputCollector, Reporter)方法。
reduce task的输出通常写到写到文件系统中,方法是:OutputCollector#collect(Object, Object)。
Reducer的输出结果没有重新排序。
/**
* Called once at the start of the task.
*/
protected void setup(Context context
) throws IOException, InterruptedException {
// NOTHING
}
setup()在任务开始时调用一次,进行初始化操作。
/**
* This method is called once for each key(这个方法被所有key使用). Most applications will define
* their reduce class by overriding this method(所有的应用都会重写这个方法). The default implementation(默认是identity函数)
* is an identity function.
*/
@SuppressWarnings("unchecked")
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context
) throws IOException, InterruptedException {
for(VALUEIN value: values) {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}
实现具体的Reducer业务逻辑。
/**
* Called once at the end of the task.
*/
protected void cleanup(Context context
) throws IOException, InterruptedException {
// NOTHING
}
实现收尾的一些关闭流的操作。
Reducer的工作流程
第一阶段是 Reducer 任务会主动从 Mapper 任务复制其输出的键值对。Mapper 任务可能会有很多,因此 Reducer 会复制多个 Mapper 的输出。
第二阶段是把复制到 Reducer 本地数据,全部进行合并,即把分散的数据合并成一个大的数据。再对合并后的数据排序。
第三阶段是对排序后的键值对调用 reduce 方法。键相等的键值对调用一次reduce 方法,每次调用会产生零个或者多个键值对。最后把这些输出的键值对写入到 HDFS 文件中。
1.Copy过程: Reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中。
默认情况下,当整个MapReduce作业的所有已执行完成的Map Task任务数超过Map Task总数的5%后,JobTracker便会开始调度执行Reduce Task任务。然后Reduce Task任务默认启动mapred.reduce.parallel.copies(默认为5)个MapOutputCopier线程到已完成的Map Task任务节点上分别copy一份属于自己的数据。 这些copy的数据会首先保存的内存缓冲区中,当内冲缓冲区的使用率达到一定阀值后,则写到磁盘上。
但是有一个问题,分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和JobTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就ok了。还有就是map端已经做完了partition,reduce根据partition标识符来拉自己需要的数据
2.Merge: 这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活,它基于JVM的heap size设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle用。这里需要强调的是,merge有三种形式:1)内存到内存 2)内存到磁盘 3)磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑,是吧。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。
3.Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,当然希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。当Reducer的输入文件已定,整个Shuffle才最终结束。
4.reduce阶段:和map函数一样也是程序员编写的,最终结果是存储在hdfs上的。
mapreduce过程小结
1.客户端提交一个作业
2.JobClient与JobTracker通信,JobTracker返回一个JobID
3.JobClient复制作业资源文件
将运行作业所需要的资源文件复制到HDFS上,包括MapReduce程序打包的JAR文件、配置文件和客户端计算所得的输入划分信息。这些文件都存放在JobTracker专门为该作业创建的文件夹中。文件夹名为该作业的Job ID。JAR文件默认会有10个副本(mapred.submit.replication属性控制);输入划分信息告诉了JobTracker应该为这个作业启动多少个map任务等信息。
4.开始提交任务(任务的描述信息:包括jobid,jar存放的位置,配置信息等等)
5.初始化任务。创建作业对象
JobTracker接收到作业后,将其放在一个作业队列里,等待作业调度器对其进行调度
6.对HDFS上的资源文件进行分片,每一个分片对应一个MapperTask
当作业调度器根据自己的调度算法调度到该作业时,会根据输入划分信息为每个划分创建一个map任务,并将map任务分配给TaskTracker执行
7.TaskTracker会向JobTracker返回一个心跳信息(任务的描述信息),根据心跳信息分配任务
TaskTracker每隔一段时间会给JobTracker发送一个心跳,告诉JobTracker它依然在运行,同时心跳中还携带着很多的信息,比如当前map任务完成的进度等信息。当JobTracker收到作业的最后一个任务完成信息时,便把该作业设置成“成功”。当JobClient查询状态时,它将得知任务已完成,便显示一条消息给用户。
8.TaskTracker从HDFS上获取作业资源文件
对于map和reduce任务,TaskTracker根据主机核的数量和内存的大小有固定数量的map槽和reduce槽。这里需要强调的是:map任务不是随随便便地分配给某个TaskTracker的,这里有个概念叫:数据本地化(Data-Local)。意思是:将map任务分配给含有该map处理的数据块的TaskTracker上,同时将程序JAR包复制到该TaskTracker上来运行,这叫“运算移动,数据不移动”。而分配reduce任务时并不考虑数据本地化。
Map端:
1.每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认为64M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。
2.在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区,也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行Combia操作,这样做的目的是让尽可能少的数据写入到磁盘。
3.当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和combia操作,目的有两个:1.尽量减少每次写入磁盘的数据量;2.尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以了。
4.将分区中的数据拷贝给相对应的reduce任务。有人可能会问:分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和JobTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就ok了哦。
Reduce端:
1.Reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。如果reduce端接受的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间的百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merge.percent决定),则对数据合并后溢写到磁盘中。
2.随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省时间。其实不管在map端还是reduce端,MapReduce都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是hadoop的灵魂。
3.合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。
总结
本次我们在学习了RecordReader的基础上分析了RecordWriter的源码,并对mapreduce中最关键的几个类之一Reducer进行了深入分析,了解了其函数以及工作流程,并结合之前学习的mapper分析了mapreduce的粗略过程,为之后的学习打下基础。