hadoop学习(七)----mapReduce原理以及操作过程

前面我们使用HDFS进行了相关的操作,也了解了HDFS的原理和机制,有了分布式文件系统我们如何去处理文件呢,这就的提到hadoop的第二个组成部分-MapReduce。

MapReduce充分借鉴了分而治之的思想,将一个数据的处理过程分为Map(映射)和Reduce(处理)两步。那么用户只需要将数据以需要的格式交给reduce函数处理就能轻松实现分布式的计算,很多的工作都由mapReduce框架为我们封装好,大大简化了操作流程。

1 MapReduce的编程思想

MapReduce的设计思路来源于LISP和其他的函数式编程语言中的映射和化简操作。数据操作的最小单位是一个键值对。用户在使用MapReduce编程模型的时候第一步就是要将数据转化为键值对的形式,map函数会以键值对作为输入,经过map函数的处理,产生新的键值对作为中间结果,然后MapReduce计算框架会自动将这些中间结果数据做聚合处理,然后将键相同的数据分发给reduce函数处理。reduce函数以键值对的形式对处理结果进行输出。要用表达式的形式表达的话,大致如下:

{key1,value1}----->{key2,List<value2>}----->{key3,value3}
2 MapReduce的运行环境

与HDFS相同的是,MapReduce计算框也是主从架构。支撑MapReduce计算框架的是JobTracker和TaskTracker两类后台进程。

2.1 JobTracker

Job Tracker 在集群中扮演了主的角色,它主要负责任务调度和集群资源监控这两个功能,但并不参与具体的计算。一个Hadoop 集群只有一个JobTracker ,存在单点故障的可能,所以必须运行在相对可靠的节点上,一JobTracker 出错,将导致集群所有正在运行的任务全部失败。

与HDFS 的NameNode 和DataNode 相似, TaskTracker 也会通过周期性的心跳向JobTracker汇报当前的健康状况和状态,心跳信息里面包括了自身计算资源的信息、被占用的计算资源的信息和正在运行中的任务的状态信息。JobTracker 则会根据各个TaskTracker 周期性发送过来的心跳信息综合考虑TaskTracker 的资源剩余量、作业优先级、作业提交时间等因素,为TaskTracker分配合适的任务。

2.2 TaskTracker

TaskTracker 在集群中扮模了从的角色,它主要负责汇报心跳和执行JobTracker 的命令这两个功能。一个集群可以有多个TaskTracker ,但一个节点只会有一个TaskTracker , 并且TaskTracker和DataNode 运行在同一个节点之中,这样, 一个节点既是计算节点又是存储节点。TaskTracker会周期性地将各种信息汇报给JobTracker ,而JobTracker 收到心跳信息, 会根据心跳信息和当前作业运行情况为该TaskTracker 下达命令, 主要包括启动任务、提交任务、杀死任务、杀死作业和重新初始化5 种命令。

2.3 客户端

用户通过客户端提交编写的MapReduce程序给JobTracker。

3 MapReduce 作业和任务

Map Reduce 作业( job )是用户提交的最小单位, 而Map/Reduce 任务( task ) 是MapReduce
计算的最小单位。当用户向Hadoop 提交一个MapReduce 作业时, JobTracker 的作业分解模块会将其分拆为
任务交由各个TaskTracker 执行, 在MapReduce 计算框架中,任务分为两种-Map 任务和Reduce 任务。

4 MapReduce 的计算资源划分

一个MapReduce 作业的计算工作都由TaskTracker 完成。用户向Hadoop 提交作业,Job Tracker 会将该作业拆分为多个任务, 并根据心跳信息交由空闲的TaskTracker 启动。一个Task Tracker 能够启动的任务数量是由TaskTracker 配置的任务槽( slot ) 决定。槽是Hadoop 的计算资源的表示模型, Hadoop 将各个节点上的多维度资源( CPU 、内存等)抽象成一维度的槽,这样就将多维度资源分配问题转换成一维度的槽分配的问题。在实际情况中,Map 任务和Reduce任务需要的计算资源不尽相同, Hadoop 又将槽分成Map 槽和Reduce 槽, 并且Map 任务只能使用Map槽, Reduce 任务只能使用Reduce槽。

Hadoop 的资源管理采用了静态资源设置方案,即每个节点配置好Map 槽和Reduce 槽的数量(配置项为mapred-site.xml 的mapred.task:tracker.map.tasks.maximum 和mapred.taskTracker.reduce.tasks.maximum),一旦Hadoop启动后将无法动态更改。

5 MapReduce 的局限性
  1. 从MapReduce 的特点可以看出MapReduce 的优点非常明显,但是MapReduce 也有其局限性,井不是处理海量数据的普适方法。它的局限性主要体现在以下几点:
    MapReduce 的执行速度慢。一个普通的MapReduce作业一般在分钟级别完成,复杂的作业或者数据量更大的情况下,也可能花费一小时或者更多,好在离线计算对于时间远没有OLTP那么敏感。所以MapReduce 现在不是,以后也不会是关系型数据库的终结者。MapReduce的慢主要是由于磁盘1/0 , MapReduce 作业通常都是数据密集型作业,大量的中间结果需要写到磁盘上并通过网络进行传输,这耗去了大量的时间。

  2. MapReduce过于底层。与SQL相比,MapReduce显得过于底层。对于普通的查询,一般人是不会希望写一个map 函数和reduce函数的。对于习惯于关系型数据库的用户,或者数据分析师来说,编写map 函数和reduce 函数无疑是一件头疼的事情。好在Hive的出现,大大改善了这种状况。

  3. 不是所有算法都能用MapReduce 实现。这意味着,不是所有算法都能实现并行。例如机器学习的模型训练, 这些算法需要状态共享或者参数间有依赖,且需要集中维护和更新。
6 来一个Hello world

本次hello world为word count程序,通过这个小程序来了解一下MapReduce的用法。

Mapper类:

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
    //声时一个IntWritable变量,作计数用,每出现一个key,给其一个value=1的值
    IntWritable one = new IntWritable(1);
    Text word = new Text();

    public void map(Object key, Text value, Context context) throws IOException,InterruptedException {
        //Hadoop读入的value是以行为单位的,其key为该行所对应的行号,因为我们要计算每个单词的数目,
        //默认以空格作为间隔,故用StringTokenizer辅助做字符串的拆分,也可以用string.split("")来作。
        StringTokenizer itr = new StringTokenizer(value.toString());
        //遍历每一个单词
        while(itr.hasMoreTokens()) {
            word.set(itr.nextToken());
            context.write(word, one);
        }
    }
}

以上就是Map打散过程。

上面我们看到有IntWritable这个数据类型,这里的Text相当于jdk中的String IntWritable相当于jdk的int类型,hadoop有8个基本数据类型,它们均实现了WritableComparable接口:

描述
BooleanWritable标准布尔型数值
ByteWritable单字节数值
DoubleWritable双字节数
FloatWritable浮点数
IntWritable整型数
LongWritable长整型数
Text使用UTF8格式存储的文本
NullWritable当《key,value》中的key或value为空时使用

下面接着写reduce类:

import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException,InterruptedException {
        int sum = 0;
        //因为map已经将文本处理为键值对的形式,所以在这里只用取出map中保存的键值
        for(IntWritable val:values) {
            sum += val.get();
        }
        result.set(sum);
        context.write(key,result);
    }
}

下面是主函数:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;
import org.apache.hadoop.util.GenericOptionsParser;

public class WordCount {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
        //判断一下命令行输入路径/输出路径是否齐全,即是否为两个参数
        if(otherArgs.length != 2) {
            System.err.println("Usage: wordcount <in> <out>");
            System.exit(2);
        }

        //此程序的执行,在hadoop看来是一个Job,故进行初始化job操作
        Job job = new Job(conf, "wordcount");
        //可以认为成,此程序要执行WordCount.class这个字节码文件
        job.setJarByClass(WordCount.class);
        //在这个job中,我用TokenizerMapper这个类的map函数
        job.setMapperClass(TokenizerMapper.class);
        //在这个job中,我用MyReducer这个类的reduce函数
        job.setCombinerClass(MyReducer.class);
        job.setReducerClass(MyReducer.class);
        //在reduce的输出时,key的输出类型为Text
        job.setOutputKeyClass(Text.class);
        //在reduce的输出时,value的输出类型为IntWritable
        job.setOutputValueClass(IntWritable.class);
        //初始化要计算word的文件的路径
        FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
        //初始化要计算word的文件的之后的结果的输出路径 
        FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
        //提交job到hadoop上去执行了,意思是指如果这个job真正的执行完了则主函数退出了,若没有真正的执行完就退出了。
        System.exit(job.waitForCompletion(true)?0:1);
    }
}

工程目录结构如下:

然后就是打jar包,我用的是idea,打包的过程就不表,自己百度一下。我打的jar包名为:hadoop.jar。打完之后将jar包上传至服务器。
在你的hadoop服务开启的状态下,输入如下命令:

#hadoop jar hadoop.jar cn.edu.hust.demo1.WordCount /user/input /user/output

中间的是主程序的路径,后面第一个路径是你的单词文档的路径,我将单词文档上传到/user/input下,/user/output的路径为输出路径。这个路径在执行上面这个命令之前不能存在,不然会报错。

执行完等一会之后,如果出现上面的输出表示运行成功。

下面是输出路径的内容:

我上传的txt 单词文件内容为:

hello world big
hello world xiaoming
the world is beautiful
xiaoming is my friend

运行完程序之后的单词统计结果为:

由此我们的单词统计就完成了。

下面我们借着词频统计来说一下MapReduce的运行过程。

7 MapReduce的运行过程深入了解

从前面的WordCount可以看出, 一个MapReduce作业经过了input,map,combine,reduce,output 五个阶段,其中combine阶段并不一定发生, map输出的中间结果被分发到reducer的过程被称为shuffle (数据混洗)。

7.1 从输入到输出的状态

在shuffle阶段还会发生copy(复制)和sort(排序)。
在MapReduce 的过程中,一个作业被分成Map和Reduce计算两个阶段,它们分别由两个或者多个Map任务和Reduce任务组成,这个在前面已经说过了。Reduce任务默认会在Map任务数量完成5%后才开始启动。

Map任务的执行过程可概括为:

  1. 首先通过用户指定的Inputformat类(如WordCount中的FilelnputFormat 类)中的getSplits方法和next方法将输入文件切片并解析成键值对作为map函数的输入。

  2. 然后map函数经过处理之后输出并将中间结果交给指定的Partitioner处理,确保中间结果分发到指定的Reduce任务处理,此时如果用户指定了Combiner,将执行combine操作。

  3. 最后map函数将中间结果保存到本地。

Reduce 任务的执行过程可概括为:

首先需要将已经完成的Map任务的中问结果复制到Reduce任务所在的节点,待数据复制完成后,再以key进行排序,通过排序,将所有key相同的数据交给reduce函数处理,处理完成后,结果直接输出到HDFS上。

7.2 input过程

如果使用HDFS上的文件作为MapReduce的输入(由于用户的数据大部分数据是以文件的
形式存储在HDFS上,所以这是最常见的情况)MapReduce计算框架首先会用
org.apache.hadoop.mapreduce.InputFormat类的子类FilelnputFormat类将作为输入的HDFS 上的文件切分形成输入分片(InputSplit),每个InputSplit将作为一个Map任务的输入,再将InputSplit解析为键值对。InputSplit的大小和数量对于MapReduce作业的性能有非常大的影响,因此有必要深入了解InputSplit 。

在List getSplits(JobContext job)方法中会对JobTracker进行拆分,确定需要多少个taskTracker。

该方法中调用了computeSplitSize(blockSize, minSize, maxSize)方法。用来计算拆分多少个taskTracker。我们看到三个参数:blockSize, minSize, maxSize,

  1. 其中minSize由mapred-site.xml文件中的配置项mapred.min.split.size决定,默认为l;
  2. maxSize由mapred-site.xml文件中的配置项mapred.max.split.size决定,默认为9223372036854775807;
  3. 而blockSize也是由hdfs-site.xml文件中的配置项dfs.block.size决定,默认为67108864字节(64MB)。

一般来说, dfs.block.size的大小是确定不变的,所以得到目标InputSplit大小,只需改变mapred.min.split.size和mapred.max.split.size的大小即可。

对于Map任务来说,处理的单位为一个InputSplit。而InputSplit是一个逻辑概念, InputSplit所包含的数据是仍然是存储在HDFS 的块里面,它们之间的关系如下图所示:

InputSplit 可以不和块对齐,根据前面的公式也可以看出,一个InputSplit的大小可以大于一个块的大小亦可以小于一个块的大小。Hadoop在进行任务调度的时候,会优先考虑本节点的数据,如果本节点没有可处理的数据或者是还需要其他节点的数据, Map 任务所在的节点会从其他节点将数据通过网络传输给自己。当InputSplit的容量大于块的容量,Map任务就必须从其他节点读取一部分数据,这样就不能实现完全数据本地性,所以当使用FileIputFormat实现InputFormat时,应尽量使InputSplit的大小和块的大小相同以提高Map任务计算的数据本地性。

7.3 Map及中间结果的输出

InputSplit将解析好的键值对交给用户编写的map函数处理,处理后的中间结果会写到本地磁盘上,在刷写磁盘的过程中,还做了partition(分区)和sort(排序)的操作。map 函数产生输出时,并不是简单地刷写磁盘。为了保证I/0 效率,采取了先写到内存的环形缓冲区,并作一次预排序。

每个Map任务都有一个环形内存缓冲区,用于存储map函数的输出。默认情况下,缓冲
区的大小是100 MB ,该值可以通过mapred-site.xml文件的io.sort.mb的配置项配置。一旦缓冲区内容达到阀值(由mapred-site.xml文件的io.sort.spill.percent 的值决定,默认为0.80或80%),一个后台线程便会将缓冲区的内容溢写(spill)到磁盘中。在写磁盘的过程中,map函数的输出继续被写到缓冲区,但如果在此期间缓冲区被填满,map会阻塞直到写磁盘过程完成。写磁盘会以轮询的方式写到mapred.local.dir(mapred-site.xml文件的配置项)配置的作业特定目录下。

在写磁盘之前,线程会根据数据最终要传送到的Reducer把缓冲区的数据划分成(默认是按照键)相应的分区。在每个分区中,后台线程技键进行内排序,此时如果有一个Combiner,它会在排序后的输出上运行。

如果己经指定Combiner且溢出写次数至少为3时,Combiner就会在输出文件写到磁盘之前运行。如前文所述,Combiner可以多次运行,并不影响输出结果。运行Combiner 的意义在于使map 输出的中间结果更紧凑,使得写到本地磁盘和传给Reducer的数据更少。

为了提高磁盘I/O 性能,可以考虑压缩map的输出,这样会让写磁盘的速度更快,节约磁盘空间,从而使传送给Reducer的数据量减少。默认情况下,map的输出是不压缩的,但只要将mapred-site.xml文件的配置项mapred.compress.map.output设为true 即可开启压缩功能。使用的压缩库由mapred-site.xml文件的配置项mapred.map.output.compression.codec指定。

map输出的中间结果存储的格式为IFile,IFile是一种支持行压缩的存储格式。Reducer通过HTTP方式得到输出文件的分区。将map输出的中间结果(map 输出)发送
到Reducer的工作线程的数量由mapred-site.xml文件的tasktracker.http.threads 配置项决定,此配置针对每个节点,即每个TaskTracker,而不是每个Map任务,默认是40,可以根据作业大小,集群规模以及节点的计算能力而增大。

7.4 shuffle

shuffle,也叫数据混洗。在某些语境中,代表map函数产生输出到reduce的消化输入的整个过程,上面我们也有一个流程图中看到shuffle是在combine和reduce之间的过程,前面map已经处理完了数据那为什么还要shuffle呢,这就是shuffle的功能:完成数据和合并和排序。

前面我们说了,Map任务输出的结果位于运行Map任务的TaskTracker所在的节点的本地磁盘上。TaskTracker需要为这些分区文件(map输出)运行Reduce任务。但是 Reduce任务可能需要多个Map 任务的输出作为其特殊的分区文件。每个Map 任务的完成时间可能不同,当只要有一个任务完成,Reduce 任务就开始复制其输出。这就是shuffle中的copy阶段。Reduce任务有少量复制线程,可以井行取得Map 任务的输出,默认值是5个线程,该值可以通过设置mapred-site.xml的mapred.reduce.parallel.copies配置项来改变。

如果map输出相当小,则会被复制到Reducer所在的TaskTracker的内存的缓冲区中,缓冲区的大小由mapred-site.xml文件中的mapred.job.shuffie.input.buffer.percent配置项指定。否则,
map输出将会被复制到磁盘。一旦内存缓冲区达到阀值大小(由mapred-site.xml 文件的
mapred.job.shuffle.merge.percent 配置项决定)或缓冲区的文件数达到阀值大小(由
mapred-site.xml文件的mapred.inmem.merge.threshold配置项决定),则合并后溢写到磁盘中。

随着溢写到磁盘的文件增多,后台线程会将它们合并为更大的、有序的文件,这会为后面的合并节省时间为了合并,压缩的中间结果都将在内存中解压缩。

复制完所有的map输出,shuffle进入sort阶段。这个阶段将合并map的输出文件,并维持其顺序排序,其实做的是归并排序。排序的过程是循环进行,如果有50个map的输出文件,而合并因子(由mapred-site.xml文件的io.sort.factor配置项决定,默认为10)为10,合并操作将进行5次,每次将10个文件合,并成一个文件,最后会有5个文件,这5个文件由于不满足合并条件(文件数小于合并因子),则不会进行合井,将会直接把这5 个文件交给reduce函数处理。至此,shuffle 阶段完成。

从shuffle的过程可以看出,Map任务处理的是一个InputSplit,而Reduce任务处理的是所有Map任务同一个分区的中间结果。

reduce阶段操作的实质就是对经过shuffle处理后的文件调用reduce函数处理。由于经过了shuffle的处理,文件都是按键分区且有序,对相同分区的文件调用一次reduce函数处理。与map的中间结果不同的是,reduce的输出一般为HDFS。

7.5 sort

上面我们讲到shuffle的过程在合并Map的时候顺带做了排序。其实排序贯穿Map和Reduce的所有任务,在MapReduce任务中一共发生了三次排序:

  1. 当map函数产生输出时,会首先写入内存的环形缓冲区,当达到设定的阀值,在刷写磁盘之前,后台线程会将缓冲区的数据划分戚相应的分区。在每个分区中,后台线程技键进行内排序。
  2. 在Map任务完成之前,磁盘上存在多个己经分好区并排好序的、大小和缓冲区一样的溢写文件,这时溢写文件将被合并成一个己分区且己排序的输出文件。由于溢写文件己经经过第一次排序,所以合并文件时只需再做一次排序就可使输出文件整体有序。
  3. 在shuffle 阶段,需要将多个Map任务的输出文件合并,由于经过第二次排序,所以合并文件时只需再做一次排序就可使输出文件整体有序。

在这3次排序中第一次是在内存缓冲区做的内排序,使用的算法是快速排序,第二次排序和第三次排序都是在文件合并阶段发生的,使用的是归并排序。

至此mapReduce的工作过程我们就说完啦,多看几次好好消化消化吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值