Hadoop学习笔记之深入浅出MR

来源:互联网 发布:ubuntu 稳定版本 编辑:程序博客网 时间:2024/05/07 10:37

MapReduce
有一则形象的博文,如何向妻子解释MapReduce
http://www.csdn.net/article/2011-08-26/303688
其实MapReduce在日常生活中无处不在,像文章末尾说的数图书馆的书,每个人数一个书架(相当于Map),最后将每个人的数字加起来(Reduce)。

MapReduce是一个计算框架,用于分布式计算。解决海量数据的计算问题。

有一个普遍的例子是:
MR

看上去有点类似于分治算法,所谓分而治之,就是将大的问题分解为相同类型的子问题,对子问题进行求解,然后合并成大问题的解。

MapReduce分为Map阶段和Reduce阶段
MapReduce是一个计算框架,但是我们所需要做的就是覆盖这两个阶段的Map函数和Reduce函数,两个函数的形参分别是< key,value>键值对

MR既为计算模型,那究竟是谁来运行MR这个模型,以及执行我们自定义的业务逻辑
   在1.0中是JobTracker和TaskTracker
   2.0中是SourceManager和NodeManager

我们先不急于说清概念,先开始看一个简单的WordCount,这类似与程序界的HelloWorld。所谓的WordCount就是单词计数,给你一篇大文章,统计其中每个单词出现的次数。出于练习,我们假设文章内容是:
hello world
hello hadoop
hello MapReduce
hadoop and MapReduce
hadoop world

程序代码如下:

public class WCMapper extends Mapper< LongWritable, Text, Text, LongWritable>{@Overrideprotected void map(LongWritable key, Text value,Context context)throws IOException, InterruptedException {    //接收数据V1    String line = value.toString();    String[] words = line.split(" ");    for(String w : words)    {        //出现一次记一个1        context.write(new Text(w), new LongWritable(1));    }   }

}

  这是Map阶段,自定义的Map必须继承Mapper,泛型的参数依次是map的输入与输出,上文已经说过,输入和输出均是< k , v >键值对(下文将map的输入简写为k1,v1,输出为k2,v2),LongWritable相当于java中的int,而Text相当于String。
Map开始读取HDFS中的数据。解析成一行行,每一行都是一个键值对,而k1则是每一行的偏移量,v1则是每一行的数据。对应于输入文件,第一次的k1就是0,v1就是Hello world
  得到数据后,我们将Text转换成java中的String类型,方便操作。在输入数据中我们每个单词均是被空格切分,所以将每一行切分成一个个单词,然后将其序列化输出,至于输出到哪里,我们后面再讨论。例如第一行输出的内容我们可以想成< Hello,1> < World,1>

public class WCReducer extends Reducer< Text, LongWritable, Text, LongWritable>{@Overrideprotected void reduce(Text key, Iterable< LongWritable> v2s,Context context)throws Exception     {        long counter = 0;        for(LongWritable i : v2s)        {            counter += i.get();        }        context.write(key, new LongWritable(counter));    }}

  这是Reduce阶段,Map的输出是Reduce的输入,至于Reduce的输出类型可以自定义,reduce 的函数形参是一个迭代器,简单来说,reduce第一次接收到的内容是< hello,{1,1,1}>,统计的是输入文件中hello一共出现次数的集合,我们在reduce中需要做的工作就是求和,最终输出的类似< hello,3>,表示hello一共在文中出现了3次。
但是这里读者可能会有点疑惑,3个< hello,1>怎么就变成了1个< hello,{1,1,1}>,这期间是一个shuffle过程,shuffle又称作的MapReduce的心脏,是奇迹发生的地方。

public class WordCount {    public static void main(String[] args) throws Exception     {           Configuration conf = new Configuration();        Job job =Job.getInstance(conf);        job.setJarByClass(WordCount.class);     //指定自定义map的类        job.setMapperClass(WCMapper.class);//指定map函数的输出的键值对的类型,如果与reduce阶段的输出键值对的类型相同,则可以省略    job.setMapOutputKeyClass(Text.class);    job.setMapOutputValueClass(LongWritable.class);//指定自定义reduce的类,其余同上    job.setReducerClass(WCReducer.class);    job.setOutputKeyClass(Text.class);    job.setOutputValueClass(LongWritable.class);//指定MR程序的输入文件和输出文件,后者必须不存在,若存在则报错    FileInputFormat.setInputPaths(job, new Path(args[0]));    FileOutputFormat.setOutputPath(job, new Path(args[1]));//提交任务,true代表执行过程中打印过程和详情    job.waitForCompletion(true);    }}

 我们定义了map阶段和reduce阶段,可是这两个阶段跟这个文件有什么关系,读者肯定感觉缺少一些什么,那就对了,我们需要将map阶段和reduce组装起来,提交这个mapreduce任务,它才能按照我们预想的执行。
首先我们需要创建一个关于配置文件信息的对象,core-default.xml, core-site.xml

然后创建一个Job实例,通过这个Job实例执行MR任务,我们通常要将MR程序打包成一个jar包,所以必须指定程序入点。

通过上面这个例子,我们可以对MR程序有个大致的了解,可以简单的概括为:
   Map先从HDFS上读取数据,解析成一行行,每一行都是一个键值对(每一个键值对调用一次map函数)—–>经过一系列的处理传输到reduce阶段—–>reduce进行汇总计算,完成后输出到HDFS上。
看起来十分简单,但是我们在中间却隐藏了不少细节。我们来看看的MR具体执行流程

MR执行流程
MR1

这张经典的图看来复杂但是总结后一共分为八步
1. map任务处理
 1.1 读取输入文件内容,解析成key、value对。对输入文件的每一行,解析成key、value对。每一个键值对调用一次map函数。
 1.2 自己的业务逻辑,对输入的key、value处理,转换成新的key、value输出。
 1.3 对输出的key、value进行分区。
 1.4 对不同分区的数据,按照key进行排序、分组。相同key的value放到一个集合中。
 1.5 (可选)分组后的数据进行归约。
 2.reduce任务处理
  2.1 对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。
  2.2对多个map任务的输出进行合并、排序。写reduce函数自己的逻辑,对输入的key、value处理,转换成新的key、value输出。
 2.3 把reduce的输出保存到文件中。

发现我们分析的少了五步,接下来我们开始细化,也就是研究将map输出作为输入传给Reducer的阶段,也称为shuffle

1、何为分区(Partitioner)?
  通俗的讲,分区就是根据数据的分区规则将数据写入到reduce里面,即写入到文件中。比如现在有一大量手机号码数据文件,我们可以将其按照前三位分为移动,联通,电信三大类,也就是将其分配到不同的Reducer上运行。最后写入到三个不同的文件,以后如果便于定位查找。提高整体的运行效率。
我们通过继承Partitioner复写其getPartition方法自定义自己的分区规则。
  但是如果不设置reducer的数量(通过setNumReduceTasks进行设置),默认启动一个Reduce,最终还是讲所有数据写入到一个结果文件中。虽然分区号返回多个,但是只启动一个reducer。一个reducer对应一个结果文件。但是如果启动的reduce数量小于指定的分区号,会报错(如果启动2个reduce数量,电信的数据会不知道往那里存)
Hadoop提供了默认的Partitioner,是HashPartitioner,它对每条记录的键进行哈希操作以决定该记录应该属于哪个分区。
通过源码我们可以更清晰的看到:

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;     }}

注:numReduceTasks是reduce的个数,默认情况只有一个reducer,因此也就只有一个分区,一个结果文件。

2、分组排序
 每个分区中的数据按照k2进行排序、按照k2进行分组,分组的目的是为了后面的reduce计算、汇总。分区的目的是根据key值决定Mapper的输出记录被传送到哪一个Reducer上处理。而分组就是与记录的key相关,在同一个分区中具有相同key值得记录是属于同一个分组。通俗的讲,分区就好像北京伸展出去的高速公路,指引着数据去往哪个方向,分区以后,进行分车道行驶,也就是分组。

3、规约(combiner)
  每一个map可能会产生大量的输出,combiner的作用就是在map端对输出先做一次合并,以减少传输到reducer的数据量。
所以combiner的功能往往根reducer的功能一样。combiner最基本是实现本地key的归并,combiner具有类似本地的reduce功能。如果不用combiner,那么,所有的结果都是reduce完成,效率会相对低下。使用combiner,先完成的map会在本地聚合,提升速度。

注意:Combiner的输出是Reducer的输入,如果Combiner是可插拔的,添加Combiner绝不能改变最终的计算结果。所以Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等

Shuffle

  map函数开始产生输出的时候,并不是直接写入到磁盘,它利用缓冲的方式写到内存,并出于效率的考虑进行预排序。
每个map任务都有一个环形缓冲区,用于存储任务的输出。默认情况缓冲区大小为100MB,一旦缓冲内容达到阈值(默认为80%),一个后台线程便开始把内容溢写到磁盘中(一旦内存缓冲区达到溢出写的阈值,就会新建一个溢出写小文件)。在写磁盘过程(Spill)中,map输出继续被写到缓冲区,但如果在此期间缓冲区被填满,map会阻塞直到写磁盘过程完成。
  在写磁盘前,线程首先根据数据最终要传送到的reducer把数据划分成相应的分区。首先按照分区号进行排序,在每个分区中,后台线程按键进行内排序(如果有一个combiner,它会在排序后的输出上运行),之所以写入小文件是为了分区和排序的方便。因此在Map任务写完其最后一个输出记录之后,会有好多的溢出写文件,均是分区且排序的小文件,按照分区号排序,每个分区中的数据按照k2进行排序。在任务完成之前,溢出写文件会被合并成一个已分区已排序的输出大文件,相同分区号的数据进行合并。
  多个map会产生多个最终的分区且排序的输出大文件,每个reduce就会得到从多个map输出上取得的相同分区的小文件,此时reduce任务进入排序阶段,更恰当的说应该是合并阶段,这个阶段合并map的输出,维持其顺序排序,最后形成一个最终文件,直接作为reduce函数的输入,处理完成后将结果写入HDFS。

Reduce通过HTTP方式得到输出文件的分区,但是reduce如何知道从哪里取得map输出?
在1.X中:
  map任务完成后,它们会通知其父tasktracker状态已经更新,然后tasktracker进而通知jobtracker。这些通知通过心跳机制进行传输。因此对于指定作业,jobtracker知道map的输出和tasktracker之间的映射关系,reducer中的一个线程定期询问jobtracker以便获取map输出的位置,直到它获得所有输出位置。由于reducer可能失败,因此tasktracker并没有在第一个reducer检索到map输出时就立即从磁盘上删除。相反,tasktracker会等待,直到jobtracker告知它可以删除map输出,这是作业完成后执行的。
在2.x中:
  NodeManager中会启动YarnChild进程,NodeManage负责管理某个节点的状态,某一个NM中会有一个MrAppMaster进程,此进程用来监控属于自己任务的YarnChild(不同机器之间的进程,走RPC协议)。YarnChild中的map任务执行完成后,会将其映射关系汇报给MrAppMaster,Reducer有个线程询问MrAppMaster,得到映射关系,通过HTTP下载相应的map输出。

Shuffle阶段可以理解为两部分:
  一个是对spill进行分区时,由于一个分区包含多个key值,所以要对分区内的< k2,v2>按照k2进行排序,即k2值相同的一串< k2,v2>存放在一起,这样一个partition内按照key值整体有序了。

  第二部分并不是排序,而是进行merge,merge有两次,一次是map端将多个spill 按照分区和分区内的key进行merge,形成一个大的文件。第二次merge是在reduce端,进入同一个reduce的多个map的输出 merge在一起,该merge理解起来有点复杂,最终不是形成一个大文件,而且期间数据在内存和磁盘上都有。关于这一点《权威指南》上有提到: 假设有50个map输出,合并因子是10,合并进行5趟,每趟将10个文件合并成一个文件,因此最后有5个中间文件,在最后reduce阶段,并没有将这5个文件合并成一个已排序的文件作为最后一趟,而是直接把数据输入reduce函数,从而省略了一次磁盘往返行程。

InputSpilt与Map的关系
 InputSpilt包含一个以字节为单位的长度和一组存储位置,一个分片并不包含数据本身,而是指向数据的引用。
数据在上传到HDFS上被分成一个个Block块,属于物理切分。
运行MR的时候,对文件要进行逻辑切分,每一个切片就是一个map处理的输入块
有多少个切片就会有多少个Map。
每个分片被划分为若干个记录,每条记录就是一个键值对,map一个接一个地处理每条记录。对应到数据库中,一个输入切片可以对应一个表上的若干行,而一条记录对应到一行。
那这个切片的大小由什么决定?
追溯到源码我们就会发现

protected long computeSplitSize(long blockSize, long minSize,                   long maxSize) {    return Math.max(minSize, Math.min(maxSize, blockSize)); }

maxSize:是javalong类型表示的最大值,
默认情况下:minSize< blockSize< maxSize
所以默认情况下切片的大小和HDFS块的大小一致,如果切片大于HDFS块,若两个块位于两台机器,读取第二块的时候还需要跨机器,效率低下

参考资料:
《Hadoop权威指南》
《Hadoop实战》
参考博文:
http://blog.sina.com.cn/s/blog_d76227260101d948.html

1 0
原创粉丝点击