MapReduce框架学习(2)——Map/Reduce及 Shuffle前后

参考: JeffreyZhou的博客园
《Hadoop权威指南》第四版

0 Map/Reduce大致流程

  1. 输入(input): 将输入数据分成一个个split,并将spilt进一步拆成<key,value>形式;
  2. 映射(map):根据输入的<key,value>进行处理,输出list<key,value>;
  3. 合并(combiner):合并(单个节点上)中间相同的key值;
  4. 分区(partition):将<key,value>分成N分,分别送到下一环节;
  5. 化简(reduce):将中间结果合并,得到最终结果;
  6. 输出(output):指定输出最终结果格式。

本博文重点在于将map/reduce中的各个小环节进行理解和应用,属于细节方面,而不是讲解MR的大致运行流程,这类知识应该作为前提了解,本文在此放置一个整体的运行例图,方便对比本博文的章节内容。

MR

图片来源:图解mapreduce原理和执行过程

2.1 Java Map/Reduce

在明白MR程序的工作原理之后,要知道一个MR作业,包括三点:

  • 输入数据
  • MR程序
  • Job配置信息

上篇博文大概讲解了输入数据的分片、格式等知识,这篇来讲讲MR过程,后面再将配置信息。

2.2 map处理

map函数由Mapper类来表示,如果不指定map函数,则系统自动指定一个Null,这意味着将输入的<key,value>对,不做任何修改,直接送到下一个环节。要自定义map函数,就继承Mapper类,复写其map函数。如wordcount程序中的这句:

public static class TokenizerMapper 
extends Mapper<Object, Text, Text, IntWritable> 
throws IOException, InterruptedException
{
public void map(Object key, Text value, Context context) 
throws IOException, InterruptedException 
	{
		// your map code
        context.write(key, value);
    }
}

Mapper是一个泛型类型,有四个形参,分别指定map函数的输入键、输入值、输出键和输出值的类型。可以看到,这几个形参基本都是java中没见过的类型,但是text 和 int都比较熟悉,没错,这是Hadoop自身提供的一套基本类型,它可以优化网络序列化传输,至于这个是什么后面再将,总之就是进行优化了,更适合Hadoop的使用。常见对应关系如下:

HadoopJava
TextString
IntWritableInteger
。。。依次类似

在程序中,使用下列语句指定map处理类:

job.setMapperClass(TokenizerMapper.class))

2.3 reduce处理

这一块如图所示,接受上一环节的输出,进行处理,并形成最终的输出结果,上一环节可能是Partitioner的输出,也可能就是map的输出,这个要视情况来决定是使用低配方案还是顶配,待会儿分析中间这几个环节,先来看看哼哈二将的哈大头。同样,要自定义reduce函数,就继承Reducer类,复写reduce函数:

public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
  public void reduce(Text key, Iterable<IntWritable> values,Context context) 
  throws IOException, InterruptedException {
  // your reduce code
    context.write(key, result);
  }
}

reduce和map很像,同样的四个形参,可以想到的是,这里的输入参数类型应该是和map(或者说上一个任意环节)的输出类型一样的,不然暗号都不一样,怎么可能成功接头。所以,这是编程中要注意到的一点。
在程序中,使用下列语句指定map处理类:

job.setReducerClass(IntSumReducer.class))

2.4 map和reduce之间的纠缠

  1. map任务将其输出写入本地磁盘,而非HDFS
      map的输出是中间结果,该中间结果由reduce任务处理后才产生最终输出结果,而且一旦作业(job)完成,map的输出结果就可以删除。

  2. 默认只有一个reduce任务
      默认情况下,所有Map任务的输出,排序后需通过网络传输发送到运行reduce任务的节点,数据在reduce端合并,然后才能由我们自定义的reduce函数处理,此时的输出存储到HDFS中。

  3. 太多map和多个reduce任务
      当然,一般都是设置多个reduce任务,不然人家map群挑一个reduce,那对reduce多不公平,但reduce,你看她名字就知道了,化简,肯定是比map少,这才能显示出其地位。
      那么,在多对多的情况下,怎么分配呢,这就是partiton的作用了,分好区,划地封侯,保证领土完整,互不侵犯。

  4. map的输出传送到reduce的输入
      前面说了,map具有数据本地优势化,但reduce没有啊,在集群中,带宽应该算是最重要的资源了,没办法,要致富,先修路,没出路,再大的能耐也没辙。那么为了减少map输出产生的数据传送,我们可以现在map本地进行一下“reduce”,没错,就是本地化的reduce。这里似乎有点乱,只要记住一点,combiner属于优化方案,每个combiner只作用于一个map上面,而reduce的作用在所有map上面。combiner函数是为了减少mapper和reducer之间的数据传输量,是否使用还需要斟酌一下。

  5. 上面的逻辑看起来比较简单,但真实运行情况要比这个复杂的多,简单的做一下对比就知道了:

WordCount举例真实情况
一个任务多个任务并发
现有的小规模输入数据,txt格式海量,不同数据来源,多种格式,实时
几个mapper,一个reducer好多reducer,超多mapper
无软、硬件故障硬件故障是一种常态

2.5 Combiner函数和Partitioner函数

  1. 前面讲了,combiner就是本地化的reducer,自定义:
public static class MyCombiner 
extends Reducer<Text,IntWritable,Text,IntWritable> {
   public void reduce(Text key, Iterable<IntWritable> values,Context context) 
   throws IOException, InterruptedException {
     context.write(key, new IntWritable(1));
   }
 }

在程序中,使用下列语句指定map处理类:

job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class)

值得注意的一点是:combiner可以不止一次的调用。。。如果至少存在3个溢出文件,则combiner就会在输出文件写到磁盘之再次运行。

  1. Partitioner决定着Map节点的输出将被分区到哪个Reduce节点。而默认的Partitioner是HashPartitioner,它根据每条数据记录的主健值进行Hash操作,获得一个非负整数的Hash码,然后用当前作业的Reduce节点数取模运算,这个取模运算只是为了平均reduce的处理能力,咱可以自定义。有N个结点的话,就会平均分配置到N个节点上,一个隔一个依次。大多情况下这个平均分配是够用了,但也会有一些特殊情况,以后补充。
    自定义:
public static class MyPartitioner extends HashPartitioner<K,V> {
   public void getPartition(K key, V value,int numReduceTasks) {
     super.getPartition(key,value,numReduceTasks);
   }
 }

2.6 到底什么是shuffle

简单说,就是将map输出作为输入,传给reducer的过程,成为shuffle。它属于不断被优化和改进的代码库的一部分,

此处划重点,优化!!!

也就是说,按照上面分析的理想状态,不就是map后输出,然后经过combiner,再将所有的输出集合到partitioner进行划分,到不同的reducer进行处理,输出到HDFS不就行啦。
  但是,再强大的理论框架,也挡不住现实的残酷。(以下3点没有查到具体的依据,都是自己的想法):
  (1)首先,直接map输出如果直接写入磁盘,一般机械硬盘读写很慢,这严重影响效率,理所当然想到利用内存(缓冲区),但问题又来了,内容的容量远远小于硬盘容量,经过内存就可能产生很多小文件(spill to disk溢出文件),但最终一个map的输出文件就只有一个,这时,就需要将多个溢出文件合并(merge on disk)。;
  (2)既然在这过程中利用了内存,内存速度快,那么在这转手的过程中,还可以顺便做一些其它的优化措施,比如,sortcombiner。排序是MR的默认行为,再进行一下combine,就可以减少spill文件。
  (3)对照下图,似乎还有一个partitions没有解释,因为上述两点,有一个前提条件:只有一个map和一个reducer。前面讲了,对于多个reducer,要将map分区送给不同的reducer,那么在哪决定分给哪个reducer呢?其实在写磁盘之前,线程首先根据数据最终要传的reducer把数据划分成相应的分区(partition)。在每个分区中,后台线程再按键进行内存中排序,再运行combiner(如果有的话)。

shuffle过程图解

上面是按照优化的目的,来解释shuffle中各个环节的意义,已经将整个过程讲了一遍,接下来就将这个过程分为map端和reduce端来说一下。

下面这段的参考来源: MapReduce原理分析记录

2.6.1 map端

shuffle_map端
  map函数开始输出时,并不是简单地将它写到磁盘,这个过程比想象中更复杂,它利用缓冲的方式写到内存,并出于效率的考虑进行预排序。
  内存缓冲区默认大小是100MB,一旦缓冲内容达到阈值(默认为0.8,即80%),一个后台线程便开始把内容溢出(spill)到磁盘,在这过程中,map输出继续写到缓冲区,互不影响。
  在任务完成之前,溢出文件被合并成一个已分区且已排序的输出文件。最终生成的文件存放在Task Tracker(即DataNode)够得着的某个本地目录内。

关于Namenode、Datanode、Jobtracker、Tasktracker区别见本系列另外一篇文章:Hadoop学习中的一些概念区分

2.6.2 reduce端

shuffle_reduce端

每个reduce task不断地通过RPC从Job Tracker(即namenode)那里获取map task是否完成的信息,如果reduce得到通知,获知某台Task Tracker上的map task执行完成,就可以开始启动shuffle的后半段过程了。
  reduce task在执行前,不断拉取当前job里,每个map的最终结果,然后对从不同地方拉取过来的数据不断的做merge,最终也形成一个文件,最为reduce task的输入。
  - copy。每个map任务的完成时间可能不同,因此在每个map任务完成时,reduce任务就开始复制其输出,从而能够并行取得map输出,默认是5个copy线程。
  - Merge。这和map阶段的merge动作一样,至少合并的是不同map端的数值。
  - “最终文件”,merge后生成的最终文件,可能存在于磁盘,也可能存在于内存中,默认是在磁盘中。

关于MR流程的一些补充

1. map任务个数

  • 等于输入文件被划分成的分块数(每个split对应一个map过程);
  • 如果文件在HDFS中,这又取决于输入文件的大小以及文件块的大小(即Block size)等于输入文件被划分成的分块数(每个split对应一个map过程)。
  • 对于大多数作业来说,一个合理的split大小趋向于HDFS的Block size,默认是128MB。

map任务个数

  • 原因:
    这就是map的数据本地化的优势,即著名的“计算向数据靠近”的思想。因为集群的带宽资源很宝贵,Hadoop在存储有输入数据的节点上运行map任务,可以获得最佳性能。
    (1)如果分片过大,跨越了两个数据块,那么对于任意一个HDFS节点,基本都不可能同时存储这两个数据块(根据HDFS的存储特点),就需要通过网络传输数据;
    (2)如果分片太小,那么关键时间反而会卡在管理分片和构建map任务的总时间,而不是计算本身,有点“得不偿失”。

2. reduce任务个数

从上面可以看到,我们并没有设置map任务的数量,因为这是根据Block size来自动划分的。
但reduce任务的数量是单独指定的,默认情况下,只有一个reducer,这当然不行,选择reducer的数量是个技术活,由于并行化程度提高,增加reducer的数量能缩短reduce过程,然而,太多的话,小文件将会更多,这又不够优化。

按照《Hadoop权威指南》中一条经验法则:
目标reducer保持在每个运行5分钟左右,且产生至少一个HDFS块(大小)的输出比较合适。

3. MR过程中的数据格式问题

在上一篇博文中,我们先从外层了解了整个MR作业的输入和输出,把MR作业当作一个黑盒子,先知道进去啥东西,出来啥结果。然后这一篇博文打开了这个黑盒子,了解了中间过程的大体框架,那么,在输入数据进入这个黑盒子以后,到输出之前,不管数据内容,先想想这中间的数据格式及形成又是如何变化的呢?
其实在前面提到过,不管是啥格式,至少这个环节的输出格式,得和下一环节的输入格式一致吧?不然口径都不一样,怎么对接?
上面只提到了setMapperClass()setReducerClass()来指定要用的map类型和reduce类型,这两个类中定义了接收的输入数据类型和最终的输出数据类型。但其实可以用下面两个函数控制reduce函数的输出类型,并且必须和reduce类产生的相匹配:

setOutputKeyClass()
setOutputValueClass()

map函数的输出类型默认情况下和reduce函数是相同的,但是,如果不同,则通过以下方法来设置map函数的输出类型:

setMapOutputKeyClass()
setMapOutputValueClass()

4. WordCount示例程序

这一大套讲下来,自己都有点迷糊了,放一个程序代码,来捋一捋思路,清醒一下:

// Map过程
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable>{
	private final static IntWritable one = new IntWritable(1);
	private Text word = new Text();
	public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
	// key 是这一行数据的起始偏移量,value是这一行的文本内容
	System.out.println("key=" +key.toString());
	System.out.println("Value=" + value.toString());
// 切分单词,可以使用split()方法
	StringTokenizer itr = new StringTokenizer(value.toString());
//遍历单词数组,输出key-value的格式
	while (itr.hasMoreTokens()) {
		word.set(itr.nextToken());
		context.write(word, one);
	}
	}
}
// Reduce过程
public static class IntSumReducer extends Reducer<Text,IntWritable,Text,IntWritable> {
	private IntWritable result = new IntWritable();
	public void reduce(Text key, Iterable<IntWritable> values,Context context) 
	throws IOException, InterruptedException {
		int sum = 0;
		for (IntWritable val : values) {
			sum += val.get();
		}
		result.set(sum);
		context.write(key, result);
	}
}

// 主程序
public static void main(String[] args) throws Exception {
	Configuration conf = new Configuration();   // 配置文件
	// System.out.println("url:" + conf.get("fs.default.name"));  // deprecated
	System.out.println("url:" + conf.get("fs.defaultFS"));
	String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
	if (otherArgs.length != 2) {
		System.err.println("Usage: wordcount <in> <out>");
		System.exit(2);
	}
	// 获取一个作业
	// Job job = new Job(conf, "word count");  // deprecated
	Job job = Job.getInstance(conf,"wordcount");  // 用job的静态方法
	// 设置job所用的那些类(class)文件在哪个jar包
	job.setJarByClass(WordCount.class);
	// 设置所用的map reduce类
	job.setMapperClass(TokenizerMapper.class);
	job.setCombinerClass(IntSumReducer.class);  // 对每个节点的map输出进行combine
	job.setReducerClass(IntSumReducer.class);  // 对所有节点的输出进行reduce

// 设置数据输出类型
	job.setOutputKeyClass(Text.class);
	job.setOutputValueClass(IntWritable.class);
// 指定要处理的输入、输出路径,
	//此处为输入参数
	FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
	FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
	//此处为固定文件目录
	// FileInputFormat.addInputPath(job, "input");
	// FileOutputFormat.setOutputPath(job, "output");
// 将job提交给集群运行
	System.exit(job.waitForCompletion(true) ? 0 : 1);
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MapReduce 是一种分布式计算框架,能够将大规模数据集分割成小块,然后在多台计算机上并行处理这些小块数据,最后将结果合并起来。其主要流程如下: 1. Map 阶段:在这个阶段,MapReduce 将大规模数据集划分成小块,然后在每个小块上运行 Map 函数,将其转换为键值对的形式,并将输出结果写入本地磁盘。Map 函数的作用是将输入数据转换为键值对,其中键表示数据的某个特征,值表示该特征对应的数据。例如,对于一个文本文件,Map 函数可以将文件中的每一行转换为一个键值对,其中键是行号,值是该行的内容。 2. Shuffle 阶段:在这个阶段,MapReduceMap 函数的输出结果根据键值对重新分配到不同的机器上,以便进行下一步的 Reduce 操作。Shuffle 阶段的主要作用是将 Map 函数的输出结果按照键进行分组,将具有相同键的值发送到同一个 Reduce 任务中。这个过程需要进行网络传输,因此需要耗费一定的时间和带宽。 3. Reduce 阶段:在这个阶段,MapReduce 将每个机器上的键值对进行合并和处理,并将结果写入输出文件。Reduce 函数的输入是一个键和一组具有该键的值,输出是一组经过归约计算后的结果。Reduce 函数的作用是对具有相同键的值进行归约操作,例如求和、求平均值等。 4. 输出阶段:在这个阶段,MapReduce 将每个 Reduce 函数的输出结果合并成最终的输出文件,并将该文件存储到分布式文件系统中。 整个 MapReduce 过程是自动化的,程序员只需要编写 MapReduce 函数,并指定输入和输出路径即可。同时,MapReduce 还具有容错性和自动恢复功能,能够在单个计算机或网络中断时自动重启任务,从而保证整个过程的可靠性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值