1.定义

是一个分布式运算程序的编程框架,能将用户编写的业务逻辑代码自带默认组件整合成一个完成的分布式运算程序。

2.优缺点

2.1优点

2.1.1易于编程

只需要简单的实现一些接口,就可以完成一个分布式程序。

2.1.2高容错性

mr程序可以部署在多台机器上,其中一台挂了,可以把上面的计算任务转移到另外一个节点上运行,由hadoop内部自动完成。

2.1.3良好的扩展性

可以通过增加机器来有效扩展其计算能力。

2.1.4海量数据的离线处理

实现千台服务器集群并发工作,提供数据处理能力。

2.2缺点

2.2.1实时计算(x)

无法处理毫秒、秒级响应。

2.2.2流式计算(x)

MR的输入数据集是静态的无法适应流式计算动态输入数据。

2.2.3有向无环图计算(x)

由于mr作业的输出结果都会写入到磁盘中,当存在多个依赖的应用程序时,前一个应用的输出作为后一个应用的输入,这样就会导致大量的磁盘IO,影响性能。

3.初步了解MR

3.1实例进程

一个完整的MR程序在分布式运行时有三类实例进程

3.1.1MrAppMaster

负责整个程序的过程调度及状态协调。

3.1.2MapTask

负责Map阶段的整个数据处理流程

3.1.3ReduceTask

负责Reduce阶段的整个数据处理流程

3.2编写规范

我们通常在编写MR的java程序的时候,一般有三部分即XxMapper、XxReducer、XxDriver。后面以wordCount案例为例子说明

3.2.1Mapper

public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

    private final Text outK = new Text();
    private final IntWritable outV = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //拿到每行
        String line = value.toString();

        String[] words = line.split(" ");

        for (String word : words) {
            outK.set(word);
            context.write(outK, outV);
        }
    }
}

①我们创建的类WordCountMapper需要继承父类Mapper。

②其中还需要指定Mapper的输入输出的kv类型。

③其中Mapper的输入涉及到InputFormat,其默认是TextFormat,表示输入的k是偏移量,v是一行数据,所以大多数情况下输入的KV基本是LongWritable和Text。 

hadoop流处理 hadoop流式计算_源码

④至于Mapper输出的KV是根据自己的业务来定,比如WordCount案例Mapper阶段将单词变为(word,1)的格式。所以k是Text,v是IntWritable。

⑤重写map方法,这里就是我们重点编程的地方,主要是写业务逻辑,可以简单的理解为输入数据中的每一行都会执行这个map方法,也就是说有多少行,map就执行多少次,所以编程的时候我们就可以把重心放在处理每行上。

扩展:其实Mapper阶段还可以重写setup和cleanup方法 ,其中setup主要做一些初始化或者预先操作,比如可以在setup阶段读取缓存中数据用于map方法处理业务逻辑;cleanup主要用于最后收尾,比如如果之前打开了流,可以在这里关闭。

hadoop流处理 hadoop流式计算_big data_02

3.2.2Reducer

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

    private final IntWritable outV = new IntWritable();;

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;

        for (IntWritable value : values) {
            sum += value.get();
        }

        outV.set(sum);

        context.write(key, outV);
    }
}

①我们创建的类WordCountReducer需要继承父类Reducer。

②同样的需要指定Reducer的输入输出类型。

③至于其输入类型,一般情况和Mapper的输出类型相同。

④输出类型由我们业务来定,由于最后输出的是(word,count),所以k是Text,v是IntWritable。

⑤重写reduce方法,在reduce中写业务逻辑,首先我们要有一个简单认知,在reduce阶段的时候,mapper阶段得到的结果相同的key会被分到一组,这一组中的value则聚合成一个集合,和spark中的groupByKey很像。简单来说就是加入mapper得到结果(emt,1),(emt,1),(emt,1)就会在执行reduce被分组为(emt,col(1,1,1));也就是reduce执行的次数就是mapper得到的结果中key的种类数。我们此时只需要在reduce中对col(1,1,1)进行简单的相加操作,就能得到(emt,3)了。

3.2.3Driver

这个比较八股文,理解加背诵即可。

public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        //2.设置jar路径
        job.setJarByClass(WordCountDriver.class);

        //3.关联mapper和reducer
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        //4.设置map输出的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        //5.设置最终输出的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        //可选。手动设置reduce任务个数
        job.setNumReduceTasks(2);

        //6.设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("datas\\mr\\hello.txt"));
        FileOutputFormat.setOutputPath(job, new Path("datas\\mr\\output1"));
        //7.提交job
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

由于我这些程序就只是简单的用着本地hadoop跑的。

如果是yarn集群话,相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是
封装了MapReduce程序相关运行参数的job对象

3.3(反)序列化

其实对java有一定基础的小伙伴对于序列化一定不会陌生了。

3.3.1什么是(反)序列化

序列化是把内存中的对象转换为字节序列以便于持久化网络传输

反序列化是将字节序列或者硬盘的持久化数据转换成内存中的对象。

3.3.2为什么要序列化

存在内存中的对象断电就消失且只能在本地进程使用,不能被发送到网络上;也就是说序列化帮助存储与网络传输

3.3.3Hadoop序列化vsJava序列化

因为java的序列化不够轻量,被序列化后的对象会附带较多的额外信息,比如校验信息,头header等等,传输不够高效,于是hadoop自己使用一套更轻量的序列化机制。从Serializable变为Writable。与java类型的对应关系如下,基本就是后面多了个Writable后缀,其中需要特别注意的是String并不是简单加后缀,而是变成了Text

Java类型

Hadoop Writable类型

Boolean

BooleanWritable

Byte

ByteWritable

Int

IntWritable

Float

FloatWritable

Long

LongWritable

Double

DoubleWritable

String

Text

Map

MapWritable

Array

ArrayWritable

Null

NullWritable

3.3.4自定义对象的序列化?

需要以下几个步骤:

①实现Wirtable接口

②重写序列化方法和反序列化方法,且顺序完全一致

hadoop流处理 hadoop流式计算_hadoop_03

③根据业务最后输出格式的需要,适当重写toString方法

hadoop流处理 hadoop流式计算_hadoop流处理_04

 ④如何后面处理的过程中该对象被作为了k使用,则必须实现comparable接口并重写compareTo方法,因为mr框架的shuffle过程会对key进行排序。

hadoop流处理 hadoop流式计算_源码_05

hadoop流处理 hadoop流式计算_源码_06

4深入理解MR

4.1MR基本组成框架

hadoop流处理 hadoop流式计算_hadoop_07

4.2Job提交

详情见这篇文章:Job提交流程

4.3InputFormat

从源码中我们可以发现InputFormat是抽象类

hadoop流处理 hadoop流式计算_big data_08

我们发现有好多类继承了它并实现了抽象方法getSplits

hadoop流处理 hadoop流式计算_hadoop_09

hadoop流处理 hadoop流式计算_big data_10

4.3.1TextInputFormat

而其中FileInputFormat也有许多类继承其,而默认的最常见的就是TextInputFormat,按行读取每条记录。是存储该行在整个文件中的起始字节偏移量, LongWritable类型。是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。

hadoop流处理 hadoop流式计算_big data_11

4.3.2CombineTextInputFormat

 由于之前我们分析源码的时候说过FileInputFormat是每个文件单独切片的,也就是说一个小文件它一定会单独切成一片,由于每个切片都会开启一个MapTask,你开启一个MapTask少说也得一个CPU,1G内存,如果该文件就1kb,有必要吗?所以说在有多个小文件时,这样做会产生大量的MapTask,效率低。

而CombineTextInputFormat就适用于小文件过多的情景,将多个小文件逻辑上规划到一个切片中,让其只交给一个MapTask处理。

其切片机制包含两部分:虚拟存储过程和切片过程二部分。

hadoop流处理 hadoop流式计算_mapreduce_12

4.4MapTask

详情见这篇文章:MapTask

4.5shuffle机制

hadoop流处理 hadoop流式计算_hadoop_13

Map方法之后,Reduce方法之前的数据处理过程称之为shuffle。

 4.5.1可选流程combine

Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量,使得传到reduce的数据量小了,比如wordcount中的(emt,1),(emt,1)直接先处理为(emt,2)。有点像之前spark探讨的reduceByKey和groupByKey,详情见这篇文章:spark常用转换算子

不过combiner的使用的前提是不影响最终业务结果,比如求平均数就不宜使用。

自定义Combiner实现步骤:

①自定义一个Combiner继承Reducer,重写Reduce方法;

②在驱动类中设置

job.setCombinerClass(WordCountCombiner.class);

其实一般情况,可以直接把自定义reducer的逻辑拿来用即可,也就是说。

job.setCombinerClass(WordCountReducer.class);

4.5.2分区

默认分区是根据key的hashCode对ReduceTask个数取模得到的。

hadoop流处理 hadoop流式计算_big data_14

分区与ReduceTask之间的关系总结

①如果ReduceTask>分区结果数,则会产生几个空的输出文件part-r-xxxxx文件;

②如果ReduceTask属于区间(1,分区结果数),则有一部分数据无处安放,会报异常;

③如果ReduceTask=1,不论分区结果数多少,最终只会产生一个结果文件part-r-00000;

④分区号必须从零开始,逐一累加,因为如果不是逐一累加,中间就有空白文件,浪费资源;

4.6ReduceTask

详情见这篇文章:ReduceTask

4.7OutputFormat

OutputFormat是一个抽象类

hadoop流处理 hadoop流式计算_源码_15

 有许多类继承了其,我们默认是TextOutputFormat,它又继承自FileOuputFormat。

hadoop流处理 hadoop流式计算_big data_16

我们发现FileOuputFormat中有一个抽象方法getRecordWriter,这个是核心 

hadoop流处理 hadoop流式计算_源码_17

TextOutputFormat通过该方法getRecordWriter来实现一行一行的输出到文件中

hadoop流处理 hadoop流式计算_hadoop_18

 4.7.1自定义OutputFormat步骤

由于一些场景需要输出数据到特地名字文件或者数据库、搜索引擎等等,所以需要自定义OutputFormat

①自定义一个类XxOutputFormat继承FileOutputFormat,重写getRecordWriter方法;

②由于getRecordWriter需要返回RecordWriter类型的对象,又需要自定义一个类XxRecordWrite继承RecordWrite,重写LogRecordWrite(主要用户初始化连接或者流创建)、write(主要业务逻辑)、close(关闭连接或者流)。

③在自定义的XxOutputFormat的getRecordWriter方法中直接new一个XxRecordWrite对象返回即可。

④最后在driver模块中记得设置一下:

//设置自定义的outputformat
job.setOutputFormatClass(XxOutputFormat.class);

4.8排序

排序操作是Hadoop的默认行为,MapTask和ReduceTask均会对数据按照key进行排序。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序

MapTask和ReduceTask中都涉及到排序,当MapTask中的环形缓冲区使用率达到一定阈值或者数据读完后,会对缓冲区中的数据进行一次快速排序并溢写到磁盘上,而当数据处理完毕后,也还对磁盘上的文件进行归并排序。对于ReduceTask则会统一对内存和磁盘上的所有数据进行一次归并排序

4.8.1部分排序

MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序

4.8.2全排序

最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。 但是这种方法慎用,尤其是生产环境下,数据量非常大,一台机器一个ReduceTask处理,完全违背了MR提供的并行计算架构思想,效率极低。

4.8.3辅助排序(GroupingComparator分组)

在Reduce端对key进行分组。应用于:在接收的key为自定义对象时,让对象中一个或几个字段相同(不包含全部字段相同)的key进入到同一个reduce方法时 ,可以采用分组排序

4.8.4二次排序

在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。比如一个自定义对象中含有时间和价格字段,这两都是排序条件,例如先按价格倒序,如果价格相同,在按时间倒序等等