大数据技术之Hadoop(第二章 mapreduceshuffer过程)

本文详细介绍了Hadoop MapReduce中的Shuffle过程,包括Map端的shuffle(数据写入缓存、溢写、分区、排序和合并)和Reduce端的shuffle(领取数据、归并数据、输入给Reduce任务)。此外,还讲解了Partitioner分区、WritableComparable排序、Combiner合并和自定义OutputFormat数据输出。重点讨论了Shuffle过程中如何通过分区、排序和合并保证数据的正确性,并提供了多个案例实操。
摘要由CSDN通过智能技术生成

3.4 Shuffle机制

shuffle: 洗牌,清洗。
// 源文件:
public static void main hello
hello static private asdfasdf ba c abc

public 1
static 2
hello 2
shuffle过程是MapReduce整个工作流程的核心环节
map : mapShuffle
reduce: reduceShuffle

1.在Map端的shuffle过程:Map输出的结果首先被写入缓存,当缓存满时,启动溢写(环形缓冲区(100M。80M))操作,把缓存写入磁盘文件,然后清空缓存

2.当启动溢写操作时,首先需要把缓存中的数据进行分区(partition),然后对每个分区进行排序(sort),合并(combine)
3.之后再写入磁盘文件,每次溢写操作会生成一个新的磁盘文件,随着Map任务的执行,会有越来越多的磁盘文件,然后通知相应的Reduce来领取属于自己的数据
.在Reduce端的shuffle过程,Reduce任务领回属于自己的数据后,然后对数据进行归并(Merge)然后交给Reduce处理
2.Map端的shuffle
**Map端的shuffle包括四个步骤

  1. 输入数据和执行Map任务:Map输入的数据一般保存在HDFS文件块中,这些文件块的格式可以是任意的
  2. 写入缓存:每个Map任务都会被分配一个缓存,Map输出的结果不是立即写入磁盘,而是首先写入缓存,积累一定缓存后,再一次性批量写入磁盘,这样可以大大减少对磁盘IO的影响,因为每次磁盘寻址开销很大,在写入缓存之前,键值对都会被序列化成字节数组
  3. 溢写(分区,排序,合并):提供给MapReduce的缓存容量是有限的,默认大小100MB,随着Map任务的执行,缓存会越来越多,这时就必须启动溢写操作(spill),溢写过程通常是由另一个单独的后台线程来完成,但是为了保证Map任务写入缓存不受溢写影响,一般设置一个溢写比例0.8,在溢写到磁盘之前,缓存中的数据首先会被分区(partition)(),MapReduce通过Partitioner接口对这些键值对进行分区,采用默认的分区方式是Hash函数对key进行哈希后再用Reduce任务的数量进行取模,这样就可以把Map输出结果均匀的分配给所有的Reduce去并行处理,当然也可以自定义分区机制,对于分区内的每个键值对,后台线程会根据key对他们进行排序(sort),排序是MapReduce的默认操作,排序结束后还包含一个可选的合并(combine)操作,如果用户事先没有定义combiner函数,就不用进行合并,如果定义了,这个操作会减少需要溢写到磁盘的数据量,合并就是把相同key的value加起来,减少键值对的数量,功能与Reduce相似,但是并不是所有的场合都可以使用combiner,因为combiner的输出是Reduce的输入,combiner绝不能改变Reduce的计算结果,一般在累加,最大值的这种场景使用,每次溢写操作都会生成一个新的溢写文件,所有写入这个文件中的数据都是经过分区和排序的。
  4. **文件归并:**在Map任务结束之前,系统会对所有的溢写文件进行归并(Merge),生成一个大的溢写文件,归并就是对具有相同key的键值对合并,形成一个新的键值对,另外,在文件归并时,如果磁盘中生成溢写文件的数量超过min.num.spills.for.combine的值时(默认是3,用户可以修改),那么就可以再次运行combiner对数据进行合并操作,从而减小写入磁盘的数据量
    ****经过上述四个步骤后,Map端的shuffle过程全部完成,最终生成的大文件会被存放在本地磁盘中,这个大文件中的数据是被分区的,不同的分区分发到不同的Reduce任务进行并行处理,JobTracker会一直监测Map任务的执行,当监测到一个Map任务完成后,就会立刻通知相关的Reduce任务来领取数据,开始Reduce端的shuffle过程。
    3.Reduce端的shuffle

**Reduce端的shuffle包括三个步骤:

1. 领取数据:Map端的shuffle过程结束后,结果会保存在本地磁盘中,Reduce任务只需要把这些数据领取(Fetch)回来存放到自己机器所在的磁盘上,因此,每个Reduce任务在执行之前,大部分时间都在领数据,每一个Reduce会不断通过RPC向JobTracker询问Map任务是否已经完成
**2. 归并数据:**领回数据后,会首先被存在Reduce任务所在机器的缓存中,如果缓存满了,就会像Map一样发生溢写操作,由于在shuffle阶段,真正的Reduce还没有执行,这时可以把内存的大部分空间分配给shuffle过程作为缓存,缓存中的数据是来自不同机器的,一般会存在很多可以合并(combine)的键值对,当溢写程序启动时,具有相同key的键值对会被归并,如果用户定义了combiner,则归并后还可以执行combiner,减少写入磁盘的数据量,每个溢写过程结束后,都会在磁盘生成一个溢写文件,当溢写文件过多时也会像Map一样被归并成一个大文件,归并是也会进行排序,当数据很少时,写入缓存就行,不需要溢写到磁盘,而是直接在内存中执行归并操作,直接输出给Reduce任务,需要说明,把这些溢写文件归并成一个大文件需要多轮归并操作,每轮归并操作的文件数量有参数io.sort.factor来决定(默认是10,可以修改)
**3. 把数据输入Reduce任务:**磁盘中经过多轮归并后得到若干个大文件,不会继续归并成一个新的大文件,而是直接输出给Reduce任务,这样可以减少磁盘的读写开销,这样整个shuffle过就结束了,接下来Reduce任务会执行Reduce函数中定义的各种映射,输出最终结果,并保存到分布式文件系统中。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结:[重点哦!!!!]

map task 从split中读取数据,进行处理后,输出key/value,对键值对进行Partitioner后,存入到缓存中,缓存默认大小是100M,当缓存内容达到80M时,启动溢写操作,把缓存区数据写入一个溢写文件,在写入文件之前,会对键值对进行分区排序和合并(如果设置的话),当该map task处理完所有数据后,需要对该map生成的所有溢写文件进行merger操作,生成一个文件作为该maptask的成果,reduce task接受到通知后,就回拉取各个map task的成果数据,放到缓存区中,当缓存区内容达到阀值时,同样执行溢写操作,生成溢写文件,当把所有的map task的成果数据读取完毕后,会把生成的所有溢写文件进行merge操作,生成一个文件作为reduce task的输出数据。

3.4.1 Shuffle机制

Mapreduce确保每个reducer的输入都是按key排序的。系统执行排序的过程(即将mapper输出作为输入传给reducer)称为shuffle,如图4-14所示。
在这里插入图片描述

3.4.2 Partition分区

问题引出:要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
1.默认partition分区

public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}

默认分区是根据key的hashCode对reduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
2.自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法

	public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
   

	@Override
	public int getPartition(Text key, FlowBean value, int numPartitions) {
   

// 1 获取电话号码的前三位
		String preNum = key.toString().substring(0, 3);
		
		int partition = 4;
		
		// 2 判断是哪个省
		if ("136".equals(preNum)) {
   
			partition = 0;
		}else if ("137".equals(preNum)) {
   
			partition = 1;
		}else if ("138".equals(preNum)) {
   
			partition = 2;
		}else if ("139".equals(preNum)) {
   
			partition = 3;
		}
		return partition;
	}
}

(2)在job驱动中,设置自定义partitioner:

job.setPartitionerClass(CustomPartitioner.class);

(3)自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task

job.setNumReduceTasks(5);

3.注意

如果reduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
如果1<reduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;

例如:假设自定义分区数为5,则
(1)job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件
(2)job.setNumReduceTasks(2);会报错
(3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件

3.4.3 Partition分区案例实操

1.需求
将统计结果按照手机归属地不同省份输出到不同文件中(分区)
2.数据准备
在这里插入图片描述
3.分析
(1)Mapreduce中会将map输出的kv对,按照相同key分组,然后分发给不同的reducetask。默认的分发规则为:根据key的hashcode%reducetask数来分发
(2)如果要按照我们自己的需求进行分组,则需要改写数据分发(分组)组件Partitioner
自定义一个CustomPartitioner继承抽象类:Partitioner
(3)在job驱动中,设置自定义partitioner: job.setPartitionerClass(CustomPartitioner.class)
4.在案例2.4的基础上,增加一个分区类

package com.bk.mapreduce.flowsum;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
   

	@Override
	public int getPartition(Text key, FlowBean value, int numPartitions) {
   
		// 1 获取电话号码的前三位
		String preNum = key.toString().substring(0, 3);
		
		int partition = 4;
		
		// 2 判断是哪个省
		if ("136".equals(preNum)) {
   
			partition = 0;
		}else if ("137".equals(preNum)) {
   
			partition = 1;
		}else if ("138".equals(preNum)) {
   
			partition = 2;
		}else if ("139".equals(preNum)) {
   
			partition = 3;
		}

		return partition;
	}
}

5.在驱动函数中增加自定义数据分区设置和reduce task设置

package com.bk.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class FlowsumDriver {
   

	public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {
   
		
		// 1 获取配置信息,或者job对象实例
		Configuration configuration = new Configuration();
		Job job = Job.getInstance(configuration);

		// 6 指定本程序的jar包所在的本地路径
		job.setJarByClass(FlowsumDriver.class);

		// 2 指定本业务job要使用的mapper/Reducer业务类
		job.setMapperClass(FlowCountMapper.class);
		job.setReducerClass(FlowCountReducer.class);

		// 3 指定mapper输出数据的kv类型
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(FlowBean.class);

		// 4 指定最终输出的数据的kv类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(FlowBean.class);

		// 8 指定自定义数据分区
		job.setPartitionerClass(ProvincePartitioner.class);
		// 9 同时指定相应数量的reduce task
		job.setNumReduceTasks(5);
		
		// 5 指定job的输入原始文件所在目录
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));

		// 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
		boolean result = job.waitForCompletion(true);
		System.exit(result ? 0 : 1);
	}
}

3.4.4 WritableComparable排序

排序是MapReduce框架中最重要的操作之一。Map Task和Reduce Task均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
对于Map Task,它会将处理的结果暂时放到一个缓冲区中,当缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行一次合并,以将这些文件合并成一个大的有序文件。
对于Reduce Task,它从每个Map Task上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则放到磁盘上,否则放到内存中。如果磁盘上文件数目达到一定阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上。当所有数据拷贝完毕后,Reduce Task统一对内存和磁盘上的所有数据进行一次合并。每个阶段的默认排序
1.排序的分类
(1)部分排序:
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。
(2)全排序:
如何用Hadoop产生一个全局排序的文件?最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低,因为一台机器必须处理所有输出文件,从而完全丧失了MapReduce所提供的并行架构。
替代方案:首先创建一系列排好序的文件;其次,串联这些文件;最后,生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序。例如:可以为上述文件创建3个分区,在第一分区中,记录的单词首字母a-g,第二分区记录单词首字母h-n, 第三分区记录单词首字母o-z。
(3)辅助排序:(GroupingComparator分组)
Mapreduce框架在记录到达reducer之前按键对记录排序,但键所对应的值并没有被排序。甚至在不同的执行轮次中,这些值的排序也不固定,因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。一般来说,大多数MapReduce程序会避免让reduce函数依赖于值的排序。但是,有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。
(4)二次排序:
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
2.自定义排序WritableComparable
(1)原理分析
bean对象实现WritableComparable接口重写compareTo方法,就可以实现排序

@Override
public int compareTo(FlowBean o) {
   
	// 倒序排列,从大到小
	return this.sumFlow > o.getSumFlow()
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西边的虫虫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值