MapReduce介绍
(一)MapReduce简介
MapReduce是一种分布式计算模型,是Google提出的,主要用于搜索领域,解决海量数据的计算问题。
MR有两个阶段组成:Map和Reduce,用户只需实现map()和reduce()两个函数,即可实现分布式计算。
(二)MapReduce有哪些角色?各自的作用是什么?
MapReduce由JobTracker和TaskTracker组成。
JobTracker负责资源管理和作业控制,TaskTracker负责任务的运行。
(三)MapReduce程序执行流程
程序执行流程图如下:
(1) 开发人员编写好MapReduce program,将程序打包运行。
(2) JobClient向JobTracker申请可用Job,JobTracker返回JobClient一个可用Job ID。
(3) JobClient得到Job ID后,将运行Job所需要的资源拷贝到共享文件系统HDFS中。
(4) 资源准备完备后,JobClient向JobTracker提交Job。
(5) JobTracker收到提交的Job后,初始化Job。
(6) 初始化完成后,JobTracker从HDFS中获取输入splits(作业可以该启动多少Mapper任务)。
(7) 与此同时,TaskTracker不断地向JobTracker汇报心跳信息,并且返回要执行的任务。
(8) TaskTracker得到JobTracker分配(尽量满足数据本地化)的任务后,向HDFS获取Job资源(若数据是本地的,不需拷贝数据)。
(9) 获取资源后,TaskTracker会开启JVM子进程运行任务。
注:
(3)中资源具体指什么?主要包含:
● 程序jar包、作业配置文件xml
● 输入划分信息,决定作业该启动多少个map任务
● 本地文件,包含依赖的第三方jar包(-libjars)、依赖的归档文件(-archives)和普通文件(-files),如果已经上传,则不需上传
(四)MapReduce工作原理
工作原理图如下:
map task
程序会根据InputFormat将输入文件分割成splits,每个split会作为一个map task的输入,每个map task会有一个内存缓冲区,输入数据经过map阶段处理后的中间结果会写入内存缓冲区,并且决定数据写入到哪个partitioner,当写入的数据到达内存缓冲区的的阀值(默认是0.8),会启动一个线程将内存中的数据溢写入磁盘,同时不影响map中间结果继续写入缓冲区。在溢写过程中,MapReduce框架会对key进行排序,如果中间结果比较大,会形成多个溢写文件,最后的缓冲区数据也会全部溢写入磁盘形成一个溢写文件(最少有一个溢写文件),如果是多个溢写文件,则最后合并所有的溢写文件为一个文件。
reduce task
当所有的map task完成后,每个map task会形成一个最终文件,并且该文件按区划分。reduce任务启动之前,一个map task完成后,就会启动线程来拉取map结果数据到相应的reduce task,不断地合并数据,为reduce的数据输入做准备,当所有的map tesk完成后,数据也拉取合并完毕后,reduce task 启动,最终将输出输出结果存入HDFS上。
(五) MapReduce的执行步骤:
1、Map任务处理
1.1 读取HDFS中的文件。每一行解析成一个<k,v>。每一个键值对调用一次map函数。 <0,hello you> <10,hello me>
1.2 覆盖map(),接收1.1产生的<k,v>,进行处理,转换为新的<k,v>输出。 <hello,1> <you,1> <hello,1> <me,1>
1.3 对1.2输出的<k,v>进行分区。默认分为一个区。详见《Partitioner》
1.4 对不同分区中的数据进行排序(按照k)、分组。分组指的是相同key的value放到一个集合中。 排序后:<hello,1> <hello,1> <me,1> <you,1> 分组后:<hello,{1,1}><me,{1}><you,{1}>
1.5 (可选)对分组后的数据进行归约。详见《Combiner》
2、Reduce任务处理
2.1 多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点上。(shuffle)详见《shuffle过程分析》
2.2 对多个map的输出进行合并、排序。覆盖reduce函数,接收的是分组后的数据,实现自己的业务逻辑, <hello,2> <me,1> <you,1>
处理后,产生新的<k,v>输出。
2.3 对reduce输出的<k,v>写到HDFS中。
(六)针对MapReduce的缺点,YARN解决了什么?
MapReduce由以下缺点:
★ JobTracker挂掉,整个作业挂掉,存在单点故障
★ JobTracker既负责资源管理又负责作业控制,当作业增多时,JobTracker内存是扩展的瓶颈
★ map task全部完成后才能执行reduce task,造成资源空闲浪费
YARN设计考虑以上缺点,对MapReduce重新设计:
★ 将JobTracker职责分离,ResouceManager全局资源管理,ApplicationMaster管理作业的调度
★ 对ResouceManager做了HA设计
★ 设计了更细粒度的抽象资源容器Container
(七)Java代码实现
1)MapReduce编程主要组件
InputFormat类:分割成多个splits和每行怎么解析。
Mapper类:对输入的每对<key,value>生成中间结果。
Combiner类:在map端,对相同的key进行合并。
Partitioner类:在shuffle过程中,将按照key值将中间结果分为R份,每一份都由一个reduce去完成。
Reducer类:对所有的map中间结果,进行合并。
OutputFormat类:负责输出结果格式。
编程框架如下:
Mapper------map
public static class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable>{
map方法
}
Reducer-----reduce
public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
reduce的方法
}
main-----主方法入口
{
组装map和reduce 并进行运行提交
}
单词统计实例:
import java.io.IOException;
import java.io.Serializable;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
/**
* 单词统计
*/
import org.apache.hadoop.mapreduce.Mapper;
/**
* 输入和输出 map类型
* KEYIN, 输入的键的类型 这里指的是每一行的起始偏移量 long 0 12
* VALUEIN,输入的value的类型 这里指的是每一行的内容 和偏移量一一对应的 String
* 输出的类型取决于 业务
* KEYOUT, 输出的键的类型 这里指的每一个单词 --- string
* VALUEOUT,输出的值的类型 这里指的单词的次数 ---- int
* @author lv_hulk
*
*
* 这里的数据类型 不能使用java的原生类型
* 序列化:数据持久化存储 或 网络传输的时候 数据需要序列化和反序列化的
* 张三---序列化------010101110-----反序列化-----张三
* java-----Serializable
* mapreduce编程中的用于传输的数据类型必须是序列化和反序列化能力的
* hadoop弃用了java中原生的Serializable 实现的自己的一套序列化和反序列化的 接口Writable 只会对数据的值进行序列化和反序列化
* 原因:java中的序列化和反序列化太重 繁琐
* Long 1
* 对于一些常用的数据类型 hadoop帮我们实现好了:
* int------intWritable
* long----LongWritable
* string-----Text
* byte------ByteWritable
* double----DoubleWritable
* float-----FloatWritable
* boolean-----BooleanWritable
* null-----NullWritable
*自己定义的需要序列化和反序列化 实现 Writable接口
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
//重写map函数
/**
* 参数:
* hadoop的底层数据读取的时候 字节读取的
* LongWritable key:输入的key 这里指的是每一行的偏移量 没有实际作用 一行的标识而已
* Text value:输入的value 这里指的是一行的内容
* Context context:上下文对象 用于传输 传输reduce中
* 函数的调用频率:
* 一行调用一次
* 如果一个文件 10行-----map函数会被调用10次
*/
@Override
protected void map(LongWritable key,
Text value,
Context context)
throws IOException, InterruptedException {
//创建一个流 进行读取(mapreduce框架帮你做了) 每一行内容进行切分
//获取每一行内容 进行切分
//text--toString()--String
String line = value.toString();
//进行切分 hello word hello ww
String[] words = line.split("\t");
//循环遍历每一个单词 进行统计 直接发送到reduce端 发送的时候 k-v
for(String w:words){
//将String---Text
Text mk=new Text(w);
//int----IntWritable
IntWritable mv=new IntWritable(1);
//hello,1 world,1 hello,1 ww,1
//这里write是直接写出 调用一次 就会写出一个 k---v写出reduce端
context.write(mk, mv);
}
}
}
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
/**
* 单词统计
*/
import org.apache.hadoop.mapreduce.Reducer;
/**
* reduce的数据来源于map
* @author lv_hulk
*KEYIN, 输入的key的类型 这里指的是map输出key类型 Text
* VALUEIN, 输入的value的类型 这里指的是map输出的value的类型 IntWritable
*
* 输出
*KEYOUT, 输出的key 这里指的单词的类型 Text
*VALUEOUT,输出的value的类型 这里指的是单词的总次数 IntWritable
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
//重写 reduce方法
/**
* 到reduce端的数据 是已经分好组的数据
* 默认情况下 按照map输出的key进行分组 将map输出的key相同的分为一组
* Text key, 每一组中的相同的key
* Iterable<IntWritable> values, 每一组中的所有的value值 封装到一个迭代器中了
Context context:上下文对象 用于传输的 写出到hdfs中
reduce调用频率:
一组调用一次 每次只统计一个单词最终结果
每一组只能访问本组的数据 没有办法和上一组的额数据 下一组的数据共享的
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//循环遍历values 求和
int sum=0;
for(IntWritable v:values){
//intwritable---int 数值类型 get 将hadoop中的类型转换为java中的类型
sum+=v.get();
}
//写出结果文件
IntWritable rv=new IntWritable(sum);
context.write(key, rv);
}
}
import java.io.IOException;
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;
/**
* 驱动类
* @author lv_hulk
*
*/
public class Driver {
public static void main(String[] args) {
//将mapper reducer类进行一个封装 封装为一个任务----job(作业)
//加载配置文件
Configuration conf=new Configuration();
//启动一个Job 创建一个job对象
try {
Job job=Job.getInstance(conf);
//设置这个job
//设置整个job的主函数入口
job.setJarByClass(Driver.class);
//设置job的mappper的类
job.setMapperClass(WordCountMapper.class);
//设置job的reducer的类
job.setReducerClass(WordCountReducer.class);
//设置map输出key value的类型
//指定了泛型 这里为什么还要设置一次 泛型的作用范围 编译的时候生效 运行的时候泛型会自动擦除
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置reduce的输出的k v类型 以下方法设置的是mr的最终输出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设定切片大小 小于128M
//设置 mapreduce.input.fileinputformat.split.maxsize 最大的切片的大小
FileInputFormat.setMaxInputSplitSize(job, 1*1024);
//设置 mapreduce.input.fileinputformat.split.minSize
//FileInputFormat.setMinInputSplitSize(job, size);
//指定需要统计的文件的输入路径 FileInputFormat 文件输入类
Path inpath=new Path(args[0]);
FileInputFormat.addInputPath(job, inpath);
//指定输出目录 输出路径不能存在的 否则会报错 默认输出是覆盖式的输出 如果输出目录存在 有可能造成原始数据的丢失
Path outpath=new Path(args[1]);
FileOutputFormat.setOutputPath(job, outpath);
//提交job 执行这一句的时候 job才会提交 上面做的一系列的工作 都是设置job
//job.submit();
job.waitForCompletion(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}