MapReduce进阶

一、Combiner

1.1原因:

这里写图片描述

在上述过程中,我们看到至少两个性能瓶颈:

(1)如果我们有10亿个数据,Mapper会生成10亿个键值对在网络间进行传输,但如果我们只是对数据求最大值,那么很明显的Mapper只需要输出它所知道的最大值即可。这样做不仅可以减轻网络压力,同样也可以大幅度提高程序效率。
网络带宽严重被占降低程序效率;
(2)假设使用美国专利数据集中的国家一项来阐述数据倾斜这个定义,这样的数据远远不是一致性的或者说平衡分布的,由于大多数专利的国家都属于美国,这样不仅Mapper中的键值对、中间阶段(shuffle)的键值对等,大多数的键值对最终会聚集于一个单一的Reducer之上,压倒这个Reducer,从而大大降低程序的性能。
单一节点承载过重降低程序性能;
在MapReduce编程模型中,在Mapper和Reducer之间有一个非常重要的组件,它解决了上述的性能瓶颈问题,它就是Combiner。

1.2特点

①与mapper和reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。
②并不是所有的job都适用combiner,只有操作满足结合律的才可设置combiner。
combine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值的话,可以使用,但是如果是求中值的话,不适用。

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

(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叠加。

融合Combiner的MapReduce:

这里写图片描述

1.3使用

使用MyReducer作为Combiner

// 设置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, new LongWritable(count));
            // 显示次数表示输出的k2,v2的键值对数量
            System.out.println("Combiner输出键值对<" + key.toString() + "," + count + ">");
        };
}

添加设置Combiner的代码

//设置Map规约Combiner
job.setCombinerClass(MyCombiner.class);

1.4小结

在实际的Hadoop集群操作中,我们是由多台主机一起进行MapReduce的,
如果加入规约操作,每一台主机会在reduce之前进行一次对本机数据的规约,
然后在通过集群进行reduce操作,这样就会大大节省reduce的时间,
从而加快MapReduce的处理速度

二、Partitioner

Map阶段总共五个步骤

step1.3就是一个分区操作
这里写图片描述
哪个key到哪个Reducer的分配过程,是由Partitioner规定的。

2.1Hadoop内置Partitioner

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

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

public class HashPartitioner<K, V> extends Partitioner<K, V> {
  public int getPartition(K key, V value,int numReduceTasks) {
	 //将key均匀布在Reduce Tasks上
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }
}

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

自定制Partitioner

public static class LiuPartitioner extends Partitioner<IntWritable, NullWritable> {
        @Override
        public int getPartition(IntWritable key, NullWritable value, int numPartitions) {
            // 实现不同的长度不同的号码分配到不同的reduce task中
            int numLength = key.toString().length();

            if (numLength == 11) 
	            return 0;
            else   
	            return 1; 
        }
 }

job.setPartitionerClass(LiuPartitioner.class);

设置Partitioner

job.setPartitionerClass(LiuPartitioner.class);
job.setNumReduceTasks(2);

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

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

三、shuffle过程

3.1整体过程

Reduce阶段三个步骤

Step2.1就是一个Shuffle[随机、洗牌]操作

这里写图片描述

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

Shuffle过程

这里写图片描述

3.2Map端

这里写图片描述

  1. 在map端首先是InputSplit,在InputSplit中含有DataNode中的数据,每一个InputSplit都会分配一个Mapper任务,Mapper任务结束后产生<K2,V2>的输出,这些输出先存放在缓存中,每个map有一个环形内存缓冲区,用于存储任务的输出。默认大小100MB(io.sort.mb属性),一旦达到阀值0.8(io.sort.spil l.percent),一个后台线程就把内容写到(spill)Linux本地磁盘中的指定目录(mapred.local.dir)下的新建的一个溢出写文件。
  2. 写磁盘前,要进行partition、sort和combine等操作。通过分区,将不同类型的数据分开处理,之后对不同分区的数据进行排序,如果有Combiner,还要对排序后的数据进行combine。等最后记录写完,将全部溢出文件合并为一个分区且排序的文件
  3. 最后将磁盘中的数据送到Reduce中,图中Map输出有三个分区,有一个分区数据被送到图示的Reduce任务中,剩下的两个分区被送到其他Reducer任务中。而图示的Reducer任务的其他的三个输入则来自其他节点的Map输出。

3.3Reduce端

这里写图片描述

  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任务中

3.4Hadoop中的压缩

Shuffle过程中看到,map端在写磁盘的时候采用压缩的方式将map的输出结果进行压缩是一个减少网络开销很有效的方法

解压缩算法的实现:Codec
Codec是Hadoop中关于压缩,解压缩的算法的实现,在Hadoop中,codec由CompressionCode的实现来表示
这里写图片描述

MapReduce的输出进行压缩,输出属性如下所示

这里写图片描述

在Java中设置输出压缩

这里写图片描述
reduce端输出压缩使用了Codec中的Gzip算法,也可以使用bzip2算法

四、MapReduce排序分组

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

Step1.4第四步中需要对不同分区中的数据进行排序和分组,默认情况按照key进行排序和分组

这里写图片描述

4.1二次排序

在Hadoop默认的排序算法中,只会针对key值进行排序。
二次排序:
在map阶段
1.使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块
调用自定义Map的map方法,将一个个<LongWritable, Text>对输入给Map的map方法。输出应该符合自定义Map中定义的输出<NewMyKey, IntWritable>
最终生成一个List<NewMyKey, IntWritable>
2.在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer
每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序, 是一个二次排序。

如果没有通过job.setSortComparatorClass设置key比较函数类,则使用key的实现的compareTo方法。
使用NewMyKey实现的compareTo方法,
在reduce阶段
1.reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序
2.然后开始构造一个key对应的value迭代器,使用jobjob.setGroupingComparatorClass设置的分组函数类
只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器
3.最后进入Reducer的reduce方法,reduce方法的输入是所有的(key和它的value迭代器)

实验
1.数据文件中,如果按照第一列升序排列,当第一列相同时,第二列升序排列
2.如果当第一列相同时,求出第二列的最小值

//数据文件:
3    3
3    2
3    1
2    2
2    1
1    1

自定义排序:
1.封装一个自定义类型作为key的新类型:将第一列与第二列都作为key
WritableComparable接口

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

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

public class MyNewKey implements WritableComparable <MyNewKey>{
	Long fistname;
	Long secondname;
	
	public MyNewKey() {
	}
	public MyNewKey(Long fist, Long second) {
		fistname = fist;
		secondname = second;
	}
	
	public Long getFistname() {
		return fistname;
	}

	public Long getSecondname() {
		return secondname;
	}

	@Override
	public void readFields(DataInput in) throws IOException {
		// TODO Auto-generated method stub
		fistname = in.readLong();
		secondname = in.readLong();
	}

	@Override
	public void write(DataOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeLong(fistname);
		out.writeLong(secondname);
	}


	@Override
	public int compareTo(MyNewKey another) {
		// TODO Auto-generated method stub
		long min = fistname - another.fistname;
		if(min !=0 ) {
			return (int)min;
		} else {
			return (int)(secondname - another.secondname);
		}
	}
	
}

改写Map、Reduce

static class MyMapper extends Mapper<Object, Object, MyNewKey, LongWritable>{
		
		LongWritable out_value = new LongWritable();
//		long x = 1;
		protected void map(Object key, Object value, Context context)
				throws IOException, InterruptedException {
			
			String[] arr = value.toString().split(" ", 2);
//			MyNewKey newkey = new MyNewKey(Long.parseLong(arr[0]), x);
			MyNewKey newkey = new MyNewKey(Long.parseLong(arr[0]), Long.parseLong(arr[1]));
			out_value.set(Long.parseLong(arr[1]));
			context.write(newkey, out_value);
		}
	}
	static class MyReduce extends Reducer<MyNewKey, LongWritable, LongWritable, LongWritable> {
		LongWritable tokenkey = new LongWritable();
		LongWritable tokenvalue = new LongWritable();

		protected void reduce(MyNewKey key, Iterable<LongWritable> values, Context context)
				throws java.io.IOException, java.lang.InterruptedException {
			
//			long x = Long.MAX_VALUE;
//			for(LongWritable e:values) {				
//				long temp = e.get();
//				if(x > temp) {
//					x = temp;
//				}
//			}
			tokenkey.set(key.getFistname());
			tokenvalue.set(key.getSecondname());
//			tokenvalue.set(x);
			context.write(tokenkey, tokenvalue);
		}
	     
	}

4.2分组

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

求出第一列相同时第二列的最小值

上面的例子看分组,因为我们自定义了一个新的key,它是以两列数据作为key的,因此这6行数据中每个key都不相同,而实际上只可以分为3组

1.改写一下Map、reduce函数代码
在Map中,new MyNewKey的第二个参数要设置为相同值,以使通过key进行分组

在Reduce中添加以下代码,第一列已经分组,从values中找到第二列最小值

long x = Long.MAX_VALUE;
			for(LongWritable e:values) {				
				long temp = e.get();
				if(x > temp) {
					x = temp;
				}
			}
tokenvalue.set(x);

2.自定义分组

为了针对新的key类型作分组,我们也需要自定义一下分组规则:
自定义了一个分组比较器MyGroupingComparator,该类实现了RawComparator接口,而RawComparator接口又实现了Comparator接口,这两个接口的定义:

public interface RawComparator<T> extends Comparator<T> {
  public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
}

public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

1.MyGroupingComparator实现两个接口:

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

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

//需要新建此类,不然初始化会出错
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);
        }
}

2.添加对分组规则的设置:
  // 设置自定义分组规则

job.setGroupingComparatorClass(MyGroupingComparator.class);

输出

1    1
2    1
3    1

for循环从values集合中找到最小值,如果不加for循环,直接设置out_value为key.getsecondname(),也是可以得到结果的,这是因为NewMyKey已经重写了排序方法,当分组后,key为分组中的第一个对象,即第二列secondname为最小值的对象。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值