1.Combiner
Combiner的出现:为什么需要进行Map规约操作
在上述过程中,我们看到至少两个性能瓶颈:
(1)如果我们有10亿个数据,Mapper会生成10亿个键值对在网络间进行传输,但如果我们只是对数据求最大值,那么很明显的Mapper只需要输出它所知道的最大值即可。这样做不仅可以减轻网络压力,同样也可以大幅度提高程序效率。 总结:网络带宽严重被占降低程序效率;
(2)假设使用美国专利数据集中的国家一项来阐述数据倾斜这个定义,这样的数据远远不是一致性的或者说平衡分布的,由于大多数专利的国家都属于美国,这样不仅Mapper中的键值对、中间阶段(shuffle)的键值对等,大多数的键值对最终会聚集于一个单一的Reducer之上,压倒这个Reducer,从而大大降低程序的性能。
总结:单一节点承载过重降低程序性能;
在MapReduce编程模型中,在Mapper和Reducer之间有一个非常重要的组件,它解决了上述的性能瓶颈问题,它就是Combiner。
①与mapper和reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。
②并不是所有的job都适用combiner,只有操作满足结合律的才可设置combiner。
combine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值的话,可以使用,但是如果是求中值的话,不适用。
每一个map都可能会产生大量的本地输出,Combiner的作用就是对map端的输出先做一次合并,以减少在map和reduce节点之间的数据传输量,以提高网络IO性能
Combiner的作用:
(1)Combiner实现本地key的聚合,对map输出的key排序value进行迭代
如下所示:
map: (K1, V1) → list(K2, V2) combine: (K2, list(V2)) → list(K2, V2) reduce: (K2, list(V2)) → list(K3, V3)
(2)Combiner还有本地reduce功能(其本质上就是一个reduce)
例如wordcount的例子和找出value的最大值的程序
combiner和reduce完全一致,如下所示:
map: (K1, V1) → list(K2, V2) combine: (K2, list(V2)) → list(K3, V3) reduce: (K3, list(V3)) → list(K4, V4)
使用combiner之后,先完成的map会在本地聚合,提升速度。对于hadoop自带的wordcount的例子,value就是一个叠加的数字,所以map一结束就可以进行reduce的value叠加,而不必要等到所有的map结束再去进行reduce的value叠加。
// 设置Map规约Combiner
job.setCombinerClass(MyReducer.class);
执行后看到map的输出和combine的输入统计是一致的,而combine的输出与reduce的输入统计是一样的。
由此可以看出规约操作成功,而且执行在map的最后,reduce之前。
小结:
在实际的Hadoop集群操作中,我们是由多台主机一起进行MapReduce的,如果加入规约操作,每一台主机会在reduce之前进行一次对本机数据的规约,然后在通过集群进行reduce操作,这样就会大大节省reduce的时间,从而加快MapReduce的处理速度
2.Partitioner与自定义Partitioner
Map阶段总共五个步骤(重要): step1.3就是一个分区操作
哪个key到哪个Reducer的分配过程,是由Partitioner规定的。
Hadoop内置Partitioner
MapReduce的使用者通常会指定Reduce任务和Reduce任务输出文件的数量(R)。用户在中间key上使用分区函数来对数据进行分区,之后在输入到后续任务执行进程。一个默认的分区函数式使用hash方法(比如常见的:hash(key) mod R)进行分区。hash方法能够产生非常平衡的分区。
将key均匀布在Reduce Tasks上
(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
如果Key为Text的话,Text的hashcode方法跟String的基本一致,都是采用的Horner公式计算,得到一个int整数。但是,如果string太大的话这个int整数值可能会溢出变成负数,所以和整数的上限值Integer.MAX_VALUE(即0111111111111111)进行与运算,然后再对reduce任务个数取余,这样就可以让key均匀分布在reduce上
自定制Partitioner
一般我们都会使用默认的分区函数HashPartitioner
自定义数据类型处理手机上网日志:
在第二列上并不是所有的数据都是手机号(84138413并不是一个手机号),任务就是在统计手机流量时,将手机号码和非手机号输出到不同的文件中
自定义LiuPartitioner
public static class LiuPartitioner extends Partitioner {
@Override
public int getPartition(Text key, KpiWritable value, int numPartitions) {
// 实现不同的长度不同的号码分配到不同的reduce task中
int numLength = key.toString().length();
if (numLength == 11) return 0;
else return 1;
}
}
设置为打包运行,设置Partitioner为LiuPartitioner设置ReducerTask的个数为2
注意:分区的例子必须要设置为打成jar包运行!
public int run(String[] args) throws Exception {
// 定义一个作业
Job job = new Job(getConf(), "MyJob");
// 分区需要设置为打包运行
job.setJarByClass(MyLiuJob.class);
// 设置输入目录
FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
// 设置自定义Mapper类
job.setMapperClass(MyMapper.class);
// 指定的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(KpiWritable.class);
// 设置Partitioner
job.setPartitionerClass(LiuPartitioner.class);
job.setNumReduceTasks(2);
// 设置自定义Reducer类
job.setReducerClass(MyReducer.class);
// 指定的类型
job.setOutputKeyClass(Text.class);
job.setOutputKeyClass(KpiWritable.class);
// 设置输出目录
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
// 提交作业
System.exit(job.waitForCompletion(true) ? 0 : 1);
return 0;
}
小结:分区Partitioner主要作用在于以下两点:
1. 根据业务需要,产生多个输出文件
2.多个reduce任务并发运行,提高整体job的运行效率
3.Shuffle过程
Reduce阶段三个步骤: Step2.1就是一个Shuffle[随机、洗牌]操作
针对多个map任务的输出按照不同的分区(Partition)通过网络复制到不同的reduce任务节点上,这个过程就称作为Shuffle。
shuffle过程
Map端
1.在map端首先是InputSplit,在InputSplit中含有DataNode中的数据,每一个InputSplit都会分配一个Mapper任务,Mapper任务结束后产生
2.写磁盘前,要进行partition、sort和combine等操作。通过分区,将不同类型的数据分开处理,之后对不同分区的数据进行排序,如果有Combiner,还要对排序后的数据进行combine。等最后记录写完,将全部溢出文件合并为一个分区且排序的文件
3.最后将磁盘中的数据送到Reduce中,图中Map输出有三个分区,有一个分区数据被送到图示的Reduce任务中,剩下的两个分区被送到其他Reducer任务中。而图示的Reducer任务的其他的三个输入则来自其他节点的Map输出。
Reduce端
1. Copy阶段:Reducer通过Http方式得到输出文件的分区。
reduce端可能从n个map的结果中获取数据,而这些map的执行速度不尽相同,当其中一个map运行结束时,reduce就会从JobTracker中获取该信息。map运行结束后TaskTracker会得到消息,进而将消息汇报给JobTracker,reduce定时从JobTracker获取该信息,reduce端默认有5个数据复制线程从map端复制数据
2.Merge阶段:如果形成多个磁盘文件会进行合并
从map端复制来的数据首先写到reduce端的缓存中,同样缓存占用到达一定阈值后会将数据写到磁盘中,同样会进行partition、combine、排序等过程。如果形成了多个磁盘文件还会进行合并,最后一次合并的结果作为reduce的输入而不是写入到磁盘中
3.Reducer的参数:最后将合并后的结果作为输入传入Reduce任务中
Hadoop中的压缩
Shuffle过程中看到,map端在写磁盘的时候采用压缩的方式将map的输出结果进行压缩是一个减少网络开销很有效的方法
解压缩算法的实现:Codec
Codec是Hadoop中关于压缩,解压缩的算法的实现,在Hadoop中,codec由CompressionCode的实现来表示
MapReduce的输出进行压缩
输出属性如下所示
在Java中设置输出压缩
reduce端输出压缩使用了Codec中的Gzip算法,也可以使用bzip2算法
4.MapReduce排序分组
MapReduce中排序和分组在哪里被执行:
Step1.4第四步中需要对不同分区中的数据进行排序和分组,默认情况按照key进行排序和分组
在Hadoop默认的排序算法中,只会针对key值进行排序
任务:
数据文件中,如果按照第一列升序排列,当第一列相同时,第二列升序排列;如果当第一列相同时,求出第二列的最小值
数据文件:
3 3
3 2
3 1
2 2
2 1
1 1
自定义排序:
1.封装一个自定义类型作为key的新类型:将第一列与第二列都作为key
定义:
public interface WritableComparable
}
自定义类型MyNewKey实现了WritableComparable的接口,
该接口中有一个compareTo()方法,当对key进行比较时会调用该方法,而我们将其改为了我们自己定义的比较规则,从而实现我们想要的效果
private static class MyNewKey implements WritableComparable {
long firstNum;
long secondNum;
public MyNewKey() {
}
public MyNewKey(long first, long second) {
firstNum = first;
secondNum = second;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(firstNum);
out.writeLong(secondNum);
}
@Override
public void readFields(DataInput in) throws IOException {
firstNum = in.readLong();
secondNum = in.readLong();
}
/*
* 当key进行排序时会调用以下这个compreTo方法
*/
@Override
public int compareTo(MyNewKey anotherKey) {
long min = firstNum - anotherKey.firstNum;
if (min != 0) {
// 说明第一列不相等,则返回两数之间小的数
return (int) min;
} else {
return (int) (secondNum - anotherKey.secondNum);
}
}
}
2.改写最初的MapReduce方法函数
public static class MyMapper extends
Mapper {
protected void map(LongWritable key,Text value,
Mapper.Context context)
throws java.io.IOException, InterruptedException {
String[] spilted = value.toString().split("\t");
long firstNum = Long.parseLong(spilted[0]);
long secondNum = Long.parseLong(spilted[1]);
// 使用新的类型作为key参与排序
MyNewKey newKey = new MyNewKey(firstNum, secondNum);
context.write(newKey, new LongWritable(secondNum));
};
}
public static class MyReducer extends
Reducer {
protected void reduce(MyNewKey key,
java.lang.Iterable values,
Reducer.Context context)
throws java.io.IOException, InterruptedException {
context.write(new LongWritable(key.firstNum), new LongWritable(
key.secondNum));
};
}
在Hadoop中的默认分组规则中,也是基于Key进行的,会将相同key的value放到一个集合中去
目的:求出第一列相同时第二列的最小值
上面的例子看分组,因为我们自定义了一个新的key,它是以两列数据作为key的,因此这6行数据中每个key都不相同产生6组,它们是:1 1,2 1,2 2,3 1,3 2,3 3。而实际上只可以分为3组,分别是1,2,3。现在首先改写一下reduce函数代码
public static class MyReducer extends
Reducer {
protected void reduce(
MyNewKey key,
java.lang.Iterable values,
Reducer.Context context)
throws java.io.IOException, InterruptedException {
long min = Long.MAX_VALUE;
for (LongWritable number : values) {
long temp = number.get();
if (temp < min) {
min = temp;
}
}
context.write(new LongWritable(key.firstNum), new LongWritable(min));
};
}
为了针对新的key类型作分组,我们也需要自定义一下分组规则:
private static class MyGroupingComparator implements
RawComparator {
/*
* 基本分组规则:按第一列firstNum进行分组
*/
@Override
public int compare(MyNewKey key1, MyNewKey key2) {
return (int) (key1.firstNum - key2.firstNum);
}
/*
* @param b1 表示第一个参与比较的字节数组
*
* @param s1 表示第一个参与比较的字节数组的起始位置
*
* @param l1 表示第一个参与比较的字节数组的偏移量
*
* @param b2 表示第二个参与比较的字节数组
*
* @param s2 表示第二个参与比较的字节数组的起始位置
*
* @param l2 表示第二个参与比较的字节数组的偏移量
*/
@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
return WritableComparator.compareBytes(b1, s1, 8, b2, s2, 8);
}
}
自定义了一个分组比较器MyGroupingComparator,该类实现了RawComparator接口,而RawComparator接口又实现了Comparator接口,这两个接口的定义:
public interface RawComparator extends Comparator {
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
}
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
}