MapReduce学习笔记
1. MapReduce编程模型- Hadoop架构
Hadoop由HDFS分布式存储、MapReduce分布式计算、Yarn资源调度三部分组成。
- MapReduce是采用一种分而治之的思想设计出来的分布式计算框架
- MapReduce由两个阶段组成:
- Map阶段(将大任务切分成一个个小的任务)
- Reduce阶段(汇总小任务的结果)
- 什么是分而治之:
- 比如一复杂、计算量大、耗时长的的任务,称为“大任务”;
- 此时使用单台服务器无法计算或较短时间内计算出结果时,可将此大任务切分成一个个小的任务,小任务分别在不同的服务器上并行的执行
- 最终再汇总每个小任务的结果
1.1 Map阶段
- map阶段有一个关键的map()函数;
- 此函数的输入是键值对
- 输出是一系列键值对,输出写入本地磁盘。
1.2 Reduce阶段
-
reduce阶段有一个关键的函数reduce()函数
-
此函数的输入也是键值对(即map的输出(kv对))
-
输出也是一系列键值对,结果最终写入HDFS
1.3 MapReduce模型图
2. MapReduce编程示例
以MapReduce的词频统计为例:统计一批英文文章当中,每个单词出现的总次数。
2.1 MapReduce原理图
- Map阶段
- 假设MR程序的输入文件“Gone With The Wind”有三个block,分别是block1、block2、block3
- MR编程时,每个block对应一个分片split
- 每一个split对应一个map任务(map task)
- 如图共3个map任务(map1、map2、map3);这3个任务的逻辑一样,所以以第一个map任务(map1)为例分析
- map1读取block1的数据;一次读取block1的一行数据;
- 产生键值对(key/value),作为map()的参数传入,调用map();
- 假设当前所读行是第一行
- 将当前所读行的行首相对于当前block开始处的字节偏移量作为key(0)
- 当前行的内容作为value(Dear Bear River)
- map()内
- (按需求,写业务代码),将value当前行内容按空格切分,得到三个单词Dear | Bear | River
- 将每个单词变成键值对,输出出去(Dear, 1) | (Bear, 1) | (River, 1);最终结果写入map任务所在节点的本地磁盘中(内里还有细节,讲到shuffle时,再细细展开)
- block的第一行的数据被处理完后,接着处理第二行;逻辑同上
- 当map任务将当前block中所有的数据全部处理完后,此map任务即运行结束
- 其它的每一个map任务都是如上逻辑
- Reduce阶段
- reduce任务(reduce task)的个数由自己写的程序编程指定,main()内的job.setNumReduceTasks(4)指定reduce任务是4个(reduce1、reduce2、reduce3、reduce4)
- 每一个reduce任务的逻辑一样,所以以第一个reduce任务(reduce1)为例分析
- map1任务完成后,reduce1通过网络,连接到map1,将map1输出结果中属于reduce1的分区的数据,通过网络获取到reduce1端(拷贝阶段)
- 同样也如此连接到map2、map3获取结果
- 最终reduce1端获得4个(Dear, 1)键值对;由于key键相同,它们分到同一组;
- 4个(Dear, 1)键值对,转换成[Dear, Iterable(1, 1, 1, )],作为两个参数传入reduce()
- 在reduce()内部,计算Dear的总数为4,并将(Dear, 4)作为键值对输出
- 每个reduce任务最终输出文件(内部里还有细节,讲到shuffle时,再细细展开),文件写入到HDFS
2.2 MR中key的作用
-
MapReduce编程中,key有特殊的作用:
-
①数据中,若要针对某个值进行分组、聚合时,需将此值作为MR中的reduce的输入的key
-
如当前的词频统计例子,按单词进行分组,每组中对出现次数做聚合(计算总和);所以需要将每个单词作为reduce输入的key,MapReduce框架自动按照单词分组,进而求出每组即每个单词的总次数
-
②另外,key还具有可排序的特性,因为MR中的key类需要实现WritableComparable接口;而此接口又继承Comparable接口(可查看源码)
-
MR编程时,要充分利用以上两点;结合实际业务需求,设置合适的key
-
2.3 创建MAVEN工程
- 使用IDEA创建maven工程
- pom.xml如下,主要用到的dependencies有
<properties>
<cdh.version>2.6.0-cdh5.14.2</cdh.version>
</properties>
<repositories>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.0-mr1-cdh5.14.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${cdh.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${cdh.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>${cdh.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>compile</scope>
</dependency>
</dependencies>
2.4 MR参考代码
-
创建包com.zsc.wordcount
-
在包中创建自定义mapper类、自定义reducer类、包含main类
2.4.1 Mapper代码
package com.zsc.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* 类Mapper<LongWritable, Text, Text, IntWritable>的四个泛型分别表示
* map方法的输入的键的类型kin、值的类型vin;输出的键的类型kout、输出的值的类型vout
* kin指的是当前所读行行首相对于split分片开头的字节偏移量,所以是long类型,对应序列化类型LongWritable
* vin指的是当前所读行,类型是String,对应序列化类型Text
* kout根据需求,输出键指的是单词,类型是String,对应序列化类型是Text
* vout根据需求,输出值指的是单词的个数,1,类型是int,对应序列化类型是IntWritable
*
*/
public class WordCountMap extends Mapper<LongWritable, Text, Text, IntWritable> {
/**
* 处理分片split中的每一行的数据;针对每行数据,会调用一次map方法
* 在一次map方法调用时,从一行数据中,获得一个个单词word,再将每个单词word变成键值对形式(word, 1)输出出去
* 输出的值最终写到本地磁盘中
* @param key 当前所读行行首相对于split分片开头的字节偏移量
* @param value 当前所读行
* @param context
* @throws IOException
* @throws InterruptedException
*/
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//当前行的示例数据(单词间空格分割):Dear Bear River
//取得当前行的数据
String line = value.toString();
//按照\t进行分割,得到当前行所有单词
String[] words = line.split("\t");
for (String word : words) {
//将每个单词word变成键值对形式(word, 1)输出出去
//同样,输出前,要将kout, vout包装成对应的可序列化类型,如String对应Text,int对应IntWritable
context.write(new Text(word), new IntWritable(1));
}
}
}
2.4.2 Reducer代码
package com.zsc.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
*
* Reducer<Text, IntWritable, Text, IntWritable>的四个泛型分别表示
* reduce方法的输入的键的类型kin、输入值的类型vin;输出的键的类型kout、输出的值的类型vout
* 注意:因为map的输出作为reduce的输入,所以此处的kin、vin类型分别与map的输出的键类型、值类型相同
* kout根据需求,输出键指的是单词,类型是String,对应序列化类型是Text
* vout根据需求,输出值指的是每个单词的总个数,类型是int,对应序列化类型是IntWritable
*
*/
public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
/**
*
* key相同的一组kv对,会调用一次reduce方法
* 如reduce task汇聚了众多的键值对,有key是hello的键值对,也有key是spark的键值对,如下
* (hello, 1)
* (hello, 1)
* (hello, 1)
* (hello, 1)
* ...
* (spark, 1)
* (spark, 1)
* (spark, 1)
*
* 其中,key是hello的键值对被分成一组;merge成[hello, Iterable(1,1,1,1)],调用一次reduce方法
* 同样,key是spark的键值对被分成一组;merge成[spark, Iterable(1,1,1)],再调用一次reduce方法
*
* @param key 当前组的key
* @param values 当前组中,所有value组成的可迭代集和
* @param context reduce上下文环境对象
* @throws IOException
* @throws InterruptedException
*/
public void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//定义变量,用于累计当前单词出现的次数
int sum = 0;
for (IntWritable count : values) {
//从count中获得值,累加到sum中
sum += count.get();
}
//将单词、单词次数,分别作为键值对,输出
context.write(key, new IntWritable(sum));// 输出最终结果
};
}
2.4.3 Main程序入口
package com.zsc.wordcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
*
* MapReduce程序入口
* 注意:
* 导包时,不要导错了;
* 另外,map\reduce相关的类,使用mapreduce包下的,是新API,如org.apache.hadoop.mapreduce.Job;;
*/
public class WordCountMain {
//若在IDEA中本地执行MR程序,需要将mapred-site.xml中的mapreduce.framework.name值修改成local
//参数 c:/test/README.txt c:/test/wc
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, WordCountMain.class.getSimpleName());
//设置job的jar包,如果参数指定的类包含在一个jar包中,则此jar包作为job的jar包; 参数class跟主类在一个工程即可;一般设置成主类
job.setJarByClass(WordCountMain.class);
//通过job设置输入/输出格式
//MR的默认输入格式是TextInputFormat,输出格式是TextOutputFormat;所以下两行可以注释掉
// job.setInputFormatClass(TextInputFormat.class);
// job.setOutputFormatClass(TextOutputFormat.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(WordCountMap.class);
//设置map combine类,减少网路传出量
job.setCombinerClass(WordCountReduce.class);
//设置处理Reduce阶段的自定义的类
job.setReducerClass(WordCountReduce.class);
//注意:如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
//设置reduce task最终输出key/value的类型
//注意:此处设置的reduce输出的key/value类型,一定要与自定义reduce类输出的kv对类型一致;否则程序运行报错
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 提交作业
job.waitForCompletion(true);
}
}
- 程序运行有两种方式,分别是windows本地运行、集群运行(打包为jar包上传至集群运行)
3. MapReduce编程:数据清洗
应用:mapreduce在企业中,可以用于对海量数据的数据清洗;当然,随着新一代大数据框架的出现,也可以使用spark、flink等框架,做数据清洗
3.1 需求
- 现有一批日志文件,日志来源于用户使用搜狗搜索引擎搜索新闻,并点击查看搜索结果过程;
- 但是,日志中有一些记录损坏,现需要使用MapReduce来将这些损坏记录(如记录中少字段、多字段)从日志文件中删除,此过程就是传说中的数据清洗。
- 并且在清洗时,要统计损坏的记录数。
3.2 日志数据结构
-
日志格式:每行记录有6个字段;分别表示时间datetime、用户ID userid、新闻搜索关键字searchkwd、当前记录在返回列表中的序号retorder、用户点击链接的顺序cliorder、点击的URL连接cli-url
关于retorder、cliorder说明:
3.3 逻辑分析
-
MapReduce程序一般分为map阶段,将任务分而治之;reduce阶段,将map阶段的结果进行聚合
-
但有些mapreduce应用不需要数据聚合的操作,也就是说不需要reduce阶段。即编程时,不需要编写自定义的reducer类;在main()中调用job.setNumReduceTasks(0)设置。本例的数据清洗就是属于此种情况
-
统计损坏的记录数,可使用自定义计数器的方式实现
-
map方法的逻辑:取得每一行数据,与每条记录的固定格式比对,是否符合:
- 若符合,则是完好的记录,原样输出
- 若不符合,则是损坏的记录,不用做其他处理,对自定义计数器累加即可
3.4 MR代码
3.4.1 Mapper类
package com.zsc.dataclean;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Counter;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
*
* 现对sogou日志数据,做数据清洗;将不符合格式要求的数据删除
* 每行记录有6个字段;
* 分别表示时间datetime、用户ID userid、新闻搜索关键字searchkwd、当前记录在返回列表中的序号retorder、用户点击链接的顺序cliorder、点击的URL连接cliurl
* 日志样本:
* 20111230111308 0bf5778fc7ba35e657ee88b25984c6e9 nba直播 4 1 http://www.hoopchina.com/tv
*
*/
public class DataClean {
/**
*
* 基本上大部分MR程序的main方法逻辑,大同小异;将其他MR程序的main方法代码拷贝过来,稍做修改即可
* 实际开发中,也会有很多的复制、粘贴、修改
*
* 注意:若要IDEA中,本地运行MR程序,需要将resources/mapred-site.xml中的mapreduce.framework.name属性值,设置成local
* @param args
*/
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, DataClean.class.getSimpleName());
//设置jar包,参数是包含main方法的类
job.setJarByClass(DataClean.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(DataCleanMapper.class);
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//注意:因为不需要reduce聚合阶段,所以,需要显示设置reduce task个数是0
job.setNumReduceTasks(0);
// 提交作业
job.waitForCompletion(true);
}
/**
*
* 自定义mapper类
* 注意:若自定义的mapper类,与main方法在同一个类中,需要将自定义mapper类,声明成static的
*/
public static class DataCleanMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
//为了提高程序的效率,避免创建大量短周期的对象,出发频繁GC;此处生成一个对象,共用
NullWritable nullValue = NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//自定义计数器,用于记录残缺记录数
Counter counter = context.getCounter("DataCleaning", "damagedRecord");
//获得当前行数据
//样例数据:20111230111645 169796ae819ae8b32668662bb99b6c2d 塘承高速公路规划线路图 1 1 http://auto.ifeng.com/roll/20111212/729164.shtml
String line = value.toString();
//将行数据按照记录中,字段分隔符切分
String[] fields = line.split("\t");
//判断字段数组长度,是否为6
if(fields.length != 6) {
//若不是,则不输出,并递增自定义计数器
counter.increment(1L);
} else {
//若是6,则原样输出
context.write(value, nullValue);
}
}
}
}
4.4.2 运行结果
-
①reduce 0%,job就已经successfully,表示此MR程序没有reduce阶段
-
②DataCleaning是自定义计数器组名;damagedRecord是自定义的计数器;值为6,表示有6条损坏记录
-
图中part-m-00000中的m表示,此文件是由map任务生成
4. MapReduce编程:用户搜索次数
4.1 需求
使用MR编程,统计sogou日志数据中,每个用户搜索的次数,结果写入HDFS
4.2 数据结构
- 数据来源自“MapReduce编程:数据清洗”中的输出结果,仍然是sogou日志数据
4.3 逻辑分析
- MR编程时,若要针对某个值对数据进行分组、聚合时,如当前的词频统计例子,需要将每个单词作为reduce输入的key,从而按照单词分组,进而求出每组即每个单词的总次数
- 此例类似上述情况:
- 统计每个用户的搜索次数,将userid放到reduce输入的key的位置;
- 对userid进行分组
- 进而统计每个用户的搜索次数
4.4 MR代码
package com.zsc.searchcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
*
* 本MR示例,用于统计每个用户搜索并查看URL链接的次数
*/
public class UserSearchCount {
/**
*
* @param args C:\test\datacleanresult c:\test\usersearch
* @throws IOException
* @throws ClassNotFoundException
* @throws InterruptedException
*/
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, UserSearchCount.class.getSimpleName());
//设置jar包,参数是包含main方法的类
job.setJarByClass(UserSearchCount.class);
//通过job设置输入/输出格式
//MR的默认输入格式是TextInputFormat,输出格式是TextOutputFormat;所以下两行可以注释掉
// job.setInputFormatClass(TextInputFormat.class);
// job.setOutputFormatClass(TextOutputFormat.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(SearchCountMapper.class);
//设置map combine类,减少网路传出量
//job.setCombinerClass(WordCountReduce.class);
//设置处理Reduce阶段的自定义的类
job.setReducerClass(SearchCountReducer.class);
//如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
//设置reduce task最终输出key/value的类型
//注意:此处设置的reduce输出的key/value类型,一定要与自定义reduce类输出的kv对类型一致;否则程序运行报错
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//提交作业
job.waitForCompletion(true);
}
public static class SearchCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
//定义共用的对象,减少GC压力
Text userIdKOut = new Text();
IntWritable vOut = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获得当前行的数据
//样例数据:20111230111645 169796ae819ae8b32668662bb99b6c2d 塘承高速公路规划线路图 1 1 http://auto.ifeng.com/roll/20111212/729164.shtml
String line = value.toString();
//切分,获得各字段组成的数组
String[] fields = line.split("\t");
//因为要统计每个user搜索并查看URL的次数,所以将userid放到输出key的位置
//注意:MR编程中,根据业务需求设计key是很重要的能力
String userid = fields[1];
//设置输出的key的值
userIdKOut.set(userid);
//输出结果
context.write(userIdKOut, vOut);
}
}
public static class SearchCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
//定义共用的对象,减少GC压力
IntWritable totalNumVOut = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for(IntWritable value: values) {
sum += value.get();
}
//设置当前user搜索并查看总次数
totalNumVOut.set(sum);
context.write(key, totalNumVOut);
}
}
}
4.4.1 结果
- 运行参数
C:\test\datacleanresult c:\test\usersearch
- 运行结果:
5. Shuffle
shuffle主要指的是map端的输出作为reduce端输入的过程
5.1 shuffle简图
5.2 shuffle细节图
-
分区使用了分区器,默认分区器是HashPartitioner
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> { public void configure(JobConf job) {} /** Use {@link Object#hashCode()} to partition. */ public int getPartition(K2 key, V2 value, int numReduceTasks) { return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; } }
5.3 map端
- 每个map任务都有一个对应的环形内存缓冲区;输出是kv对,先写入到环形缓冲区(默认大小100M),当内容占据80%缓冲区空间后,由一个后台线程将缓冲区中的数据溢出写到一个磁盘文件
- 在溢出写的过程中,map任务可以继续向环形缓冲区写入数据;但是若写入速度大于溢出写的速度,最终造成100m占满后,map任务会暂停向环形缓冲区中写数据的过程;只执行溢出写的过程;直到环形缓冲区的数据全部溢出写到磁盘,才恢复向缓冲区写入
- 后台线程溢写磁盘过程,有以下几个步骤:
- 先对每个溢写的kv对做分区;分区的个数由MR程序的reduce任务数决定;默认使用HashPartitioner计算当前kv对属于哪个分区;计算公式:(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks
- 每个分区中,根据kv对的key做内存中排序;
- 若设置了map端本地聚合combiner,则对每个分区中,排好序的数据做combine操作;
- 若设置了对map输出压缩的功能,会对溢写数据压缩
- 随着不断的向环形缓冲区中写入数据,会多次触发溢写(每当环形缓冲区写满100m),本地磁盘最终会生成多个溢出文件
- 合并溢写文件:在map task完成之前,所有溢出文件会被合并成一个大的溢出文件;且是已分区、已排序的输出文件
- 小细节:
- 在合并溢写文件时,如果至少有3个溢写文件,并且设置了map端combine的话,会在合并的过程中触发combine操作;
- 但是若只有2个或1个溢写文件,则不触发combine操作(因为combine操作,本质上是一个reduce,需要启动JVM虚拟机,有一定的开销)
5.4 reduce端
-
reduce task会在每个map task运行完成后,通过HTTP获得map task输出中,属于自己的分区数据(许多kv对)
-
如果map输出数据比较小,先保存在reduce的jvm内存中,否则直接写入reduce磁盘
-
一旦内存缓冲区达到阈值(默认0.66)或map输出数的阈值(默认1000),则触发归并merge,结果写到本地磁盘
-
若MR编程指定了combine,在归并过程中会执行combine操作
-
随着溢出写的文件的增多,后台线程会将它们合并大的、排好序的文件
-
reduce task将所有map task复制完后,将合并磁盘上所有的溢出文件
-
默认一次合并10个
-
最后一批合并,部分数据来自内存,部分来自磁盘上的文件
-
进入“归并、排序、分组阶段”
-
每组数据调用一次reduce方法
5.5 Shuffle总结
- map端
- map()输出结果先写入环形缓冲区
- 缓冲区100M;写满80M后,开始溢出写磁盘文件
- 此过程中,会进行分区、排序、combine(可选)、压缩(可选)
- map任务完成前,会将多个小的溢出文件,合并成一个大的溢出文件(已分区、排序)
- reduce端
- 拷贝阶段:reduce任务通过http将map任务属于自己的分区数据拉取过来
- 开始merge及溢出写磁盘文件
- 所有map任务的分区全部拷贝过来后,进行阶段合并、排序、分组阶段
- 每组数据调用一次reduce()
- 结果写入HDFS