MR运行原理
一、MapReduce概述
<1>概念
分布式计算框架,解决大数据计算问题
在集群上并发的计算运行分布式程序
分布式、海量数据的计算框架
单机版:资源有限(磁盘、内存、CPU)、存储能力、处理能力
分布式版:
多台计算机存储海量的数据 (HDFS思想存储大数据)
计算:
1. 并发计算
2. 汇总结果数据
3. 上面的两个阶段如果协调工作,如何启动运行,数据源如何查找... ...
4. 业务逻辑(统计每个单词的次数)
MapReduce: 分布式的运算框架,只需要关注业务逻辑的实现和处理
1.核心功能:将用户的编写的业务逻辑代码 和 框架自带的组件整合成一个完成分布式运算程序,
并发的运行在一个hadoop集群上。
2.阶段的划分:
Map(并发任务) + Reduce (汇总任务)
3.处理流程:
<2>特点
处理大数据(处理PB级别)
容灾容错 (把挂掉的机器上的计算任务,转移给其他节点)
速度慢 (离线数据分析)
扩展性强(通过添加新的节点,实现功能的扩展)
<3>底层的实现流程
一共两个阶段:map阶段+reduce阶段
map:
允许并发操作(例如:10个maptask并发执行)
按行读数据
将数据切割
存储(k-v)
将结果传递给reduce
reduce:
处理的数据从map阶段获取
允许并发操作(例如:10个reducetask并发执行)
按序分组
统计
输入结果
<4> MapReduce案例----wordCount
统计1000个文件中的所有的字母出现的次数
思路
编写MR工程的流程:
配置程序的运行信息 + 业务逻辑代码
1. 启动类(包含main方法),启动作业job
整个程序需要使用一个Driver来进行提交,提交的内容是:描述了各种必要参数配置的job对象
1.1 job在启动前,设置相关的必要的属性
例如:指定任务MapTask / ReduceTask ,指定操作数据源,结果数据源,设置reduce的个数,设置运行模式 config... ...
job.setMapperClass(xxxx.class);
job.setReducerClass(xxxx.class);
1.2 提交作业
2. 自定义Mapper类,继承Mapper类
2.1 指定Mapper的输入数据的kv形式(kv类型自定义,推荐使用hadoop提供的数据类型)
数据的传输( 要求数据序列化:使用java的序列化 / 使用hadoop提供的序列化 [轻量的、高效的] )
Mapper类中接受的数据形式:
第一行数据:hello world tom ,k-v, 发送给Mapper:key=0 ,value=hello world tom
第二行数据:tom hello nice ,k-v, 发送给Mapper:key=17 ,value=tom hello nice
... ... ... .... ..
2.2 指定Mapper的输出数据的kv形式
2.3 重写map方法 [ 一个MapTask进程 ] ,对每个输入的kv调用一次 (一行)
3. 自定义Reducer类,继承Reducer类
3.1 指定Reducer接受到的数据的kv形式(kv类型自定义,推荐使用hadoop提供的数据类型)
数据的传输( 要求数据序列化:使用java的序列化 / 使用hadoop提供的序列化 [轻量的、高效的] )
指定的kv类型是Mapper输出的kv类型
k----v迭代器
3.2 指定Reducer输出的数据的kv形式,持久化到本地的数据类型
3.3 重写reduce方法 [ 一个Reducer进程 ] , 对一组相同的key的kv组进行一次调用(一组)
4. 问题
运行:Output directory file:/e:/results already exists 错误
job作业的输出路径不能存在:Output directory
解决:
判断实参个数,大于1删除参数2
5. 部署运行
本地:配置hadoop路径
hadoop集群:
1. 将编写的工厂打包成jar包
2. 导入linux中
3. 使用hadoop jar 命令将当前的jar包提交到hadoop提群的yarn上运行
4. 输入和输出路径都在hadoop集群上
Map端
/**
* 定义输出和输出数据的类型:
*
*
* KEYIN, 输入的数据的类型(按行),行号 ,long
* VALUEIN, 输入的数据的value的类型 ,一行数据,字符串
* KEYOUT, 输出的数据的类型, 单词,字符串
* VALUEOUT , 输出的数据的value的类型,1,数字
*
*
*序列化问题:
* 数据格式、数据的传输效率、数据的大小
*
* java序列化:重量级的序列化,体积较大,传输速率低
*
* Hadoop提供了序列化机制:轻量级、高效的
* int ------- IntWritable
* String ------ Text
* long ------ LongWritable
*
*/
public class CountMap extends Mapper<LongWritable, Text, Text, IntWritable>{
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
//1. 获取当前行数据
String lineStr = value.toString();
System.out.println("line:"+lineStr);
//2. 切割
String[] words = lineStr.split(" ");
//3. 返回数据
for (String w : words) {
context.write(new Text(w), new IntWritable(1));
}
}
}
Reduce 端
public class CountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text text, Iterable<IntWritable> it,
Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int counts = 0;
//1. 统计每一个单词的次数
for (IntWritable count : it) {
counts += count.get();
}
//2. 返回结果
context.write(text, new IntWritable(counts));
}
}
client端
/**
* 程序的入口
*
*/
public class DriverClient {
public static void main(String[] args) throws Exception {
//1. 获取统计单词次数的任务
Configuration conf = new Configuration();
Job cwJob = Job.getInstance(conf);
//2. 设置jar加载的路径
cwJob.setJarByClass(DriverClient.class);
//3. 加入map和reduce
cwJob.setMapperClass(CountMap.class);
cwJob.setReducerClass(CountReduce.class);
//4. 设置输出和输入数据的类型
cwJob.setMapOutputKeyClass(Text.class);
cwJob.setMapOutputValueClass(IntWritable.class);
cwJob.setOutputKeyClass(Text.class);
cwJob.setMapOutputValueClass(IntWritable.class);
//5. 设置数据的来源和输出
//map可以接受和处理多个文件的数据
FileInputFormat.setInputPaths(cwJob, new Path(args[0]));
FileOutputFormat.setOutputPath(cwJob, new Path(args[1]));
//6. 提交任务,执行任务
cwJob.submit();
//cwJob.waitForCompletion(true);
}
}
二、MapReduce运行流程
1. 在MapReduce作业中的进程
1. MRAppMaster : 负责整个程序在执行过程中的调度和状态的协调(yarn节点上)
2. YarnChild : 负责map阶段的整个数据处理流程 ,maptask
3. YarnChild : 负责reduce阶段的整个数据处理流程 ,reducetask
2. mr程序执行流程
1. 启动mr程序,最先启动的进程是MRAppMaster , 它启动后根据本次job的描述信息,
计算出当前需要使用的maptask的实例个数,向集群申请启动响应数据量的maptask进程
2. maptask进程启动后,根据给定的数据切片 (具体是哪个文件中的哪块区域数据) 范围进行数据的处理
处理流程:
2.1 利用客户端指定的InputFormat来获取 RecordReader 需要读取的数据,形成kv格式
2.2 将输入的kv对传递给 客户端 指定的map类的map方法, map方法实现业务处理,将map方法处理的结果以kv对形式输出缓存中
2.3 将缓冲中的kv对按照 k 进行分区排序,不断写入磁盘文件
3. MRAppMaster监控到所有的maptask 任务进程执行完毕后,根据 客户端指定的参数 启动相对应数量的reducetask进程
MRAppMaster告知reducetask处理的数据的范围和位置
4. reducetask 进程启动后,根据MRAppMaster告知的待处理数据的位置,从maptask所运行机器上获取多个maptask的输出结果
重新对接受到数据进行合并(根据key)和排序,根据相同的key进行kv对的分组,调用用户重写的reduce方法 实现具体的业务
计算出处理结果,将处理结果返回给 客户端指定的OutFormat进行数据的持久化
MapReduce:job
maptask:任务
reducetask:任务
MrAppMaster 配置作业job配置参数,
maptask 并发度:文件切片 , 向集群申请maptask进程
FileInputFormat, computeSplitSize(){
max(min,min(max, blocksize)) //128MB //以文件为单位}
1 long最大值 128M
maptask进程:
1. job配置操作的数据的位置:Inputformat(FileInputFormat),RecoredReader读取切片数据,将数据以kv交给maptask
2. 接受数据,执行map方法,实现数据的操作
3. 将结果数据以kv形式写出(任务队列:map并发执行,map全部执行完毕后,reduce才执行)
将结果数据先存放到内存中(缓冲区)(排序,分组),空间有限
缓冲区达到80%,将数据持久化到磁盘(空间较大)临时目录
MrAppMaster监听到所有的maptask进程执行完毕后,启动reduce进程
1. reduce接受处理数据 (从临时目录中获取map存储数据)
2. 将结果数据先存放到内存中(缓冲区)(排序,分组),空间有限
缓冲区达到80%,将数据持久化到磁盘(空间较大)临时目录
3. 执行reduce方法,将结果返回 (job作业的配置参数中指定的输出位置)
二、MapReduce 并行度 决定机制
MapTask并行度 决定机制
并行度 map阶段的任务处理时的并发程度(影响job的处理速度)
一个job的map的并行度 由客户端在提交job时 已经确定了,FileInputFormat类中的getSplits()方法,对数据源数据进行分片
具体的个数:根据 待处理数据的 逻辑切片 (split) 来决定 ,每一个split都会分配一个maptask进程
ReduceTask的并发度决定机制
数据时由用户可以直接指定:默认值是1
job.setNumReduceTask(num)
如果数据分配不均匀,在reduce阶段会发生数据的倾斜,根据具体业务需求来进行设定
三、切片机制
FileInputFormat中使用切片方式:
默认采用 数据块大小 来切割(逻辑上),默认大小等于 block的大小(128m)
以 文件 为单位,针对每一个文件单独切片
切片大小:FileInputFormat中的computeSplitSize(blocksize,minsize,maxsize) 的方法决定
blocksize默认值:128M
例子:
file1.txt 250M
file2.txt 100M
调用getSplits()方法处理数据源,获取切片信息List集合:
f1-split1 0-128M
f1-split2 129-250M
f2-split1 0-100M
切片优化
1. 问题:
默认情况下,使用的TextInputFormat ,切片的大小是128M
protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
切片机制:以文件为单位,小文件之间不会合并
切片个数多,maptask进程多,处理数据少,效率低
2. 解决的思想:
在maptask处理数据前,在输入流读取数据前,将小文件进行合并
3. 实现:
使用Hadoop提供的API:CombineTextInputFormat ,不使用系统默认的 TextInputFormat
CombineTextInputFormat作用: 逻辑上 的 合并小文件
4. 案例:
有三个小文件:words.txt,words2.txt,words3.txt
默认情况下 : maptask / maptask / maptask
实现的效果: maptask
//设置切片的大小 //取消使用TextInputFormat
cwJob.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(cwJob, 2048);
CombineTextInputFormat.setMinInputSplitSize(cwJob, 1024);
四、数据的分区
1. 现象:分区
默认分区个数是reducetask的个数,默认的个数是1 (reduce默认的个数是 1)
//设置reduce的个数:
cwJob.setNumReduceTasks(3); // 创建多个分区
分区中的内容的存放规则:根据对key求hash%分区的个数
reduce从对应的一个分区中提取数据,执行后续操作
2. 问题:无法设定规则,不灵活
3. 需求:统计单词出现的次数,
首字母: 将a-g交给一个reduce处理,将h-n交给一个reduce处理,将o-z交给reduce处理
4. 实现:
通过Hadoop提供的API: Partitioner分区
控制数据存放的分区
5. 案例:
首字母: 将a-g交给一个reduce处理,将h-n交给一个reduce处理,将o-z交给reduce处理
6. 实现步骤:
1. 自定义类,继承Partitioner类,重写getPartition方法,此方法返回数据存放分区的具体位置
2. 在作业类中设置使用自定义分区类(指定分区算法)
//设置分区算法类
cwJob.setPartitionerClass(CountPartitioner.class);
//设置reduce的个数:
cwJob.setNumReduceTasks(3); //分区算法个reducetask的个数是密切相关的
3. 注意:
1<reduceTask的个数<Partitioner指定分区的个数 ,报错的
五、数据的排序
1. 现象:
在MapReduce中都会对数据的进行排序,默认操作,排序的规则:根据key的字典顺序
MapTask中:将处理结果存储到缓冲区中,在将数据移动到磁盘文件中之前对所有的数据进行排序
map中所有的数据处理完毕后,全部存储到磁盘的文件中后,对所有文件进行一次合并
2. 需求:按照总流量降序显示数据
88
1111111
10000000000
3. 解决:
自定义排序规则
操作自定义对象:实现序列化结构 / writable / writableComparable 接口
重写此writableComparable 接口的compareTo方法,指定排序规则
//自定义:使用一个字段设定排序规则
//自定义:使用多个字段设定排序规则
六、数据的合并
在每一个maptask将输出数据写入磁盘前,对数据进行合并(按照相同的key)
reducetask接受所有的maptask后合并操作(按照相同的key)
1. 需求:在reduce之前先进行合并操作
2. 自定义合并类:Hadoop提供的API , Combiner ,本质上是一个Reducer
3. 实现步骤:
1. 创建一个用户自定义类,继承 Reducer
2. 重写方法reduce,汇总
3. 输出结果数据
4. 在作业job中设置用户自定义的合并类
4. 执行时机
combiner先执行,reduce后执行
5. 案例:
减少reduce读取的操作数据的条数,减轻reduce压力
七、案例 单词统计
Map端
/**
* 定义输出和输出数据的类型:
*
*
* KEYIN, 输入的数据的类型(按行),行号 ,long
* VALUEIN, 输入的数据的value的类型 ,一行数据,字符串
* KEYOUT, 输出的数据的类型, 单词,字符串
* VALUEOUT , 输出的数据的value的类型,1,数字
*
*
*序列化问题:
* 数据格式、数据的传输效率、数据的大小
*
* java序列化:重量级的序列化,体积较大,传输速率低
*
* Hadoop提供了序列化机制:轻量级、高效的
* int ------- IntWritable
* String ------ Text
* long ------ LongWritable
*
*/
public class CountMap extends Mapper<LongWritable, Text, Text, IntWritable>{
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
//1. 获取当前行数据
String lineStr = value.toString();
System.out.println("line:"+lineStr);
//2. 切割
String[] words = lineStr.split(" ");
//3. 返回数据
for (String w : words) {
context.write(new Text(w), new IntWritable(1));
}
}
}
combine 端
public class CountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
Text t = new Text();
IntWritable iw =new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int n = 0;
//1.合并操作
for (IntWritable intWritable : values) {
n += intWritable.get();
}
t.set(key);iw.set(n);
//2.输出结果
context.write(t,iw);
}
}
partition 端
/**
* 设置分区规则
*/
public class CountPartitioner extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text key, IntWritable value, int numPartitions) {
//将a-g交给一个reduce处理,将h-n交给一个reduce处理,将o-z交给reduce处理
int index = 2;
//1. 获取key
String word = key.toString().toLowerCase();
//2. 获取可以首字母
char firstChar = word.charAt(0);
//0----48
//A----65
//a----97
//3. 判断当前单词存放的分区index
if(firstChar>=97&&firstChar<='g'){
index = 0;
}else if(firstChar>='h'&&firstChar<='n'){
index = 1;
}
System.out.println(word+"--->"+firstChar+"----->index:"+index);
return index;
}
}
Reduce 端
public class CountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text text, Iterable<IntWritable> it,
Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int counts = 0;
//1. 统计每一个单词的次数
for (IntWritable count : it) {
counts += count.get();
}
//2. 返回结果
context.write(text, new IntWritable(counts));
}
}
client 端
/**
* 程序的入口
*
*/
public class DriverClient {
/**
*
* e:/test e:/results
*
*
* args 接受,程序在运行传递的实参
* args[0] = e:/test
* args[1] = e:/results (不能存在的)
*/
public static void main(String[] args) throws Exception {
//1. 获取统计单词次数的任务
Configuration conf = new Configuration();
//输出路径的判断
if(args.length>1){
//删除指定的输出路径
FileSystem.get(conf).delete(new Path(args[1]));
}
Job cwJob = Job.getInstance(conf);
//2. 设置jar加载的路径
cwJob.setJarByClass(DriverClient.class);
//3. 加入map和reduce
cwJob.setMapperClass(CountMap.class);
cwJob.setReducerClass(CountReduce.class);
//4. 设置输出和输入数据的类型
cwJob.setMapOutputKeyClass(Text.class);
cwJob.setMapOutputValueClass(IntWritable.class);
cwJob.setOutputKeyClass(Text.class);
cwJob.setMapOutputValueClass(IntWritable.class);
//5. 设置数据的来源和输出
//map可以接受和处理多个文件的数据
FileInputFormat.setInputPaths(cwJob, new Path(args[0]));
FileOutputFormat.setOutputPath(cwJob, new Path(args[1]));
//设置切片的大小 //取消使用TextInputFormat
cwJob.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(cwJob, 2048);
CombineTextInputFormat.setMinInputSplitSize(cwJob, 1024);
//23 + 1000 + 2000
//23+1000=1023+1 = 1024 / 1999
//设置分区算法类
cwJob.setPartitionerClass(CountPartitioner.class);
//设置reduce的个数:
cwJob.setNumReduceTasks(4);
//设置合并操作
cwJob.setCombinerClass(CountCombiner.class);
//6. 提交任务,执行任务
// cwJob.submit();
cwJob.waitForCompletion(true);
}
}