MapReduce高级->Combiner-Partitioner-Shuffle-分组

本文深入解析MapReduce中的Combiner、Partitioner、Shuffle及排序分组机制,介绍如何通过这些组件优化MapReduce作业,提高数据处理效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Combiner

1、为什么需要Combiner?

Combiner是为了解决网络带宽严重被占降低程序效率和单一节点承载过重降低程序性能这两个问题而产生的;

①与mapperreducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。

②并不是所有的job都适用combiner,只有操作满足结合律的才可设置combinercombine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值的话,可以使用,但是如果是求中值的话,不适用。

2、它是MapReduce的一种优化手段

每一个map都可能会产生大量的本地输出,Combiner的作用就是对map端的输出先做一次合并,以减少在map和reduce节点之间的数据传输量,以提高网络IO性能。

3、Combiner的作用

1Combiner实现本地key的聚合,对map输出的key排序value进行迭代

       如下所示:

    map:(K1, V1) list(K2, V2) 
combine:(K2, list(V2)) list(K2, V2) 
reduce:(K2, list(V2)) list(K3, V3)

2Combiner还有本地reduce功能(其本质上就是一个reduce

         例如wordcount的例子和找出value的最大值的程序

          combinerreduce完全一致,如下所示:

          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一结束就可以进行reducevalue叠加,而不必要等到所有的map结束再去进行reducevalue叠加。

4、融合Combiner的MapReduce


//设置Map规约Combiner

   job.setCombinerClass(MyReducer.class);

执行后看到map的输出和combine的输入统计是一致的,而combine的输出与reduce的输入统计是一样的。

由此可以看出规约操作成功,而且执行在map的最后,reduce之前。

自己定义Combiner

public static class MyCombiner extends  Reducer<Text,LongWritable, Text,LongWritable> {

        protected void reduce(

                Text key, java.lang.Iterable<LongWritable> values,

                org.apache.hadoop.mapreduce.Reducer<Text,LongWritable, Text,LongWritable>.Context context)

                throws java.io.IOException,InterruptedException {

            // 显示次数表示规约函数被调用了多少次,表示k2有多少个分组

            System.out.println("Combiner输入分组<" +key.toString() + ",N(N>=1)>");

            long count = 0L;

            for (LongWritable value : values) {

                count += value.get();

                // 显示次数表示输入的k2,v2的键值对数量

                System.out.println("Combiner输入键值对<" +key.toString() + ",”+value.get() + ">");

            }

            context.write(key, newLongWritable(count));

            // 显示次数表示输出的k2,v2的键值对数量

            System.out.println("Combiner输出键值对<" +key.toString() + "," + count +">");

        };

   }

添加设置Combiner的代码

   // 设置Map规约Combiner

   job.setCombinerClass(MyCombiner.class);

总结:

在实际的Hadoop集群操作中,我们是由多台主机一起进行MapReduce的,

如果加入规约操作,每一台主机会在reduce之前进行一次对本机数据的规约,

然后在通过集群进行reduce操作,这样就会大大节省reduce的时间,

从而加快MapReduce的处理速度

二、Partitioner与自定义Partitioner

map阶段总共五个步骤


key分配到Reducer的过程,是由Partitioner规定的。

1、Hadoop内置Partitioner

MapReduce的使用者通常会指定Reduce任务和Reduce任务输出文件的数量(R)。

用户在中间key上使用分区函数来对数据进行分区,之后在输入到后续任务执行进程。一个默认的分区函数式使用hash方法(比如常见的:hash(key) mod R)进行分区。hash方法能够产生非常平衡的分区

Hadoop中自带了一个默认的分区类HashPartitioner它继承了Partitioner类,提供了一个getPartition的方法

key均匀布在ReduceTasks上:(key.hashCode() & Integer.MAX_VALUE) %numReduceTasks

如果KeyText的话,Texthashcode方法跟String的基本一致,都是采用的Horner公式计算,得到一个int整数。但是,如果string太大的话这个int整数值可能会溢出变成负数,所以和整数的上限值Integer.MAX_VALUE(即0111111111111111)进行与运算,然后再对reduce任务个数取余,这样就可以让key均匀分布在reduce 

2、自定制Partitioner

一般我们都会使用默认的分区函数HashPartitioner

自定义LiuPartitioner

 public static classLiuPartitioner extendsPartitioner<Text,KpiWritable> {

        @Override

        public intgetPartition(Text key,KpiWritable value,intnumPartitions) {

            // 实现不同的长度不同的号码分配到不同的reduce task

            intnumLength =key.toString().length();

            if (numLength == 11) return 0;

            else   return 1;

        }

   }

3、分区Partitioner主要作用在于以下两点

1.根据业务需要,产生多个输出文件
2.多个reduce任务并发运行,提高整体job的运行效率
三、Shuffle

1、Reduce阶段三个步骤:


2、Shuffle是什么?

针对多个map任务的输出按照不同的分区(Partition)通过网络复制到不同的reduce任务节点上,这个过程就称作为Shuffle

3、Shuffle过程


map端


(1)map端首先是InputSplit,在InputSplit中含有DataNode中的数据,每一个InputSplit都会分配一个Mapper任务,Mapper任务结束后产生<K2,V2>的输出,这些输出先存放在缓存中,每个map有一个环形内存缓冲区,用于存储任务的输出。默认大小100MBio.sort.mb属性),一旦达到阀值0.8(io.sort.spill.percent),一个后台线程就把内容写到(spill)Linux本地磁盘中的指定目录(mapred.local.dir)下的新建的一个溢出写文件

(2)写磁盘前,要进行partitionsortcombine等操作。通过分区,将不同类型的数据分开处理,之后对不同分区的数据进行排序,如果有Combiner,还要对排序后的数据进行combine。等最后记录写完,将全部溢出文件合并为一个分区且排序的文件

(3)最后将磁盘中的数据送到Reduce中,图中Map输出有三个分区,有一个分区数据被送到图示的Reduce任务中,剩下的两个分区被送到其他Reducer任务中。而图示的Reducer任务的其他的三个输入则来自其他节点的Map输出。

Reduce端


1)Copy阶段:Reducer通过Http方式得到输出文件的分区。

     reduce端可能从nmap的结果中获取数据,而这些map的执行速度不尽相同,当其中一个map运行结束时,reduce就会从JobTracker中获取该信息。map运行结束后TaskTracker会得到消息,进而将消息汇报给JobTrackerreduce定时从JobTracker获取该信息,reduce端默认有5个数据复制线程从map端复制数据

(2)Merge阶段:如果形成多个磁盘文件会进行合并

  从map端复制来的数据首先写到reduce端的缓存中,同样缓存占用到达一定阈值后会将数据写到磁盘中,同样会进行partitioncombine、排序等过程。如果形成了多个磁盘文件还会进行合并,最后一次合并的结果作为reduce的输入而不是写入到磁盘中

(3)Reducer的参数:最后将合并后的结果作为输入传入Reduce任务中

四、MapReduce排序分组

MapReduce中排序和分组在哪里被执行


Hadoop默认的排序算法中,只会针对key值进行排序

WritableComparable接口

定义:

public interface WritableComparable<T> extends Writable,Comparable<T> {

}

自定义类型MyNewKey实现了WritableComparable的接口,

该接口中有一个compareTo()方法,当对key进行比较时会调用该方法,而我们将其改为了我们自己定义的比较规则,从而实现我们想要的效果

private static class MyNewKey implements WritableComparable<MyNewKey> {

        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 intcompareTo(MyNewKeyanotherKey) {

            long min = firstNum - anotherKey.firstNum;

            if (min != 0) {

                // 说明第一列不相等,则返回两数之间小的数

                return (int) min;

            } else {

                return (int) (secondNum - anotherKey.secondNum);

            }

        }

   }

分组

Hadoop中的默认分组规则中,也是基于Key进行的,会将相同keyvalue放到一个集合中去

自定义分组:

为了针对新的key类型作分组,我们也需要自定义一下分组规则:

private static class MyGroupingComparator implements

            RawComparator<MyNewKey> {

 

        /*

         * 基本分组规则:按第一列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<T> extendsComparator<T> {

 public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);

}

public interfaceComparator<T> {

   int compare(T o1, T o2);

   boolean equals(Object obj);

}



实现分组步骤:

1.MyGroupingComparator实现这两个接口

RawComparator中的compare()方法是基于字节的比较,

Comparator中的compare()方法是基于对象的比较

由于在MyNewKey中有两个long类型,每个long类型又占8个字节。这里因为比较的是第一列数字,所以读取的偏移量为8字节。

2.添加对分组规则的设置:

// 设置自定义分组规则

  job.setGroupingComparatorClass(MyGroupingComparator.class);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值