10天Hadoop快速突击(2)——MapReduce计算模型详解

本文深入解析MapReduce计算模型,包括其核心组件JobTracker和TaskTracker的功能,Map和Reduce阶段的工作原理,以及如何通过优化任务调度、数据预处理等方式提升性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


MapReduce计算模型详解

为什么要用MapReduce?

        非常简单、易于实现且扩展性强。MapReduce适合处理海量数据,它会被多台主机同时处理,通常会有较快的速度。

MapReduce计算模型

        要了解MapReduce,首先需要了解MapReduce的载体是什么。在Hadoop中,用于执行MapReduce任务的机器有两个角色:一个是JobTracker,另一个是TaskTrackerJobTracker是用于管理和调度工作的TaskTracker是用于执行工作的。一个Hadoop集群中只有一台JobTracker。

        MapReduce Job

        在Hadoop中,每个MapReduce任务都被初始化为一个Job。每个Job又可以分为两个阶段:Map阶段和Reduce阶段。这两个阶段分别用两个函数来表示,即Map函数和Reduce函数。Map函数接收一个<key,value>形式的输入,然后产生同样为<key,value>形式的中间输出,Hadoop会负责将所有具有相同中间key值的value集合到一起传递给Reduce函数,Reduce函数接收一个如<key,(list of values)>形式的输入,然后对这个value集合进行处理并输出结果,Reduce的输出也是<key,value>形式的。
        为了方便理解,分别将三个<key,value>对标记为<k1,v1>、<k2,v2>、<k3,v3>,那么上面所述的过程就可以用图3-1来表示了。

4777709bd087bc09562088b132ea78f47ff769be
        
JobConf conf = new JobConf(MyMapre.class);
conf.setJobName("wordcount");

conf.setInputFormat(TextInputFormat.class);
conf.setOutputFormat(TextOutputFormat.class);

conf.setMapperClass(Map.class);
conf.setReducerClass(Reduce.class);

FileInputFormat.setInputPaths(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));

        Job的初始化过程。Main函数调用Jobconf类来对MapReduce Job进行初始化,然后调用setJobName()方法命名这个Job。对Job进行合理的命名有助于更快地找到Job,以便在JobTracker和TaskTracker的页面中对其进行监视。接着就会调用setInputPath()和setOutputPath()设置输入输出路径。

  1. InputFormat()和InputSplit
    InputSplit是Hadoop中用来把输入数据传送给每个单独的Map,InputSplit存储的并非数据本身,而是一个分片长度和一个记录数据位置的数组。生成InputSplit的方法可以通过Inputformat()来设置。当数据传送给Map时,Map会将输入分片传送到InputFormat()上,InputFormat()则调用getRecordReader()方法生成RecordReader,RecordReader再通过creatKey()、creatValue()方法创建可供Map处理的<key,value>对,即<k1,v2>。简而言之,InputFormat()方法是用来生成可供Map处理的<key,value>对的。

        Hadoop预定义了多种方法将不同类型的输入数据转化为Map能够处理的<key,value>对,它们都继承自InputFormat,分别是:

BaileyBorweinPlouffe.BbpInputFormat 
 ComposableInputFormat
 CompositeInputFormat
 DBInputFormat
 DistSum.Machine.AbstractInputFormat
 FileInputFormat
其中,FileInputFormat又有多个子类,分别为:
 CombineFileInputFormat
 KeyValueTextInputFormat
 NLineInputFormat
 SequenceFileInputFormat
 TeraInputFormat
 TextInputFormat

        其中,TextInputFormat是Hadoop默认的输入方法,在TextInputFormat中,每个文件(或其一部分)都会单独作为Map的输入,而这是继承自FileInputFormat的。之后,每行数据都会生成一条记录,每条记录则表示成<key,value>形式:
                key值是每个数据的记录在数据分片中的字节偏移量,数据类型是LongWritable;
                value值是每行的内容,数据类型是Text。
        也就是说,输入数据会以如下的形式被传入Map中

file01:
hello world bye world
file02
hello hadoop bye hadoop

        因为file01和file02都会被单独输入到一个Map中,因此它们的key值都是0。

   2.OutputFormat()

        对于每一种输入格式都有一种输出格式与其对应。同样,默认的输出格式是TextOutputFormat,这种输出方式与输入类似,会将每条记录以一行的形式存入文本文件。不过,它的键和值可以是任意形式的,因为程序内部会调用toString()方法将键和值转换为String类型再输出。

    3. Map()和Reduce()
        Map()方法和Reduce()方法是重点,从前面的内容知道,Map()函数接收经过InputFormat处理所产生的<k1, v1>,然后输出<k2, v2>。WordCount的Map()函数如下:
public class MyMapre {
   public static class Map extends MapReduceBase implements Mapper<LongWritable, Text, Text, IntWritable> {
     private final static IntWritable one = new IntWritable(1);

     private Text word = new Text();

     public void map(LongWritable key, Text value, 
OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
       String line = value.toString();
       StringTokenizer tokenizer = new StringTokenizer(line);
       while (tokenizer.hasMoreTokens()) {
         word.set(tokenizer.nextToken());
         output.collect(word, one);
     }
   }

}

        Map()函数继承自MapReduceBase,并且它实现了Mapper接口,此接口是一个范型类型,它有4种形式的参数,分别用来指定Map()的输入key值类型、输入value值类型、输出key值类型和输出value值类型。在本例中,因为使用的是TextInputFormat,它的输出key值是LongWritable类型,输出value值是Text类型,所以Map()的输入类型即为<LongWriteable,Text>。如前面的内容所述,在本例中需要输出<word,1>这样的形式,因此输出的key值类型是Text,输出的value值类型是IntWritable。

        实现此接口类还需要实现Map()方法,Map()方法会负责具体对输入进行操作,在本例中,Map()方法对输入的行以空格为单位进行切分,然后使用OutputCollect收集输出的。即<k2,v2>。

    Reduce()函数如下:

public static class Reduce extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable> {
   public void reduce(Text key, Iterator<IntWritable> values, 
   OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
    int sum = 0;
    while (values.hasNext()) {
    sum += values.next().get();
    }
    output.collect(key, new IntWritable(sum));
    }
}

        Reduce()函数也继承自MapReduceBase,需要实现Reducer接口。Reduce()函数以Map()的输出作为输入,因此Reduce()的输入类型是<Text,IntWritable>。而Reduce()的输出是单词和它的数目,因此,它的输出类型是<Text,IntWriteable>。Reduce()函数也要实现Reduce()方法,在此方法中,Reduce()函数将输入的key值作为输出的key值,然后将获得的多个value值加起来,作为输出的value值。

    4.运行MapReduce程序

    首先安装Hadoop,然后输入编译打包生成的JAR程序,如下所示(以Hadoop-0.20.2为例,安装路径是~/hadoop):

mkdir FirstJar
javac -classpath ~/hadoop/hadoop-0.20.2-core.jar -d  FirstJar
WordCount.java
jar -cvf wordcount.jar -C FirstJar/ .

          首先建立FirstJar,然后编译文件生成.class,存放到文件夹FirstJar中,并将FirstJar中的文件打包生成wordcount.jar文件。
接着上传输入文件(输入文件是file01,file02,存放在~/input):

~/hadoop/bin/hadoop dfs –mkdir input
~/hadoop/bin/hadoop dfs –put ~/input/file0* input

        在此上传过程中,先建立文件夹input,然后上传文件file01、file02到input中。

        最后运行生成的JAR文件,为了叙述方便,先将生成的JAR文件放入Hadoop的安装文件夹中(HADOOP_HOME),然后运行如下命令:

~/hadoop/bin/hadoop jar wordcount.jar WordCount input output
        Hadoop命令(注意不是Hadoop本身)会启动一个JVM来运行这个MapReduce程序,并自动获取Hadoop的配置,同时把类的路径(及其依赖关系)加入到Hadoop的库中。
5. 新的API
        从0.20.2版本开始,Hadoop提供了一个新的API。新的API是在org.apache.hadoop.mapreduce中的,旧版的API则在org.apache.hadoop.mapred中。新的API不兼容旧的API。
        在新的API中,Mapper与Reducer已经不是接口而是抽象类。而且Map函数与Reduce函数也已经不再实现Mapper和Reducer接口,而是继承Mapper和Reducer抽象类。这样做更容易扩展,因为添加方法到抽象类中更容易。
      新的API中更广泛地使用了context对象,并使用MapContext进行MapReduce间的通信,MapContext同时充当OutputCollector和Reporter的角色。
        Job的配置统一由Configurartion来完成,而不必额外地使用JobConf对守护进程进行配置。
        由Job类来负责Job的控制,而不是JobClient,JobClient在新的API中已经被删除。

        此外,新的API同时支持“推”和“拉”式的迭代方式。在以往的操作中,对是被推入到Map中的,但是在新的API中,允许程序将数据拉入Map中,Reduce也一样。这样做更加方便程序分批处理数据。

    MapReduce的数据流和控制流

        简单的控制流大概是这样的:JobTracker调度任务给TaskTracker,TaskTracker执行任务时,会返回进度报告。JobTracker则会记录进度的进行状况,如果某个TaskTracker上的任务执行失败,那么JobTracker会把这个任务分配给另一台TaskTracker,直到任务执行完成。

        数据首先按照TextInputFormat形式被处理成多个InputSplit,然后输入到多个Map中,Map程序会读取InputSplit指定位置的数据,然后按照设定的方式处理该数据,最后写入到本地磁盘中。注意,这里并不是写到HDFS上,因为Map的输出在Job完成后即可删除了,因此不需要存储到HDFS上,虽然存储到HDFS上会更安全,但是因为网络传输会降低MapReduce任务的执行效率,因此Map的输出文件是写在本地磁盘上的。如果Map程序在没来得及将数据传送给Reduce时就崩溃了(程序出错或机器崩溃),那么JobTracker只需要另选一台机器重新执行这个Task就可以了。

        Reduce会读取Map的输出数据,合并value,然后将它们输出到HDFS上。Reduce的输出会占用很多的网络带宽,不过这与上传数据一样是不可避免的。

        除此之外,有两种情况需要注意:
        1)MapReduce在执行过程中往往不止一个Reduce Task,Reduce Task的数量是可以程序指定的。当存在多个Reduce Task时,每个Reduce会搜集一个或多个key值。需要注意的是,当出现多个Reduce Task时,每个Reduce Task都会生成一个输出文件。

        2)另外,没有Reduce任务的时候,系统会直接将Map的输出结果作为最终结果,同时Map Task的数量可以看做是Reduce Task的数量,即有多少个Map Task就有多少个输出文件。

MapReduce任务的优化

        MapReduce计算模型的优化涉及了方方面面的内容,但是主要集中在两个方面:一是计算性能方面的优化;二是I/O操作方面的优化。这其中,又包含六个方面的内容。

  1. 任务调度
    任务调度是Hadoop中非常重要的一环,这个优化又涉及两个方面的内容。计算方面:Hadoop总会优先将任务分配给空闲的机器,使所有的任务能公平地分享系统资源。I/O方面:Hadoop会尽量将Map任务分配给InputSplit所在的机器,以减少网络I/O的消耗。
  2. 数据预处理与InputSplit的大小
    MapReduce任务擅长处理少量的大数据,而在处理大量的小数据时,MapReduce的性能就会逊色很多。因此在提交MapReduce任务前可以先对数据进行一次预处理,将数据合并以提高MapReduce任务的执行效率,这个办法往往很有效。如果这还不行,可以参考Map任务的运行时间,当一个Map任务只需要运行几秒就可以结束时,就需要考虑是否应该给它分配更多的数据。通常而言,一个Map任务的运行时间在一分钟左右比较合适,可以通过设置Map的输入数据大小来调节Map的运行时间。在FileInputFormat中(除了CombineFileInputFormat),Hadoop会在处理每个Block后将其作为一个InputSplit,因此合理地设置block块大小是很重要的调节方式。除此之外,也可以通过合理地设置Map任务的数量来调节Map任务的数据输入。
  3. Map和Reduce任务的数量
    合理地设置Map任务与Reduce任务的数量对提高MapReduce任务的效率是非常重要的。默认的设置往往不能很好地体现出MapReduce任务的需求,不过,设置它们的数量也要有一定的实践经验。

        首先要定义两个概念——Map/Reduce任务槽。Map/Reduce任务槽就是这个集群能够同时运行的Map/Reduce任务的最大数量。比如,在一个具有1200台机器的集群中,设置每台机器最多可以同时运行10个Map任务,5个Reduce任务。那么这个集群的Map任务槽就是12000,Reduce任务槽是6000。任务槽可以帮助对任务调度进行设置。

        设置MapReduce任务的Map数量主要参考的是Map的运行时间,设置Reduce任务的数量就只需要参考任务槽的设置即可。一般来说,Reduce任务的数量应该是Reduce任务槽的0.95倍或是1.75倍,这是基于不同的考虑来决定的。当Reduce任务的数量是任务槽的0.95倍时,如果一个Reduce任务失败,Hadoop可以很快地找到一台空闲的机器重新执行这个任务。当Reduce任务的数量是任务槽的1.75倍时,执行速度快的机器可以获得更多的Reduce任务,因此可以使负载更加均衡,以提高任务的处理速度    

    4. Combine函数

        Combine函数是用于本地合并数据的函数。在有些情况下,Map函数产生的中间数据会有很多是重复的,比如在一个简单的WordCount程序中,因为词频是接近与一个zipf分布的,每个Map任务可能会产生成千上万个记录,若将这些记录一一传送给Reduce任务是很耗时的。所以,MapReduce框架运行用户写的combine函数用于本地合并,这会大大减少网络I/O操作的消耗。此时就可以利用combine函数先计算出在这个Block中单词the的个数。合理地设计combine函数会有效地减少网络传输的数据量,提高MapReduce的效率。

        在MapReduce程序中使用combine很简单,只需在程序中添加如下内容:

job.setCombinerClass(combine.class);
在WordCount程序中,可以指定Reduce类为combine函数,具体如下:
job.setCombinerClass(Reduce.class);
     5.压缩
        编写MapReduce程序时,可以选择对Map的输出和最终的输出结果进行压缩(同时可以选择压缩方式)。在一些情况下,Map的中间输出可能会很大,对其进行压缩可以有效地减少网络上的数据传输量。对最终结果的压缩虽然会减少数据写HDFS的时间,但是也会对读取产生一定的影响,因此要根据实际情况来选择。
     6.自定义comparator

        在Hadoop中,可以自定义数据类型以实现更复杂的目的,比如,当读者想实现k-means算法(一个基础的聚类算法)时可以定义k个整数的集合。自定义Hadoop数据类型时,推荐自定义comparator来实现数据的二进制比较,这样可以省去数据序列化和反序列化的时间,提高程序的运行效率。

    Hadoop流 

        Hadoop流提供了一个API,允许用户使用任何脚本语言写Map函数或Reduce函数。Hadoop流的关键是,它使用UNIX标准流作为程序与Hadoop之间的接口。因此,任何程序只要可以从标准输入流中读取数据并且可以写入数据到标准输出流,那么就可以通过Hadoop流使用其他语言编写MapReduce程序的Map函数或Reduce函数。

bin/hadoop jar contrib/streaming/hadoop-0.20.2-streaming.jar -input input -output output -mapper /bin/cat –reducer usr/bin/wc
        从这个例子中可以看到,Hadoop流引入的包是hadoop-0.20.2-streaming.jar,并且具有如下命令:
            -input 指明输入文件路径
            -output 指明输出文件路径
            -mapper 指定map函数

            -reducer 指定reduce函数

        Hadoop流的工作原理并不复杂,其中Map的工作原理如图3-4所示(Reduce与其相同)。

d6ed847c4cef6859dfb25bcc5fde0f8acaf19e76

        当一个可执行文件作为Mapper时,每一个Map任务会以一个独立的进程启动这个可执行文件,然后在Map任务运行时,会把输入切分成行提供给可执行文件,并作为它的标准输入(stdin)内容。当可执行文件运行出结果时,Map从标准输出(stdout)中收集数据,并将其转化为<key,value>对,作为Map的输出。
        Reduce与Map相同,如果可执行文件做Reducer时,Reduce任务会启动这个可执行文件,并且将对转化为行作为这个可执行文件的标准输入(stdin)。然后Reduce会收集这个可执行文件的标准输出(stdout)的内容。并把每一行转化为<key,value>对,作为Reduce的输出。
        Map与Reduce将输出转化为<key,value>对的默认方法是:将每行的第一个tab符号(制表符)之前的内容作为key,之后的内容作为value。如果没有tab符号,那么这一行的所有内容会作为key,而value值为null。当然这是可以更改的。
        值得一提的是,可以使用Java类作为Map,而用一个可执行程序作为Reduce;或使用Java类作为Reduce,而用可执行程序作为Map。

Hadoop流的命令
Hadoop流提供自己的流命令选项及一个通用的命令选项,用于设置Hadoop流任务。首先介绍一下流命令。

  1. Hadoop流命令选项

    Hadoop流命令
    参数可选/必选参数可选/必选
    -input必选-cmdenv可选
    -output必选-inputreader可选
    -mapper必选-verbose可选
    -reducer必选-lazyOutput可选
    -file可选-numReduce tasks可选
    -inputformat可选-mapdebug可选
    -outputformat可选-reducedebug可选
    -partitioner可选-io可选
    -combiner可选  

            表3-1所示的Hadoop流命令中,必选的4个很好理解,分别用于指定输入/输出文件的位置及Map/Reduce函数。在其他的可选命令中,这里我们只解释常用的几个。
    -file
    -file指令用于将文件加入到Hadoop的Job中。上面的例子中,cat和wc都是Linux系统中的命令,而在Hadoop流的使用中,往往需要使用自己写的文件(作为Map函数或Reduce函数)。一般而言,这些文件是Hadoop集群中的机器上没有的,这时就需要使用Hadoop流中的-file命令将这个可执行文件加入到Hadoop的Job中。

        -combiner

        这个命令用来加入combiner程序。

        -inputformat和-outputformat

        这两个命令用来设置输入输出文件的处理方法,这两个命令后面的参数必须是Java类。

  Hadoop流通用的命令选项

        Hadoop流的通用命令用来配置Hadoop流的Job。需要注意的是,如果使用这部分配置,就必须将其置于流命令配置之前,否则命令会失败。

Hadoop流的Job设置命令
参数可选/必选参数可选/参数
-conf可选-files可选
-D可选-libjars可选
-fs可选-archives可选
-jt可选  

        Hadoop流的API是一个扩展性非常强的框架,它与程序相连的部分只有数据,因此可以接受任何适用于UNIX标准输入/输出的脚本语言,比如Bash、PHP、Ruby、Python等

    Hadoop Pipes

        Hadoop Pipes提供了一个在Hadoop上运行C++程序的方法。与流不同的是,流使用的是标准输入输出作为可执行程序与Hadoop相关进程间通信的工具,而Pipes使用的是Sockets。

        具体示例程序,请参考https://yq.aliyun.com/articles/174041

总结:

    MapReduce的计算模型中关键内容是一个流程四个方法

        一个流程指的是数据流程,输入数据到<k1,v1>、<k1,v1>到<k2,v2>、<k2,v2>到<k3,v3>、<k3,v3>到输出数据。

        四个方法就是这个数据转换过程中使用的方法(分别是InputFormat、Map、Reduce、OutputFormat),以及其对应的转换过程。

参考内容:

华章科技出版的《Hadoop实战》第二版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

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

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

打赏作者

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

抵扣说明:

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

余额充值