接着上文所说的map的输入以及通过调用读取器的nextKeyValue判断是否还有下一条数据,读取数据,重新赋值偏移量,以及给key赋值偏移量pos(当前行相对于整个文件的一个位置),value(当前行的数据),完成了map阶段的输入以及重新定义,接下来该聊的就是map的输出阶段,该阶段是比较复杂的,分为两个分支;第一分支是没有reduce的阶段,在日常的MR的过程中最简单的优化手段就是在尽量的避免reduce的产生,因为reduce阶段存在shuffle阶段,只要有reduce阶段的发生,那么shuffle阶段就是不可避免的,而在整个MR的过程中,最耗时的就是shuffle阶段,现阶段的大部分对于MR的优化基本是都是旨在减少甚至避免shuffle对整个过程造成的影响,此处就不多讲了,在接下来的文章中会将MR的调优仔细讲一下,有需要的话,可以参考之后的文章。另外一个分支就是存在reduce阶段,这个分支将是最主要也是最为复杂的阶段。
现在进入主题,进入map输出阶段的源码分析,入口就是在自定义的map的context.write()方法。
进入代码,就会发现其实是调用的mapContext的Write方法。
而这write方法其实是上一篇中在初始化MapContext的时候根据是否存在reduce阶段,选择output方式之后加入mapcontext的,代码如下;
所以问题就回到了,这个时候的output到底是啥呢?话不多说,上代码;
这下对这个output算是有一定了解了吧,对的,其实如果不存在reduce阶段的话,map的输出底层是调用hdfs的输出,将结果直接输出,不存在排序相关的过程,接下来就是直接输出的时候的相关源码了;
/**
* 直接输出相关的源码
* 对容器进行初始化
*/
NewDirectOutputCollector(MRJobConfig jobContext,
JobConf job, TaskUmbilicalProtocol umbilical, TaskReporter reporter)
throws IOException, ClassNotFoundException, InterruptedException {
/*****************接下来是集群的监控或者通信服务,暂时不用关注*******************/
this.reporter = reporter;
mapOutputRecordCounter = reporter
.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
fileOutputByteCounter = reporter
.getCounter(FileOutputFormatCounter.BYTES_WRITTEN);
List<Statistics> matchedStats = null;
if (outputFormat instanceof org.apache.hadoop.mapreduce.lib.output.FileOutputFormat) {
matchedStats = getFsStatistics(org.apache.hadoop.mapreduce.lib.output.FileOutputFormat
.getOutputPath(taskContext), taskContext.getConfiguration());
}
fsStats = matchedStats;
/*****************end*******************/
long bytesOutPrev = getOutputBytes(fsStats);
/**
* 获取当前输出格式化类的输出器
* 这个方式可以联想到输入格式化类有自己默认的读取器,输出格式化类也有着自己的默认的读取器
* out = new LineRecordWriter<K, V>(fileOut, keyValueSeparator);
*/
out = outputFormat.getRecordWriter(taskContext);
long bytesOutCurr = getOutputBytes(fsStats);
fileOutputByteCounter.increment(bytesOutCurr - bytesOutPrev);
}
/**
* 真正的写出的过程
* @param key
* @param value
* @throws IOException
* @throws InterruptedException
*/
@Override
@SuppressWarnings("unchecked")
public void write(K key, V value)
throws IOException, InterruptedException {
reporter.progress();
long bytesOutPrev = getOutputBytes(fsStats);
/**
* 此处调用了LineRecordWriter的write方法
*/
out.write(key, value);
long bytesOutCurr = getOutputBytes(fsStats);
fileOutputByteCounter.increment(bytesOutCurr - bytesOutPrev);
mapOutputRecordCounter.increment(1);
}
//接下来是进入LineRecordWriter内部查看write的实现,需要注意的是,输出的时候是不经过缓冲区,直接将key,value进行输出
public synchronized void write(K key, V value)
throws IOException {
boolean nullKey = key == null || key instanceof NullWritable;
boolean nullValue = value == null || value instanceof NullWritable;
if (nullKey && nullValue) {
return;
}
if (!nullKey) {
writeObject(key);
}
/**
* 判断key和value是否为空,不过我不知道为什么会这么判断,key值会为空吗?
*/
if (!(nullKey || nullValue)) {
//此处的out为 DataOutputStream extends FilterOutputStream
out.write(keyValueSeparator);
}
if (!nullValue) {
writeObject(value);
}
out.write(newline);
}
再往下走其实就没什么必要性,因为底层调用的hdfs的底层的一个output的工具,个人觉得没什么必要性继续往下追,所以基本也就到此,浅尝辄止了。
接下来才是重头戏,就是在存在reduce的情况下的输出到底是什么样的呢?
好了,上代码,代码如下;
首先明确一点,此处的output已经变了;
output = new NewOutputCollector(taskContext, job, umbilical, reporter);
在接下来的这段代码,将会说明reduce的个数,到底是靠什么去规定的,以及分区器是干什么的;
/**
* 初始化一个可排序的容器,注意此时是支持排序的,为的是输出有序的数据
* 初始化分区器,这时候加载分区器,如果没有自定义,就会默认选择hashPartition
* 如果reduce没有指定,默认只有一个,则每次指挥返回0号分区
*/
NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
JobConf job,
TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, ClassNotFoundException {
//存储key,value,partition的容器--》MapOutputCollector--》MapOutputbuffer
collector = createSortingCollector(job, reporter);
/**
* 分区个数等于设定的reduce的个数
* reduce的个数是跟分区数相同的
* reduece的 个数是可以人为指定的,所以reduce的个数,并行度,其实是由人为的去控制的
* 重写分区器partitioner的作用是,通过人为的干预分组过程,尽量避免出现数据倾斜的问题
*/
partitions = jobContext.getNumReduceTasks();
if (partitions > 1) {
//自定义分区器,继承org.apache.hadoop.mapreduce.Partitioner<K,V> -》根据数据抽样自定义分区器可有效防止在进行mr的时候出现数据倾斜的现象
//默认是hash分区器
//自定义分区器的作用是为了在一定程度上避免大量数据倾斜的产生
partitioner = (org.apache.hadoop.mapreduce.Partitioner<K, V>)
ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
} else {
//获取分区号
partitioner = new org.apache.hadoop.mapreduce.Partitioner<K, V>() {
@Override
public int getPartition(K key, V value, int numPartitions) {
return partitions - 1;
}
};
}
}
记住这段代码;partitions = jobContext.getNumReduceTasks();
你的分区数是由你的reduce的个数决定的,那么reduce的个数哪来的呢?就是我们自己在编写代码的时候自己设置的,也就是说我们自己可以人为的规定reduce的分区个数,但是在决定分区个数的时候最好是先做抽样,避免分区设置太多很多都是无用的,或者分区数太少,reduce的并行度起不来,导致任务执行很长时间。
接下来的重点就是;
就是这个创建可排序的容器,上代码;
private <KEY, VALUE> MapOutputCollector<KEY, VALUE>
createSortingCollector(JobConf job, TaskReporter reporter)
throws IOException, ClassNotFoundException {
MapOutputCollector.Context context =
new MapOutputCollector.Context(this, job, reporter);
/**
* 容器的底层实现是MapOutputBuffer
*/
Class<?>[] collectorClasses = job.getClasses(
JobContext.MAP_OUTPUT_COLLECTOR_CLASS_ATTR, MapOutputBuffer.class);
int remainingCollectors = collectorClasses.length;
for (Class clazz : collectorClasses) {
try {
if (!MapOutputCollector.class.isAssignableFrom(clazz)) {