MapReduce是一个分布式并行计算引擎,基于它写出来的应用程序能够运行在由上千个机器组成的大型集群上,并以一种可靠、容错、并行的方式处理TB级别的数据集。
-
一、编程模型
这里我们以 WordCount 为实例:
有一批文件,规模为 TB 级或者 PB 级,如何统计这些文件中所有单词出现的次数。
-
很理所应当的一个想法是,先统计每个文件中单词的频次,再合并累加不同文件中同一个单词出现的频次。其实这种逻辑就是典型的Map然后Reduce逻辑。
-
1.1 MR的典型4阶段
请看下图:
(1)Split分割阶段:把一个大文件分割为3块,分割后的3块数据就可以并行处理,每一块交给一个 Mapper处理。
(2)Map阶段:以每个单词为key,以1作为词频数value,然后输出。
(3)Shuffle阶段 [‘ʃʌf(ə)l]:将相同的单词key放在一个桶里面,然后交给Reducer处理。
(4)Reduce阶段:Reducer接受到shuffle后的数据,会将相同的单词进行合并,得到每个单词的词频数,最后将统计好的每个单词的词频数作为输出结果。也就是说,Reducer的作用是计算相同Key的value的总和。
-
源码如下:
import java.io.IOException;
import java.util.StringTokenizer;
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.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCount {
// Mapper
public static class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable>{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
// Mapper类默认使用TextInputFormat,它会将文件中的数据一行一行地处理
public void map(Object key, Text value, Context context
) throws IOException, InterruptedException {
// StringTokenizer会根据空格将一行文本拆分为一个个word
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
// 以word为key,以 one 为value
context.write(word, one);
}
}
}
// Reducer
public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values,
Context context
) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
// 设置 Combiner
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
分析上述代码,可以看出MR的如下特点:
(1)MR所有的输出/输出都是以 <K, V >
的形式。
(2)要实现一个简单的MR程序,只需:
a. 定义Mapper和Reducer;
b. 指定输出路径和输出路径;
-
1.2 Combiner
Mapper和Reducer往往并不在一个计算节点上, 依赖网络进行数据传输。Combiner的作用就是在一个节点上,对本地的Mapper进行一次合并,所以往往我们会直接将编写的Reducer类指定为Combiner。举个例子:
没有Combiner的情况下一个节点Mapper的输出是:
< Hello, 1>
< World, 1>
< Bye, 1>
< World, 1>
有了Combiner之后输出是:
< Bye, 1>
< Hello, 1>
< World, 2>
这样可以减少数据的冗余和网络传输的负担。
-
1.3 Partitioner
官方文档:
The
Mapper
outputs are sorted and then partitioned perReducer
. The total number of partitions is the same as the number of reduce tasks for the job. Users can control which keys (and hence records) go to whichReducer
by implementing a customPartitioner
.
也就是说,Mapper输出的 k,v 对交由哪个Reducer处理是由Partitioner决定的。分区数和Reducer的个数一致。Reducer和Mapper的个数是可以设置的。
HashPartitioner is the default
Partitioner
.
1.4 Reducer
Reducer实际上负责三个操作:
- Shuffle:通过HTTP获取输入。
- Sort:对输入的 k,v 对排序。
- Reduce:计算相同key的value和。
二、Yarn进行任务调度
YARN是一个资源管理、任务调度的框架,主要包含三大模块:ResourceManager(RM)、NodeManager(NM)、ApplicationMaster(AM)。其中,ResourceManager负责所有资源的监控、分配和管理;ApplicationMaster负责每一个具体应用程序的调度和协调;NodeManager负责每一个节点的维护。对于所有的applications,RM拥有绝对的控制权和对资源的分配权。而每个AM则会和RM协商资源,同时和NodeManager通信来执行和监控task。几个模块之间的关系如图所示:
- The MapReduce framework consists of a single master ResourceManager
, one worker NodeManager
per cluster-node, and MRAppMaster
per application。
- The framework takes care of scheduling tasks, monitoring them and re-executes the failed tasks.
三、应用场景
(1)WordCount:统计文件中单词的频率
(2)数据去重
(3)数据排序
(4)单表关联:要求从给出的数据中寻找所关心的数据。
(5)多表关联
上述应用的代码看这里:https://blog.csdn.net/lilianforever/article/details/51871944
(6)自然连接
下面着重分析一下排序问题。
全排序
正常情况下,Mapreduce的保障之一就是送到Reducer端的数据总是根据Reducer的输入键进行排序的,如果我们使用单个Reducer,排序就会直接了当,但是只是使用一个Reducer的情况少之又少,如果使用了多个Reducer,那么就只可能会保证每一个Reducer内的内容是会根据键进行排序的,而不会保证Reducder之间也是有序的。
全排序的技巧包含在Partitioner的实现中,我们需要将键的取值范围转换为一个索引(0-25),例如这里的键就是所有的英文单词,不过我们需要得出划分几个索引范围,然后这些索引分配给相应的reducer。
接下来的一个问题是:
我们可能需要动态的指定reducer的输入键的索引的范围,这里我们需要将我们的partitioner实现Configurable接口,因为在初始化的过程中,hadoop框架就会加载我们自定义的Partitioner实例,当hadoop框架通过反射机制实例化这个类的时候,它就会检查这个类型是不是Configurable实例,如果是的话,就会调用setConf,将作业的Configuration对象设置过来,我们就可以在Partitioner中获取到配置的变量了。
代码实现:
public class GlobalSort {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
configuration.set("key.indexRange","26");
Job job = Job.getInstance(configuration);
job.setNumReduceTasks(2);
job.setJarByClass(GlobalSort.class);
job.setMapperClass(GlobalSortMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setReducerClass(GlobalSortReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
job.setPartitionerClass(GlobalSortPartitioner.class);
FileInputFormat.setInputPaths(job,new Path("F:\\wc\\input"));
FileOutputFormat.setOutputPath(job,new Path("F:\\wc\\output"));
job.waitForCompletion(true);
}
}
class GlobalSortMapper extends Mapper<LongWritable,Text,Text,LongWritable>{
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//value是获取的一行的数据的内容,此处可以split
String[] splits = value.toString().split(" ");
for(String str : splits){
context.write(new Text(str), new LongWritable(1L));
}
}
}
class GlobalSortPartitioner extends Partitioner<Text,LongWritable> implements Configurable {
private Configuration configuration = null;
private int indexRange = 0;
public int getPartition(Text text, LongWritable longWritable, int numPartitions) {
//假如取值范围等于26的话,那么就意味着只需要根据第一个字母来划分索引
int index = 0;
if(indexRange==26){
index = text.toString().toCharArray()[0]-'a';
}else if(indexRange == 26*26 ){
//这里就是需要根据前两个字母进行划分索引了
char[] chars = text.toString().toCharArray();
if (chars.length==1){
index = (chars[0]-'a')*26;
}
index = (chars[0]-'a')*26+(chars[1]-'a');
}
int perReducerCount = indexRange/numPartitions;
if(indexRange<numPartitions){
return numPartitions;
}
for(int i = 0;i<numPartitions;i++){
int min = i*perReducerCount;
int max = (i+1)*perReducerCount-1;
if(index>=min && index<=max){
return i;
}
}
//这里我们采用的是第一种不太科学的方法
return numPartitions-1;
}
public void setConf(Configuration conf) {
this.configuration = conf;
indexRange = configuration.getInt("key.indexRange",26*26);
}
public Configuration getConf() {
return configuration;
}
}
class GlobalSortReducer extends Reducer<Text,LongWritable,Text,LongWritable>{
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
for(LongWritable value : values){
count += value.get();
}
context.write(key, new LongWritable(count));
}
}
输入的文件:
hello a
hello abc
helo jyw
he lq
mo no m n
zz za
输出:
part-r-00000
a 1
abc 1
he 1
hello 2
helo 1
jyw 1
lq 1
m 1
mo 1
part-r-00001
n 1
no 1
za 1
zz 1