前言
mapreduce是hadoop的计算框架,它与hdfs关系紧密。可以说它的计算思想是基于分布式文件而设计的。
MR计算模型
MapReduce最早是由Google公司研究提出的一种面向大规模数据处理的并行计算模型和方法。也可以说它是:“分布式计算的始祖”。
计算流程是:输入分片 —> map阶段 —> combiner阶段(可选) —> shuffle阶段 —> reduce阶段
输入分片(input split)
在进行map计算之前,mapreduce会根据输入文件计算输入分片,每个输入分片针对一个map任务。
- 关于 mapreduce分片算法为:
假如我们设定hdfs的块的大小是64mb,输入有三个文件,大小分别是3mb、65mb和127mb,那么mapreduce会把3mb文件分为一个输入分片,65mb则是两个输入分片而127mb也是两个输入分片.换句话说我们可以通过合并小文件做输入分片调整,那么就不会有5个map任务,而且每个map执行的数据大小不均的情况发生。- 关于读取其它数据源:
例如mysql,先将mysql数据读取到hdfs上,然后通过以上分片算法分片
map阶段
map操作都是本地化操作也就是在数据存储节点上进行,负责将当前存储节点上的数据,整理成K,V格式。要程序员编写。
K,V格式:
- K:存储用于关键的字段信息(类似于sql中, join的条件字段)
- V:其它数据信息 (类似于sql中,整行)
combiner阶段
combiner阶段是程序员可选择的,combiner其实也是一种reduce操作。它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作。例如我们对文件里的单词频率做统计,map计算时候如果碰到一个hadoop的单词就会记录为1,但是这篇文章里hadoop可能会出现n多次,那么map输出文件冗余就会很多,因此在reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了宽带的传输效率,毕竟hadoop计算力宽带资源往往是计算的瓶颈也是最为宝贵的资源,但是combiner操作是有风险的,使用它的原则是combiner的输入不会影响到reduce计算的最终输入,例如:如果计算只是求总数,最大值,最小值可以使用combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。
combine时一个本地化的reduce操作,对相同的key做一个合并操作,提高带宽的利用率
shuffle阶段
将map的输出作为reduce的输入的过程就是shuffle了,这个是mapreduce优化的重点地方。
- 每个 Map 任务的计算结果都会写入到本地文件系统
map写入磁盘的过程十分的复杂,内存开销是很大的,map在做输出时候会在内存里开启一个环形内存缓冲区,这个缓冲区专门用来输出的,默认大小是100mb,并且在配置文件里为这个缓冲区设定了一个阀值,默认是0.80,如果缓冲区的内存达到了阀值的80%时候,这个守护线程就会把内容写到磁盘上,这个过程叫spill
- 等Map任务快要计算完成的时候,MapReduce 计算框架会启动 shuffle 过程.在 Map 任务进程调用一个 Partitioner 接口,对 Map 产生的每个 <key, value> 进行 Reduce 分区选择,然后通过 HTTP 通信发送给对应的 Reduce 进程。
- MapReduce 框架默认的 Partitioner 用 Key 的哈希值对 Reduce任务数量取模,相同的 Key 一定会落在相同的 Reduce 任务 ID 上
- 如果reduce对顺序有要求,可以定义每个partition的边界,大的数据到一个Reduce,后序只要把各个reduce的结果相加就行。可能会导致每个partition上分配到的记录数相差很大,hadoop提供了采样器帮我们预估整个边界,以使数据的分配尽量平均。
-
Reduce 任务进程对收到的数据进行排序和合并,相同的 Key 放在一起,组成一个 传递给 Reduce 执行
-
如果我们定义了combiner函数,那么排序前还会执行combiner操作。1步骤中的数据结点,也会进行2,3步骤的模拟。产生结果后再继续2,3步骤。
reduce阶段
针对shuffle阶段准备好的输入开始计算。
用MR实现left-join
我们要把数据库的数据存储和计算进行分离。假如计算引挚选用MR.怎么做呢?
Hive就是基于MR和HDFS思想实现的
假设对如下2张表做leftjoin
factory表:
factoryname addressed
Beijing Red Star 1
Shenzhen Thunder 3
Guangzhou Honda 2
Beijing Rising 1
Guangzhou Development Bank 2
Tencent 3
Back of Beijing 1
address表:
addressID addressname
1 Beijing
2 Guangzhou
3 Shenzhen
4 Xian
取出两个表中共同列作为map中的key,同时需要标识每个列所在的表,供在reduce中拆分
//汇聚所有addressID相同的
protected void map(LongWritable key, Text value,Context context)
throws IOException, InterruptedException {
String path = ((FileSplit)context.getInputSplit()).getPath().getName();//获取文件名
String line = value.toString();
StringTokenizer st = new StringTokenizer(value.toString());
String[] tmp = line.split(" +");
if(tmp.length ==2){
String first = tmp[0];
String second = tmp[1];
if(path.equals("factory")){
if(first.equals("factoryname")) return;
k.set(second);
v.set(first+"1");
}else if(path.equals("address")){
if(second.equals("addressname")) return;
k.set(first);
v.set(second+"2");
}
context.write(k,v);
}
}
//以factory为主表拆分
protected void reduce(Text key, Iterable<Text> value,Context context)
throws IOException, InterruptedException {
List<String> factory = new ArrayList<String>();
List<String> address = new ArrayList<String>();
for(Text val : value){
String str = val.toString();
String stf = str.substring(str.length()-1);
String con = str.substring(0,str.length()-1);
int flag = Integer.parseInt(stf);
if(flag == 1){
factory.add(con);
}else if(flag ==2){
address.add(con);
}
}
for(int i=0;i<factory.size();i++){
k.set(factory.get(i));
for(int j=0;j<address.size();j++){
v.set(address.get(j));
context.write(k, v);
}
}
}
再来聊combiner
在map端使用combiner合并数据可以减少需要通过网络io的数据,有效增加map reduce程序的运行效率。
combiner默认直接采用已有的reducer代码,而采用这种相同逻辑的combiner要求提前执行combiner程序,合并的数据不会影响到reducer端最终的合并。
如果上述“left join”案例使用combiner,将会改变reduce的输入。另结果异常。比如统计单词、求最大/小值等,这些程序的数据提前合并不会影响到reducer端的最终合并。再以比如求平均数为例子
public class AverageMapper extends Mapper<LongWritable, Text, Text, FloatWritable> {
private Text text = new Text();
private FloatWritable number = new FloatWritable();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] row = value.toString().split("\t");
String student = row[0];
String score = row[2];
text.set(student);
number.set(Float.parseFloat(score));
context.write(text, number);
}
}
public class AverageReducer extends Reducer<Text, FloatWritable, Text, FloatWritable> {
private FloatWritable avg = new FloatWritable();
@Override
protected void reduce(Text key, Iterable<FloatWritable> values, Context context) throws IOException, InterruptedException {
float sum = 0;
int count = 0;
for (FloatWritable value : values) {
sum += value.get();
count++;
}
avg.set(sum / count);
context.write(key, avg);
}
}
如果使用
job.setCombinerClass(AverageReducer.class);
最终的结果:可以发现最终结果比正确的平均值变小了。主要错误是在map端使用combiner程序进行提前聚合。
public class CombinerMapper extends Mapper<LongWritable, Text, Text, Text> {
private Text student = new Text();
private Text score = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] split = value.toString().split("\t");
student.set(split[0]);
score.set(split[2]);
context.write(student, score);
}
}
public class AverageCombiner extends Reducer<Text, Text, Text, Text> {
private Text text = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
int sum = 0;
int count = 0;
for (Text value : values) {
sum += Integer.parseInt(value.toString());
count++;
}
text.set("" + sum + "\t" + count);
context.write(key, text);
}
}
public class CombinerReducer extends Reducer<Text, Text, Text, FloatWritable> {
private FloatWritable avg = new FloatWritable();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
float sum = 0;
float count = 0;
for (Text value : values) {
String[] split = value.toString().split("\t");
sum += Integer.parseInt(split[0]);
count += Integer.parseInt(split[1]);
}
avg.set(sum / count);
context.write(key, avg);
}
}
使用上述自定义combiner求平均值的mapreduce程序就能得到最大的结果。
Partition机制
除了上述自定义combiner方式,mapReduce还可以自定义分区 Partition.
mapreducer shuffle数据向reducer输出的时是根据HashPartitioner分区器来进行数据的分区的。
如果碰到:需要将相同号码段的手机号码放到同一个文件中,比如135开头的一个文件,136开头的文件
public class CusPartition extends Partitioner<Text, LongWritable> {
@Override
public int getPartition(Text key, LongWritable value, int numPartitions) {
int partition = 3;
// 获取key的前三位 135 -》0 136 -》1 137 -》2 138 -》 3
String index = key.toString().substring(0, 3);
if ("135".equals(index)){
partition = 0 ;
} else if ("136".equals(index)){
partition = 1;
} else if ("137".equals(index)){
partition = 2;
}
return partition;
}
}
可以通过
job.setPartitionerClass(CusPartition.class);
来设默认分区规则,来达到我们的效果
MR排序
排序是 MR 中非常重要的操作之一
- MapTask 和 ReduceTask 都会对数据按照 key 进行排序。该操作是默认行为。任何 MR 程序中数据均会被排序,而不看逻辑是否需要。
- MapTask 中,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率到一定的阈值,再对缓冲区数据进行一次快排,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
- ReduceTask 中,它从每个 MapTask 上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则储存在内存上。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件。如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完后,ReduceTask 统一对内存和磁盘上的所有数据进行一次归并排序。
ps: 每一个ReduceTask生成一个文件,每一个MapTask对应一个文件
部分排序
MapReduce 根据输入记录的键对数据集排序,保证输出的每个文件内部有序。
// key需要实现 WritableComparable 接口重写 compareTo 方法,就可以实现部分排序
@Override
public int compareTo(FlowBean o) {
int result;
// 按照总流量大小,倒序排列
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
全局排序
只设置一个ReduceTask最终输出结果只要一个文件,且文件内部有序.
该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了 MapReduce 所提供的并行架构。
通过Partition + 部分排序的方式,生成多个有序文件。然后按分区顺序将文件合并。
辅助排序
在 Reduce 端对 key 进行分组。应用于:在接收的 key 为 bean 对象时,想让一个或几个字段相同(全部字段比较不相同)的 key 进入到同一个 reduce 方法时,可以采用分组排序。
// 实现WritableComparator接口,并通过以下函数设置
job.setGroupingComparatorClass(OrderGroupingComparator.class);
二次排序
对进入同一个reduce的 键 或键的部分 进行排序,即 map的排序不是我reduce输入想要的排序
job.setSortComparatorClass(SortComparator.class);
主要参考
《hadoop权威指南》
《Mapreduce的排序(全局排序、分区加排序、Combiner优化)
》
《MR – WritableComparable排序》