摘要:文章仅供学习交流,也是自己对学习知识的一种梳理;
学习分三步:为什么用,怎么用,最后就是拔高,擅于使用,也就是所谓的精通!
MAPREDUCE原理篇(1)
Mapreduce 是一个分布式运算程序的编程框架,使用户开发“基于hadoop的数据分析应用”的核心框架;
Mapreduce 核心功能是将用户编写的业务逻辑代码和自带的默认组件整合成一个完整的分布式运算程序,并运行在一个hadoop集群上;
-
为什么用 Mapreduce
- 海量数据在单机上处理,应为硬件资源限制(如内存,CPU),无法胜任。
- 一旦单机版程序扩展到集群来分布式运行,将极大增加程序的复杂度和开发难度。
- 引入Mapreduce框架后,开发人员可以将绝大部分工作集中在业务逻辑的开发,而将分布式计算的复杂性交由框架处理;
单机版:内存受限,磁盘受限,运算能力受限 分布式:
|
可见在程序由单机版扩成分布式时,会引入大量的复杂工作。为了提高开发效率,可以将分布式程序中的公共功能封装成框架,让开发人员可以将精力集中于业务逻辑。
而mapreduce就是这样一个分布式程序的通用框架,其应对以上问题的整体结构如下:
|
-
MapReduce 运行流程
- 一个MR程序启动的时候,最先启动的MRAppMaster,MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask数量,然后向集群申请机器启动相应的maptask进程;
- maptask进程启动之后,根据给定的数据切片范围进行数据处理,主体流程为:
- 利用客户指定的inputformat来获取RecordReader读取数据,形成输入KV对
- 将输入KV对传递给客户定义的map()方法,做逻辑运算,并将map()方法输出的KV对收集到缓存
- 将缓存中的KV对按照K分区排序后不断溢写到磁盘文件
- MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据范围(数据分区)
- Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算,并收集运算输出的结果KV,然后调用客户指定的outputformat将结果数据输出到外部存储
-
MapTask 并行度决定机制
maptask 的并行度决定map 阶段的任务并行度,进而影响到整个job的处理速度。
一个job 的 map 阶段并行度由客户端在提交 job 时决定
基本逻辑:将待处理数据执行逻辑切片(即按照一个特定的切片大小,将待处理数据划分为逻辑上的多个 split ),然后每一个split分配一个maptask并行实力处理。
-
FileInputFormat 切片机制
切片定义在FileInputFormat 类中的getSplit() 方法中,有兴趣可以研究一下
默认的切片机制:
- 简单的按照文件内容长度进行切片
- 切片大小,默认等于block大小
- 切片时,不考虑数据集整体,而是正对每一个文件单独切片
通过分析源码,在FileInputFormat中,计算切片大小的逻辑:
Math.max(minSize, Math.min(maxSize, blockSize)); 切片主要由这几个值来运算决定
minsize:默认值:1 配置参数: mapreduce.input.fileinputformat.split.minsize |
maxsize:默认值:Long.MAXValue 配置参数:mapreduce.input.fileinputformat.split.maxsize |
blocksize |
因此,默认情况下,切片大小=blocksize
maxsize(切片最大值):
参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值
minsize (切片最小值):
参数调的比blockSize大,则可以让切片变得比blocksize还大
但是,不论怎么调参数,都不能让多个小文件“划入”一个split
-
map并行度的经验之谈
如果硬件配置为2*12core + 64G,恰当的map并行度是大约每个节点20-100个map,最好每个map的执行时间至少一分钟。
如果job的每个map或者 reduce task的运行时间都只有30-40秒钟,那么就减少该job的map或者reduce数,每一个task(map|reduce)的setup和加入到调度器中进行调度,这个中间的过程可能都要花费几秒钟,所以如果每个task都非常快就跑完了,就会在task的开始和结束的时候浪费太多的时间。
配置task的JVM重用[dht1] 可以改善该问题:
(mapred.job.reuse.jvm.num.tasks,默认是1,表示一个JVM上最多可以顺序执行的task数目(属于同一个Job)是1。也就是说一个task启一个JVM)
如果input的文件非常的大,比如1TB,可以考虑将hdfs上的每个block size设大,比如设成256MB或者512MB
JVM重用技术不是指同一Job的两个或两个以上的task可以同时运行于同一JVM上,而是排队按顺序执行。
-
ReduceTask并行度的决定
reducetask的并行度同样影响整个job的执行并发度和执行效率,但与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:
job.setNumReduceTasks(4);
如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜
注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask
尽量不要运行太多的reduce task。对大多数job来说,最好rduce的个数最多和集群中的reduce持平,或者比集群的 reduce slots小。这个对于小集群而言,尤其重要。
代码示例
package com.mapreduce;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/**
*
*
* Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>
* KEYIN:是指框架读取到的数据的key类型
* 在默认的读取数据组件InputFormat下,读取的key是一行文本的偏移量,所以key的类型是long类型的
*
* VALUEIN指框架读取到的数据的value类型
* 在默认的读取数据组件InputFormat下,读到的value就是一行文本的内容,所以value的类型是String类型的
*
* keyout是指用户自定义逻辑方法返回的数据中key的类型 这个是由用户业务逻辑决定的。
* 在我们的单词统计当中,我们输出的是单词作为key,所以类型是String
*
* VALUEOUT是指用户自定义逻辑方法返回的数据中value的类型 这个是由用户业务逻辑决定的。
* 在我们的单词统计当中,我们输出的是单词数量作为value,所以类型是Integer
*
* 但是,String ,Long都是jdk中自带的数据类型,在序列化的时候,效率比较低。hadoop为了提高序列化的效率,他就自己自定义了一套数据结构。
*
* 所以说在我们的hadoop程序中,如果该数据需要进行序列化(写磁盘,或者网络传输),就一定要用实现了hadoop序列化框架的数据类型
*
*
* Long------->LongWritable
* String----->Text
* Integer---->IntWritable
* null------->nullWritable
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
/**
* 这个map方法就是mapreduce程序中被主体程序MapTask所调用的用户业务逻辑方法
*
* Maptask会驱动我们的读取数据组件InputFormat去读取数据(KEYIN,VALUEIN),每读取一个(k,v),他就会传入到这个用户写的map方法中去调用一次
*
* 在默认的inputFormat实现中,此处的key就是一行的起始偏移量,value就是一行的内容
*/
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//获取每一行的文本内容
String lines = value.toString();
String[] words = lines.split(" ");
for (String word :words) {
context.write(new Text(word), new IntWritable(1));
}
}
}
package com.mapreduce;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
/***
*
*
* reducetask在调用我们的reduce方法
*
* reducetask应该接收到map阶段(前一阶段)中所有maptask输出的数据中的一部分;
* (key.hashcode% numReduceTask==本ReduceTask编号)
*
* reducetask将接收到的kv数据拿来处理时,是这样调用我们的reduce方法的:
*
* 先讲自己接收到的所有的kv对按照k分组(根据k是否相同)
*
* 然后将一组kv中的k传给我们的reduce方法的key变量,把这一组kv中的所有的v用一个迭代器传给reduce方法的变量values
*
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
int count =0;
for(IntWritable v :values){
count += v.get();
}
context.write(key, new IntWritable(count));
}
}
package com.mapreduce;
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.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
/**
*
*
* 本类是客户端用来指定wordcount job程序运行时候所需要的很多参数
*
* 比如:指定哪个类作为map阶段的业务逻辑类 哪个类作为reduce阶段的业务逻辑类
* 指定用哪个组件作为数据的读取组件 数据结果输出组件
* 指定这个wordcount jar包所在的路径
*
* ....
* 以及其他各种所需要的参数
*/
public class WordCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://hadoop01:9000");
// conf.set("mapreduce.framework.name", "yarn");
// conf.set("yarn.resourcemanager.hostname", "mini1");
Job job = Job.getInstance(conf);
//$JAVA_HOMEbin -cp hdfs-2.3.4.jar:mapreduce-2.0.6.4.jar;
//告诉框架,我们的的程序所在jar包的位置
// job.setJar("/root/wordcount.jar");
job.setJarByClass(WordCountDriver.class);
//告诉程序,我们的程序所用的mapper类和reducer类是什么
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//告诉框架,我们程序输出的数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//告诉框架,我们程序使用的数据读取组件 结果输出所用的组件是什么
//TextInputFormat是mapreduce程序中内置的一种读取数据组件 准确的说 叫做 读取文本文件的输入组件
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
//告诉框架,我们要处理的数据文件在那个路劲下
FileInputFormat.setInputPaths(job, new Path("d://wordcount/input"));
//告诉框架,我们的处理结果要输出到什么地方
FileOutputFormat.setOutputPath(job, new Path("d://wordcount/output"));
boolean res = job.waitForCompletion(true);
System.exit(res?0:1);
}
}