众所周知,Hadoop框架使用Mapper将数据处理成一个个的key/value键值对,在网络节点间对其进行整理(shuffle),然后使用Reducer处理数据并进行最终输出。这其中假如我们有10亿


众所周知,Hadoop框架使用Mapper将数据处理成一个个的key/value键值对,在网络节点间对其进行整理(shuffle),然后使用Reducer处理数据并进行最终输出。这其中假如我们有10亿个数据,Mapper会生成10亿个键值对在网络间进行传输( 网络带宽严重被占降低程序效率),所有数据都经过reduce处理,造成Reducer的巨大压力,从而大大降低程序的性能。
      为了解决上述问题Combiner横空出世。
  • Combiner

    每一个map都可能会产生大量的本地输出,Combiner的作用就是对map端的输出先做一次合并,以减少在map和reduce节点之间的数据传输量,以提高网络IO性能,是MapReduce的一种优化手段之一,其具体的作用如下所述。

      (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),例如Hadoop自带的wordcount的例子和找出value的最大值的程序,combiner和reduce完全一致,如下所示:

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

  • 注意:
    ①、与mapper和reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。

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

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

  • 以WordCount为例

// WordCount 的mapper、reducer与之前一样

// WCCombiner (须extends Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>)
// combiner最基本是实现本地key的归并,combiner具有类似本地的reduce功能, 
public class WordCReducer
        extends Reducer<Text, LongWritable, Text, LongWritable> {
    protected void reduce(Text key, java.lang.Iterable<LongWritable> values,
            Reducer<Text, LongWritable, Text, LongWritable>.Context context)
            throws java.io.IOException, InterruptedException {
        long count = 0L;
        for (LongWritable value : values) {
            count += value.get();
        }
        context.write(key, new LongWritable(count));
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
// 设置Map规约Combiner
job.setCombinerClass(MyCombiner.class);
 
 
  • 1
  • 2

调试运行的控制台输出信息

  1. Mapper
    Mapper输出<hello, 1>
    Mapper输出<tom, 1>
    Mapper输出<hello, 1>
    Mapper输出<kevin, 1>
    Mapper输出<tom, 1>

  2. Combiner
    Combiner输入键值对<hello, 1>
    Combiner输入键值对<hello, 1>
    Combiner输出键值对<hello, 2>
    Combiner输入键值对<kevin, 1>
    Combiner输出键值对<kevin, 1>
    Combiner输入键值对<tom, 1>
    Combiner输入键值对<tom, 1>
    Combiner输出键值对<hello, 2>

  3. Reducer
    Reducer输入键值对<hello, 2>
    Reducer输入键值对<kevin, 1>
    Reducer输入键值对<tom, 2>

一、初步探索Partitioner

1.1 再次回顾Map阶段五大步骤

  在第四篇博文《初识MapReduce》中,我们认识了MapReduce的八大步凑,其中在Map阶段总共五个步骤,如下图所示:

map steps

  其中,step1.3就是一个分区操作。通过前面的学习我们知道Mapper最终处理的键值对<key, value>,是需要送到Reducer去合并的,合并的时候,有相同key的键/值对会送到同一个Reducer节点中进行归并。哪个key到哪个Reducer的分配过程,是由Partitioner规定。在一些集群应用中,例如分布式缓存集群中,缓存的数据大多都是靠哈希函数来进行数据的均匀分布的,在Hadoop中也不例外。

1.2 Hadoop内置Partitioner

  MapReduce的使用者通常会指定Reduce任务和Reduce任务输出文件的数量(R)。用户在中间key上使用分区函数来对数据进行分区,之后在输入到后续任务执行进程。一个默认的分区函数式使用hash方法(比如常见的:hash(key) mod R)进行分区。hash方法能够产生非常平衡的分区,鉴于此,Hadoop中自带了一个默认的分区类HashPartitioner,它继承了Partitioner类,提供了一个getPartition的方法,它的定义如下所示:

复制代码
/** Partition keys by their {@link Object#hashCode()}. */
public class HashPartitioner<K, V> extends Partitioner<K, V> {
  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

}
复制代码

  现在我们来看看HashPartitoner所做的事情,其关键代码就一句:(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; 

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

partitoner

二、自己定制Partitioner

  大部分情况下,我们都会使用默认的分区函数HashPartitioner。但有时我们又有一些特殊的应用需求,所以我们需要定制Partitioner来完成我们的业务。这里以第五篇—自定义数据类型处理手机上网日志为例,来对其中的日志内容做一个特殊的分区:

  从上图中我们可以发现,在第二列上并不是所有的数据都是手机号(例如:84138413并不是一个手机号),我们任务就是在统计手机流量时,将手机号码和非手机号输出到不同的文件中。

2.1 自定义KpiPartitioner

复制代码
    /*
     * 自定义Partitioner类
     */
    public static class KpiPartitioner extends Partitioner<Text, KpiWritable> {
        @Override
        public int getPartition(Text key, KpiWritable value, int numPartitions) {
            // 实现不同的长度不同的号码分配到不同的reduce task中
            int numLength = key.toString().length();
            if (numLength == 11) {
                return 0;
            } else {
                return 1;
            }
        }
    }
复制代码

  这里按手机和非手机号码的区分是按该字段的长度来划分,如果是11位则为手机号。接下来,就是重新修改run方法中的代码:设置为打包运行,设置Partitioner为KpiPartitioner,设置ReducerTask的个数为2

复制代码
    public int run(String[] args) throws Exception {
        // 首先删除输出目录已生成的文件
        FileSystem fs = FileSystem.get(new URI(INPUT_PATH), getConf());
        Path outPath = new Path(OUTPUT_PATH);
        if (fs.exists(outPath)) {
            fs.delete(outPath, true);
        }
        // 定义一个作业
        Job job = new Job(getConf(), "MyKpiJob");
        // 分区需要设置为打包运行
        job.setJarByClass(MyKpiJob.class);
        // 设置输入目录
        FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
        // 设置自定义Mapper类
        job.setMapperClass(MyMapper.class);
        // 指定<k2,v2>的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(KpiWritable.class);
        // 设置Partitioner
        job.setPartitionerClass(KpiPartitioner.class);
        job.setNumReduceTasks(2);
        // 设置Combiner
        job.setCombinerClass(MyReducer.class);
        // 设置自定义Reducer类
        job.setReducerClass(MyReducer.class);
        // 指定<k3,v3>的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputKeyClass(KpiWritable.class);
        // 设置输出目录
        FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
        // 提交作业
        System.exit(job.waitForCompletion(true) ? 0 : 1);
        return 0;
    }
复制代码

注意:分区的例子必须要设置为打成jar包运行!

2.2 打成jar包并在Hadoop中运行

  (1)通过Eclipse导出jar包

  (2)通过FTP上传到Linux中,可以使用各种FTP工具,我一般使用XFtp。

  (3)通过Hadoop Shell执行jar包中的程序

  

  (4)查看执行结果文件:

  首先是part-r-00000,它展示了手机号码的统计结果

  然后是part-r-00001,它展示了非手机号码的统计结果

  (5)通过Web接口验证Partitioner的运行:通过访问http://hadoop-master:50030

  ①是否有2个Reduce任务?

  从图中可以看出,总共有2个Reduce任务;

  ②Reduce输出结果是否一致?

  手机号码有20条记录,一致!

  非手机号码只有1条记录,一致!

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

(1)根据业务需要,产生多个输出文件;

(2)多个reduce任务并发运行,提高整体job的运行效率

参考资料

(1)吴超,《深入浅出Hadoop》:http://115.28.208.222/

(2)万川梅、谢正兰,《Hadoop应用开发实战详解(修订版)》:http://item.jd.com/11508248.html

(3)Suddenly,《Hadoop日记Day17-分区》:http://www.cnblogs.com/sunddenly/p/4009568.html

(4)三劫散仙,《如何使用Hadoop中的Partitioner》:http://qindongliang.iteye.com/blog/2043136

Shuffle过程是MapReduce的核心,也被称为奇迹发生的地方。要想理解MapReduce, Shuffle是必须要了解的。我看过很多相关的资料,但每次看完都云里雾里的绕着,很难理清大致的逻辑,反而越搅越混。前段时间在做MapReduce job 性能调优的工作,需要深入代码研究MapReduce的运行机制,这才对Shuffle探了个究竟。考虑到之前我在看相关资料而看不懂时很恼火,所以在这里我尽最大的可能试着把Shuffle说清楚,让每一位想了解它原理的朋友都能有所收获。如果你对这篇文章有任何疑问或建议请留言到后面,谢谢! 

        Shuffle的正常意思是洗牌或弄乱,可能大家更熟悉的是Java API里的Collections.shuffle(List)方法,它会随机地打乱参数list里的元素顺序。如果你不知道MapReduce里Shuffle是什么,那么请看这张图:
 



        这张是官方对Shuffle过程的描述。但我可以肯定的是,单从这张图你基本不可能明白Shuffle的过程,因为它与事实相差挺多,细节也是错乱的。后面我会具体描述Shuffle的事实情况,所以这里你只要清楚Shuffle的大致范围就成-怎样把map task的输出结果有效地传送到reduce端。也可以这样理解, Shuffle描述着数据从map task输出到reduce task输入的这段过程。 

        在Hadoop这样的集群环境中,大部分map task与reduce task的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果。如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重。这种网络消耗是正常的,我们不能限制,能做的就是最大化地减少不必要的消耗。还有在节点内,相比于内存,磁盘IO对job完成时间的影响也是可观的。从最基本的要求来说,我们对Shuffle过程的期望可以有: 

  • 完整地从map task端拉取数据到reduce 端。
  • 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗。
  • 减少磁盘IO对task执行的影响。


        OK,看到这里时,大家可以先停下来想想,如果是自己来设计这段Shuffle过程,那么你的设计目标是什么。我想能优化的地方主要在于减少拉取数据的量及尽量使用内存而不是磁盘。 

        我的分析是基于Hadoop0.21.0的源码,如果与你所认识的Shuffle过程有差别,不吝指出。我会以WordCount为例,并假设它有8个map task和3个reduce task。从上图看出,Shuffle过程横跨map与reduce两端,所以下面我也会分两部分来展开。 

        先看看map端的情况,如下图: 


 
       

        上图可能是某个map task的运行情况。拿它与官方图的左半边比较,会发现很多不一致。官方图没有清楚地说明partition, sort与combiner到底作用在哪个阶段。我画了这张图,希望让大家清晰地了解从map数据输入到map端所有数据准备好的全过程。 

        整个流程我分了四步。简单些可以这样说,每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。 

        当然这里的每一步都可能包含着多个步骤与细节,下面我对细节来一一说明: 
1.        在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split。Split与block的对应关系可能是多对一,默认是一对一。在WordCount例子里,假设map的输入数据都是像“aaa”这样的字符串。 

2.        在经过mapper的运行后,我们得知mapper的输出是这样一个key/value对: key是“aaa”, value是数值1。因为当前map端只做加1的操作,在reduce task里才去合并结果集。前面我们知道这个job有3个reduce task,到底当前的“aaa”应该交由哪个reduce去做呢,是需要现在决定的。 

        MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。 

        在我们的例子中,“aaa”经过Partitioner后返回0,也就是这对值应当交由第一个reducer来处理。接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。 

        整个内存缓冲区就是一个字节数组,它的字节索引及key/value存储结构我没有研究过。如果有朋友对它有研究,那么请大致描述下它的细节吧。 

3.        这个内存缓冲区是有大小限制的,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写,字面意思很直观。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写,互不影响。 

        当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。 

        在这里我们可以想想,因为map task的输出是需要发送到不同的reduce端去,而内存缓冲区没有对将发送到相同reduce端的数据做合并,那么这种合并应该是体现是磁盘文件中的。从官方图上也可以看到写到磁盘中的溢写文件是对不同的reduce端的数值做过合并。所以溢写过程一个很重要的细节在于,如果有很多个key/value对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录。 

        在针对每个reduce端而合并数据时,有些数据可能像这样:“aaa”/1, “aaa”/1。对于WordCount例子,就是简单地统计单词出现的次数,如果在同一个map task的结果中有很多个像“aaa”一样出现多次的key,我们就应该把它们的值合并到一块,这个过程叫reduce也叫combine。但MapReduce的术语中,reduce只指reduce端执行从多个map task取数据做计算的过程。除reduce外,非正式地合并数据只能算做combine了。其实大家知道的,MapReduce中将Combiner等同于Reducer。 

        如果client设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。所以从我的想法来看,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。 

4.        每次溢写会在磁盘上生成一个溢写文件,如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在。当map task真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge。Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map 读取时值是8,因为它们有相同的key,所以得merge成group。什么是group。对于“aaa”就是像这样的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。 

        至此,map端的所有工作都已结束,最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。 

        简单地说,reduce task在执行之前的工作就是不断地拉取当前job里每个map task的最终结果,然后对从不同地方拉取过来的数据不断地做merge,也最终形成一个文件作为reduce task的输入文件。见下图:
 



        如map 端的细节图,Shuffle在reduce端的过程也能用图上标明的三点来概括。当前reduce copy数据的前提是它要从JobTracker获得有哪些map task已执行结束,这段过程不表,有兴趣的朋友可以关注下。Reducer真正运行之前,所有的时间都是在拉取数据,做merge,且不断重复地在做。如前面的方式一样,下面我也分段地描述reduce 端的Shuffle细节: 
1.        Copy过程,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中。 

2.        Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活,它基于JVM的heap size设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle用。这里需要强调的是,merge有三种形式:1)内存到内存  2)内存到磁盘  3)磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑,是吧。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。 

3.        Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,当然希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。至于怎样才能让这个文件出现在内存中,之后的性能优化篇我再说。当Reducer的输入文件已定,整个Shuffle才最终结束。然后就是Reducer执行,把结果放到HDFS上。 

        上面就是整个Shuffle的过程。细节很多,我很多都略过了,只试着把要点说明白。当然,我可能也有理解或表述上的很多问题,不吝指点。我希望不断地完善和修改这篇文章,能让它通俗、易懂,看完就能知道Shuffle的方方面面。至于具体的实现原理,各位有兴趣就自己去探索,如果不方便的话,留言给我,我再来研究并反馈。

原文地址:http://langyu.iteye.com/blog/992916

自己的理解:


  1.    map task 从split中读取数据,进行处理后,输出key/value,对键值对进行Partitioner后,存入到缓存中,缓存默认大小是100M,当缓存内容达到80M时,启动溢写操作,把缓存区数据写入一个溢写文件,在写入文件之前,会对键值对进行排序和合并(如果设置的话),当该map task处理完所有数据后,需要对该map生成的所有溢写文件进行merger操作,生成一个文件作为该maptask的成果,reduce task接受到通知后,就回拉取各个map task的成果数据,放到缓存区中,当缓存区内容达到阀值时,同样执行溢写操作,生成溢写文件,当把所有的map task的成果数据读取完毕后,会把生成的所有溢写文件进行merge操作,生成然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。
  2. Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中当然希望它存放于内存中,直接作为

MapReduce排序分组

原文地址:Hadoop Mapreduce分区、分组、二次排序过程详解[转]作者:

徐海蛟  教学用途

1、MapReduce中数据流动
      (1)最简单的过程:    map  reduce
      (2)定制了partitioner以将map的结果送往指定reducer的过程: map  partition  reduce
      (3)增加了在本地先进性一次reduce(优化)过程: map  combin(本地reduce)  partition  -reduce
2、Mapreduce中Partition的概念以及使用。
(1)Partition的原理和作用
                得到map给的记录后,他们该分配给哪些reducer来处理呢?hadoop采用的默认的派发方式是根据散列值来派发的,但是实际中,这并不能很高效或者按照我们要求的去执行任务。例如,经过partition处理后,一个节点的reducer分配到了20条记录,另一个却分配道了10W万条,试想,这种情况效率如何。又或者,我们想要处理后得到的文件按照一定的规律进行输出,假设有两个reducer,我们想要最终结果中part-00000中存储的是"h"开头的记录的结果,part-00001中存储其他开头的结果,这些默认的partitioner是做不到的。所以需要我们自己定制partition来根据自己的要求,选择记录的reducer。自定义partitioner很简单,只要自定义一个类,并且继承Partitioner类,重写其getPartition方法就好了,在使用的时候通过调用Job的setPartitionerClass指定一下即可

                Map的结果,会通过partition分发到Reducer上。Mapper的结果,可能送到Combiner做合并,Combiner在系统中并没有自己的基类,而是用Reducer作为Combiner的基类,他们对外的功能是一样的,只是使用的位置和使用时的上下文不太一样而已。Mapper最终处理的键值对<key,  value>,是需要送到Reducer去合并的,合并的时候,有相同key的键/值对会送到同一个Reducer那。哪个key到哪个Reducer的分配过程,是由Partitioner规定的。它只有一个方法,

                getPartition(Text  key,  Text  value,  int  numPartitions)

输入是Map的结果对<key,  value>和Reducer的数目,输出则是分配的Reducer(整数编号)。就是指定Mappr输出的键值对到哪一个reducer上去。系统缺省的Partitioner是HashPartitioner,它以key的Hash值对Reducer的数目取模,得到对应的Reducer。这样保证如果有相同的key值,肯定被分配到同一个reducre上。如果有N个reducer,编号就为0,1,2,3……(N-1)。

(2)Partition的使用
                分区出现的必要性,如何使用Hadoop产生一个全局排序的文件?最简单的方法就是使用一个分区,但是该方法在处理大型文件时效率极低,因为一台机器必须处理所有输出文件,从而完全丧失了MapReduce所提供的并行架构的优势。事实上我们可以这样做,首先创建一系列排好序的文件;其次,串联这些文件(类似于归并排序);最后得到一个全局有序的文件。主要的思路是使用一个partitioner来描述全局排序的输出。比方说我们有1000个1-10000的数据,跑10个ruduce任务,  如果我们运行进行partition的时候,能够将在1-1000中数据的分配到第一个reduce中,1001-2000的数据分配到第二个reduce中,以此类推。即第n个reduce所分配到的数据全部大于第n-1个reduce中的数据。这样,每个reduce出来之后都是有序的了,我们只要cat所有的输出文件,变成一个大的文件,就都是有序的了

基本思路就是这样,但是现在有一个问题,就是数据的区间如何划分,在数据量大,还有我们并不清楚数据分布的情况下。一个比较简单的方法就是采样,假如有一亿的数据,我们可以对数据进行采样,如取10000个数据采样,然后对采样数据分区间。在Hadoop中,patition我们可以用TotalOrderPartitioner替换默认的分区。然后将采样的结果传给他,就可以实现我们想要的分区。在采样时,我们可以使用hadoop的几种采样工具,RandomSampler,InputSampler,IntervalSampler。

              这样,我们就可以对利用分布式文件系统进行大数据量的排序了,我们也可以重写Partitioner类中的compare函数,来定义比较的规则,从而可以实现字符串或其他非数字类型的排序,也可以实现二次排序乃至多次排序。

2、MapReduce中分组的概念和使用
        分区的目的是根据Key值决定Mapper的输出记录被送到哪一个Reducer上去处理。而分组的就比较好理解了。笔者认为,分组就是与记录的Key相关。在同一个分区里面,具有相同Key值的记录是属于同一个分组的。

3、MapReduce中Combiner的使用
                很多MapReduce程序受限于集群上可用的带宽,所以它会尽力最小化需要在map和reduce任务之间传输的中间数据。Hadoop允许用户声明一个combiner  function来处理map的输出,同时把自己对map的处理结果作为reduce的输入。因为combiner  function本身只是一种优化,hadoop并不保证对于某个map输出,这个方法会被调用多少次。换句话说,不管combiner  function被调用多少次,对应的reduce输出结果都应该是一样的。

  下面我们以《权威指南》的例子来加以说明,假设1950年的天气数据读取是由两个map完成的,其中第一个map的输出如下:
  (1950,  0)
  (1950,  20)
  (1950,  10)

第二个map的输出为:
              (1950,  25)
              (1950,  15)

而reduce得到的输入为:(1950,  [0,  20,  10,  25,  15]),  输出为:(1950,  25)

  由于25是集合中的最大值,我们可以使用一个类似于reduce  function的combiner  function来找出每个map输出中的最大值,这样的话,reduce的输入就变成了:
  (1950,  [20,  25])

  各个funciton  对温度值的处理过程可以表示如下:max(0,  20,  10,  25,  15)  =max(max(0,  20,  10),  max(25,  15))  max(20,  25)  25

  注意:并不是所有的函数都拥有这个属性的(有这个属性的函数我们称之为commutative和associative),例如,如果我们要计算平均温度,就不能这样使用combiner  function,因为mean(0,  20,  10,  25,  15)  =14,而mean(mean(0,  20,  10),mean(25,  15))  mean(10,  20)  15

  combiner  function并不能取代reduce  function(因为仍然需要reduce  function处理来自不同map的带有相同key的记录)。但是他可以帮助减少需要在map和reduce之间传输的数据,就为这一点combiner  function就值得考虑使用。

4、Shuffle阶段排序流程详解
                我们首先看一下MapReduce中的排序的总体流程。

                MapReduce框架会确保每一个Reducer的输入都是按Key进行排序的。一般,将排序以及Map的输出传输到Reduce的过程称为混洗(shuffle)。每一个Map都包含一个环形的缓存,默认100M,Map首先将输出写到缓存当中。当缓存的内容达到“阈值”时(阈值默认的大小是缓存的80%),一个后台线程负责将结果写到硬盘,这个过程称为“spill”。Spill过程中,Map仍可以向缓存写入结果,如果缓存已经写满,那么Map进行等待。

Spill的具体过程如下:首先,后台线程根据Reducer的个数将输出结果进行分组,每一个分组对应一个Reducer。其次,对于每一个分组后台线程对输出结果的Key进行排序。在排序过程中,如果有Combiner函数,则对排序结果进行Combiner函数进行调用。每一次spill都会在硬盘产生一个spill文件。因此,一个Map  task有可能会产生多个spill文件,当Map写出最后一个输出时,会将所有的spill文件进行合并与排序,输出最终的结果文件。在这个过程中Combiner函数仍然会被调用。从整个过程来看,Combiner函数的调用次数是不确定的。下面我们重点分析下Shuffle阶段的排序过程:

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

                第二部分并不是排序,而是进行merge,merge有两次,一次是map端将多个spill  按照分区和分区内的key进行merge,形成一个大的文件。第二次merge是在reduce端,进入同一个reduce的多个map的输出  merge在一起,该merge理解起来有点复杂,最终不是形成一个大文件,而且期间数据在内存和磁盘上都有。所以shuffle阶段的merge并不是严格的排序意义,只是将多个整体有序的文件merge成一个大的文件,由于不同的task执行map的输出会有所不同,所以merge后的结果不是每次都相同,不过还是严格要求按照分区划分,同时每个分区内的具有相同key的<key,value>对挨在一起。

                Shuffle排序综述:如果只定义了map函数,没有定义reduce函数,那么输入数据经过shuffle的排序后,结果为key值相同的输出挨在一起,且key值小的一定在前面,这样整体来看key值有序(宏观意义的,不一定是按从大到小,因为如果采用默认的HashPartitioner,则key  的hash值相等的在一个分区,如果key为IntWritable的话,每个分区内的key会排序好的),而每个key对应的value不是有序的。

5、MapReduce中辅助排序的原理与实现
(1)任务
我们需要把内容如下的sample.txt文件处理为下面文件:

源文件:Sample.txt

bbb  654

ccc  534

ddd  423

aaa  754

bbb  842

ccc  120

ddd  219

aaa  344

bbb  214

ccc  547

ddd  654

aaa  122

bbb  102

ccc  479

ddd  742

aaa  146

目标:part-r-00000

aaa  122

bbb  102

ccc  120

ddd  219

(2)工作原理
      过程导引:
      1、定义包含记录值和自然值的组合键,本例中为MyPariWritable.

      2、自定义键的比较器(comparator)来根据组合键对记录进行排序,即同时利用自然键和自然值进行排序。(aaa  122组合为一个键)。

      3、针对组合键的Partitioner(本示例使用默认的hashPartitioner)和分组comparator在进行分区和分组时均只考虑自然键。

      详细过程:
首先在map阶段,使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块splites,同时InputFormat提供一个RecordReder的实现。本例子中使用的是TextInputFormat,他提供的RecordReder会将文本的一行的行号作为key,这一行的文本作为value。这就是自定义Map的输入是<LongWritable,  Text>的原因。然后调用自定义Map的map方法,将一个个<LongWritable,  Text>对输入给Map的map方法。注意输出应该符合自定义Map中定义的输出<  MyPariWritable,  NullWritable>。最终是生成一个List<  MyPariWritable,  NullWritable>。在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer。每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序。可以看到,这本身就是一个二次排序。在reduce阶段,reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序。然后开始构造一个key对应的value迭代器。这时就要用到分组,使用jobjob.setGroupingComparatorCla ss设置的分组函数类。只要这个比较器比较的两个key相同,他们就属于同一个组(本例中由于要求得每一个分区内的最小值,因此比较MyPariWritable类型的Key时,只需要比较自然键,这样就能保证只要两个MyPariWritable的自然键相同,则它们被送到Reduce端时候的Key就认为在相同的分组,由于该分组的Key只取分组中的第一个,而这些数据已经按照自定义MyPariWritable比较器排好序,则第一个Key正好包含了每一个自然键对应的最小值),它们的value放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。最后就是进入Reducer的reduce方法,reduce方法的输入是所有的key和它的value迭代器。同样注意输入与输出的类型必须与自定义的Reducer中声明的一致。一个文件作为reduce task的输入数据



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值