在hadoop Mapreduce优化技术中,总会涉及到Combiner和Partitioner,Combiner和Partitioner是用来优化MapReduce的。可以提高MapReduce的运行效率,下面就来谈谈这两种技术及其简单的使用。
1 Combiner技术
Combiner是一个本地化的reduce操作,它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作。集群上的可用带宽限制了MapReduce作业的数量,因此尽量避免map和reduce任务之间的数据传输是有利的。Hadoop允许用户针对map任务的输出指定一个combiner(就像mapper,reducer)。combiner函数的输出作为reduce函数的输入。由于combiner术语优化方案,所以Hadoop无法确定对map任务输出记录调用多少次combiner(如果需要)。换言之,不管调用多次combiner,reducer的输出结果都是一样的。
首先通过下面的示意图直观的了解一下Combiner的位置和作用。
从下图可以看出,Combiner介于 Mapper和Reducer之间,combine作为 Map任务的一部分,执行完 map 函数后紧接着执行combine,而reduce 必须在所有的 Map 任务完成后才能进行。 而且还可以看出combine的过程与reduce的过程类似,都是对相同的单词key合并其词频,很多情况下可以直接使用reduce函数来完成Combiner过程。
1.1 Combiner解析:
1. ombiner可以看做局部的Reducer(local reducer)。
2. Combiner作用是合并相同的key对应的value。
3. 在Mapper阶段,不管Combiner被调用多少次,都不应改变 Reduce的输出结果。
4. Combiner通常与Reducer的逻辑是一样的,一般情况下不需要单独编写Combiner,直接使用Reducer的实现就可以了。
5. Combiner在Job中是如下设置的。
job.setCombinerClass(Reducer.class);//Combiner一般情况下,默认使用Reducer的实现
1.2 Combiner的优点
能够减少Map Task输出的数据量(即磁盘IO)。对spill,merge文件都可以进行压缩。
中间结果非常大导致IO成为瓶颈时压缩非常有用,可以通过mapreduce.map.output.compress(default:false)设置为true进行压缩,数据会被压缩写入磁盘,读数据读的是压缩数据需要解压,在实际经验中Hive在Hadoop的运行的瓶颈一般都是IO而不是CPU,压缩一般可以10倍的减少IO操作,压缩的方式Gzip,Lzo,BZip2,Lzma等,其中Lzo是一种比较平衡选择,mapreduce.map.output.compress.codec(default:org.apache.hadoop.io.compress.DefaultCodec)参数设置。但这个过程会消耗CPU,适合IO瓶颈比较大。
能够减少Reduce-Map网络传输的数据量(网络IO)。Map Task 输出越少,Reduce从Map结果中拉取的数据量就越少,自然就减少了网络传输的数据量。
1.3 Combiner的使用场景
并不是所有的场景都可以使用Combiner,必须满足结果可以累加。适合于Sum()求和,并不适合Average()求平均数。
例如,求0、20、10、25和15的平均数,直接使用Reduce求平均数Average(0,20,10,25,15),得到的结果是14, 如果先使用Combiner分别对不同Mapper结果求平均数,Average(0,20,10)=10,Average(25,15)=20,再使用Reducer求平均数Average(10,20),得到的结果为15,很明显求平均数并不适合使用Combiner。
2 Partitioner
Partitioner 处于 Mapper阶段,当Mapper处理好数据后,这些数据需要经过Partitioner进行分区,来选择不同的Reducer处理,从而将Mapper的输出结果均匀的分布在Reducer上面执行。
对于map输出的每一个键值对,系统都会给定一个partition,partition值默认通过计算key的hash值后对Reduce task的数量取模获得。如果一个键值对的partition值为1,意味着这个键值对会交给第一个Reducer处理。
Partitioner解析:
Partitioner决定了Map Task 输出的每条数据交给哪个Reduce Task 来处理。Partitioner 有两个功能:
1) 均衡负载。它尽量将工作均匀地分配给不同的 Reduce。
2)效率。它的分配速度一定要非常快。
Partitioner 的默认实现:hash(key) mod R,这里的R代表Reduce Task 的数目,意思就是对key进行hash处理然后取模。很多情况下,用户需要自定义 Partitioner,比如“hash(hostname(URL)) mod R”,它确保相同域名下的网页交给同一个 Reduce Task 来处理。 用户自定义Partitioner,需要继承Partitioner类,实现它提供的一个方法。
public class WordCountPartitioner extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text key, IntWritable value, int numPartitions) {
// TODO Auto-generated method stub
int a = key.hashCode()%numPartitions;
if(a>=0)
return a;
else
return 0;
}
}
前两个参数分别为Map的key和value。args 为 Reduce 的个数,用户可以自己设置。
如何自定义partitioner?
每一个Reduce的输出都是有序的,但是将所有Reduce的输出合并到一起却并非是全局有序的,如果要做到全局有序,我们该怎么做呢?最简单的方式,只设置一个Reduce task,但是这样完全发挥不出集群的优势,而且能应对的数据量也很受限。最佳的方式是自己定义一个Partitioner,用输入数据的最大值除以系统Reduce task数量的商作为分割边界,也就是说分割数据的边界为此商的1倍、2倍至numPartitions-1倍,这样就能保证执行partition后的数据是整体有序的。
解决数据倾斜:另一种需要我们自己定义一个Partitioner的情况是各个Reduce task处理的键值对数量极不平衡。对于某些数据集,由于很多不同的key的hash值都一样,导致这些键值对都被分给同一个Reducer处理,而其他的Reducer处理的键值对很少,从而拖延整个任务的进度。当然,编写自己的Partitioner必须要保证具有相同key值的键值对分发到同一个Reducer。
自定义的Key包含了好几个字段,比如自定义key是一个对象,包括type1,type2,type3,只需要根据type1去分发数据,其他字段用作二次排序。