MAPREDUCE原理篇
Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架;
Mapreduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个hadoop集群上;
为什么要MAPREDUCE
- 海量数据在单机上处理因为硬件资源限制,无法胜任
- 而一旦将单机版程序扩展到集群来分布式运行,将极大增加程序的复杂度和开发难度
- 引入mapreduce框架后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理
设想一个海量数据场景下的wordcount需求:
- 单机版:内存受限,磁盘受限,运算能力受限
- 分布式:
1、文件分布式存储(HDFS)
2、运算逻辑需要至少分成2个阶段(一个阶段独立并发,一个阶段汇聚)
3、运算程序如何分发
4、程序如何分配运算任务(切片)
5、两阶段的程序如何启动?如何协调?
整个程序运行过程中的监控?容错?重试?
可见在程序由单机版扩成分布式时,会引入大量的复杂工作。为了提高开发效率,可以将分布式程序中的公共功能封装成框架,让开发人员可以将精力集中于业务逻辑。
而mapreduce就是这样一个分布式程序的通用框架,其应对以上问题的整体结构如下:
- MRAppMaster(mapreduce application master)
- MapTask
- ReduceTask
MAPREDUCE框架结构及核心运行机制
结构
一个完整的mapreduce程序在分布式运行时有三类实例进程:
- MRAppMaster:负责整个程序的过程调度及状态协调
- mapTask:负责map阶段的整个数据处理流程
- ReduceTask:负责reduce阶段的整个数据处理流程
MR程序运行流程
流程解析
-
一个mr程序启动的时候,最先启动的是MRAppMaster,MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程
-
maptask进程启动之后,根据给定的数据切片范围进行数据处理,主体流程为:
a)利用客户指定的inputformat来获取RecordReader读取数据,形成输入KV对
b)将输入KV对传递给客户定义的map()方法,做逻辑运算,并将map()方法输出的KV对收集到缓存
c)将缓存中的KV对按照K分区排序后不断溢写到磁盘文件 -
MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据范围(数据分区)
-
Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算,并收集运算输出的结果KV,然后调用客户指定的outputformat将结果数据输出到外部存储
MapTask并行度决定机制
maptask的并行度决定map阶段的任务处理并发度,进而影响到整个job的处理速度。那么,mapTask并行实例是否越多越好呢?其并行度又是如何决定呢?
mapTask并行度的决定机制
一个job的map阶段并行度由客户端在提交job时决定
而客户端对map阶段并行度的规划的基本逻辑为:
将待处理数据执行逻辑切片(即按照一个特定切片大小,将待处理数据划分成逻辑上的多个split),然后每一个split分配一个mapTask并行实例处理
block为物理切片,客户端map切片设置为逻辑切片,任务切片是一个逻辑划分,一个切片对应一个maptask
这段逻辑及形成的切片规划描述文件,由FileInputFormat实现类的getSplits()方法完成,其过程如下图:
- 查看数据在哪个目录
/wordcount/srcdata/SomeData.dat
- 开始处理(规划切片)目录下的每一个文件
- 遍历到第一个文件
SomeData.dat
- 获取文件大小,
fs.sizeOf(SomeData.dat)
- 计算切片大小
computeSplitSize(Math.max(minSize,Math.max(maxSize,blockSize))) = blockSize = 128M
- 开始切,形成第一个切片:
SomeData.dat 0-128M
,第二个切片SomeData.dat 128-256M
,第三个切片SomeData.dat 256-300M
- 将切片信息写入写入到切片规则文件
- 获取文件大小,
FileInputFormat切片机制
- 切片定义在InputFormat类中的getSplit()方法
- FileInputFormat中默认的切片机制:
a) 简单地按照文件的内容长度进行切片
b) 切片大小,默认等于block大小
c) 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
比如待处理数据有两个文件:
file1.txt 320M
file2.txt 10M
经过FileInputFormat的切片机制运算后,形成的切片信息如下:
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
file2.txt.split1-- 0~10M
- FileInputFormat中切片的大小的参数配置
通过分析源码,在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还大
选择并发数的影响因素:
1、运算节点的硬件配置
2、运算任务的类型:CPU密集型还是IO密集型
3、运算任务的数据量
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重用可以改善该问题:
(mapred.job.reuse.jvm.num.tasks,默认是1,表示一个JVM上最多可以顺序执行的task
数目(属于同一个Job)是1。也就是说一个task启一个JVM)
- 如果input的文件非常的大,比如1TB,可以考虑将hdfs上的每个block size设大,比如设成256MB或者512MB
ReduceTask并行度的决定
reducetask的并行度同样影响整个job的执行并发度和执行效率,但与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:
//默认值是1,手动设置为4
job.setNumReduceTasks(4);
如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜
- 注意: reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask
尽量不要运行太多的reduce task。对大多数job来说,最好rduce的个数最多和集群中的reduce持平,或者比集群的 reduce slots小。这个对于小集群而言,尤其重要。
客户端提交Job流程(简化)
- 客户端job调用job.waitForCompletion(),集群内部会调用submit()方法
- JobSubmit类中的成员变量cluster记录当前提交的集群状态(yarn或本地模拟器)
- 根据stagingDir与jobId生成配置文件存储路径hdfs://…/.staging/jobId、file://…/.staging/jobId
- 生成切片文件job.split
- 生成job配置文件job.xml
- 获取所要执行的Jar包
- 把以上文件上传到集群执行
MAPREDUCE实践篇
MAPREDUCE 示例编写及编程规范
编程规范
- 用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper的输出数据是KV对的形式(KV的类型可自定义)
- Mapper中的业务逻辑写在map()方法中
- map()方法(maptask进程)对每一个<K,V>调用一次
- Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法
- 用户自定义的Mapper和Reducer都要继承各自的父类
- 整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象
WordCount处理流程
- 读数据
- 按行处理
- 按空格切分行内单词
- HashMap(单词,value+1)
- 等自己的数据片全部读取完之后,将hashMap按首字母范围分成3个(设置的reduce个数)hashMap
- 将3个hashmap发送给3个reduce程序
WordCount程序示例
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
Map程序
/**
* WordCountMapper程序由MapReduce框架的map task(多个分布在不同的机器)调用
*
* KEYIN:默认情况,mr框架所读到的一行文本的起始偏移量,Long,
* 但是在hadoop有自己更精简的序列化接口(Java自己的序列化接口对MR来说传递的冗余数据太多),
* 所以不用直接使用Long,而用LongWritable
* VALUEIN:默认情况,mr框架所读到的一行文本的内容,String,同上用Text
*
* KEYOUT:用户自定义逻辑处理完成之后输出数据中的Key,此处是单词,String,同上用Text
* VALUEOUT:用户自定义逻辑处理完成之后输出数据中的value,在此处是单词次数,Integer,同上用IntWritable
*
* 网络传输需要序列化
* @author lxf
* @version v1.0
* @date 2018/3/30 8:29
*/
public class WordCountMapper extends Mapper<LongWritable,Text,Text,IntWritable> {
/**
* map阶段的业务逻辑就写在自定义的map()方法中
* mapTask会对每一行输入数据调用一次我们自定义的map方法
* @param key 每一行单词的起始偏移量
* @param value 每一行的值
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//将mapTask传的文本转换为String
String line = value.toString();
//根据空格将这一行切分成单词
String[] words = line.split(" ");
//将单词输出为<单词,1>
for (String word : words) {
//将单词作为key,将次数1作为value,以便于后续的数据分发,可以根据单词分发,以便于相同单词会到相同的reduce task
context.write(new Text(word),new IntWritable(1));
}
}
}
Reduce程序
/**
* keyIn,valueIn对应mapper输出的keyOut,valueOut类型对应
* keyOut,valueOut是自定义reduce逻辑处理结果的输出数据类型
*
* keyOut单词
* valueOut是总次数
*
* 输出文件一般指定到hdfs上易于查看
* @author lxf
* @version v1.0
* @date 2018/3/30 9:14
*/
public class WordCountReducer extends Reducer<Text,IntWritable,Text,IntWritable> {
/**
*
* <hello,1><hello,1><hello,1><hello,1><hello,1><hello,1>
* <banana,1><banana,1><banana,1><banana,1><banana,1><banana,1>
* @param key 输入参数key,是一组相同单词KV的key(传入的是一组相同单词的第一个(key,value)对的第一个key)
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int count = 0;
for (IntWritable value : values) {
count += value.get();
}
context.write(key,new IntWritable(count));
}
}
Driver程序,若要在集群上运行,因本程序不依赖第三方Jar,可以单独把程序打为jar包而不用包含其依赖的Jar,直接发送到集群上执行(集群中包含运行所依赖的Jar)
/**
* 相当于yarn集群的客户端
* 需要在此封装mr程序的相关运行参数,指定jar包
* 最后提交给yarn
* @author lxf
* @version v1.0
* @date 2018/3/30 11:26
*/
public class WordCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
/*
运行集群模式,就是把程序提交到yarn上
要想运行为集群模式,以下3个参数要指定集群上的值
1. 若在window运行则需要配置yarn
conf.set("mapreduce.framework.name","yarn");
conf.set("yarn.resourcemanager.hostname","master");
conf.set("fs.defaultFS","hdfs://master:9000");
2. linux上有yarn配置信息,不需要配
*/
Job job = Job.getInstance(conf);
//指定本程序的jar包所在的本地路径
job.setJarByClass(WordCountDriver.class);
//指定本业务job要使用mapper/Reducer业务类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//指定mapper输出数据kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//指定最终输出数据kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//指定job的输入原始文件所在目录
FileInputFormat.setInputPaths(job,new Path(args[0]));
//指定job的输出结果所在目录
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//将job中配置的相关参数,以及job所用的java类所在的jar包,提交给yarn去运行
/*job.submit();*/
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
上传到集群执行
java -cp wordcount.jar com.lxf.bigdata.mr.wcdemo.WordCountDriver /wordcount/input /wordcount/output
但时这时会提示Exception,原因是程序运行所依赖的Jar包并没有自动加入到classpath中,所以应该使用如下
hadoop jar wordcount.jar com.lxf.bigdata.mr.wcdemo.WordCountDriver /wordcount/input /wordcount/output
把hadoop安装目录中的所有Jar添加到classpath
wordcount流程示意图
- 客户端获取待处理数据信息,根据参数配置信息形成一个任务分配规划
- 将job.split、wc.jar、job.xml提交给ResourceManager,启用MR appMaster
- 启动maptask加载要计算的数据,调用自定义的mapper方法进行map计算,并将计算后的结果分类
- 将对应分类的数据传递给reduce task进行reduce计算
- 最终将计算后的结果输出到指定的输出目录
MAPREDUCE程序运行模式
本地运行模式
- mapreduce程序是被提交给LocalJobRunner在本地以单进程的形式运行
- 而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上
- 怎样实现本地运行?写一个程序,不要带集群的配置文件(本质是你的mr程序的conf中是否有
mapreduce.framework.name=local
以及yarn.resourcemanager.hostname
参数) - 本地模式非常便于进行业务逻辑的debug,只要在eclipse中打断点即可
如果在windows下想运行本地模式来测试程序逻辑,需要在windows中配置环境变量:
%HADOOP_HOME% = d:/hadoop-2.6.1
%PATH% = %HADOOP_HOME%\bin
并且要将d:/hadoop-2.6.1的lib和bin目录替换成windows平台编译的版本
public class WordCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
/********************************/
// 是否使用本地模式查看以下参数是否为“local” 不配置默认使用本地模式
conf.set("mapreduce.framework.name","local");
//本地文件系统
conf.set("fs.defaultFS","file:///");
/****以上不配置,默认使用本地
********************************/
// 本地模式运行mr程序时,输入输出数据既可以时本地,也可以在hdfs上
// 到底在哪里,就看以下两行配置是哪里生效
// 即可以指定为本地文件系统,也可以指定为hadoop集群
/*conf.set("fs.defaultFS","hdfs://master:9000");*/
Job job = Job.getInstance(conf);
//指定本程序的jar包所在的本地路径(通过Hadoop jar可以加载相关的配置到classpath)
job.setJarByClass(WordCountDriver.class);
//job.setXXX();
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
集群运行模式
- 将
mapreduce
程序提交给yarn
集群resourcemanager
,分发到很多的节点上并发执行 - 处理的数据和输出结果应该位于hdfs文件系统
- 提交集群的实现步骤:
方法一、将程序打成JAR
包,然后在集群的任意一个节点上用hadoop
命令启动$ hadoop jar wordcount.jar cn.itcast.bigdata.mrsimple.WordCountDriver inputpath outputpath
方法二、直接在linux的eclipse中运行main方法;
1.打包成jar
,
2job.setJarByClass(jar包绝对路径);
3.项目中main
方法要带参数:mapreduce.framework.name=yarn
以及yarn
的两个基本配置或添加配置文件
方法三、如果要在windows的eclipse中提交job给集群;
1.打包成jar
2.job.setJarByClass(jar包绝对路径);
3.项目中main
方法要带参数:mapreduce.framework.name=yarn
以及yarn
的两个基本配置或添加配置文件
4修改YarnRunner
类,自定义包与原始YarnRunner
包名相同https://gitee.com/SoFeelLove/hadoop_itcast/tree/master/src/main/java/org/apache/hadoop/mapred/YarnRunner.java
/*
运行集群模式,就是把程序提交到yarn上
要想运行为集群模式,以下3个参数要指定集群上的值
linux上有yarn配置信息,不需要配,使用hadoop jar 命令是hadoop会自动加载所需要的配置信息到classpath中(以下3个配置都包含)
若在window运行则需要配置yarn
*/
conf.set("mapreduce.framework.name","yarn");
conf.set("yarn.resourcemanager.hostname","master");
conf.set("fs.defaultFS","hdfs://master:9000");
//指定本程序的jar包所在的本地路径(通过Hadoop jar可以加载相关的配置到classpath)
//若使用java -jar 运行,不配置以上参数的情况下需要下载集群中的hadoop配置文件并打包到jar中,
//并使用绝对路径指定当前jar包位置,以便yarn加载jar包到集群,job.setJarByClass(WordCountDriver.class);
job.setJarByClass(WordCountDriver.class);
mapreduce程序在集群中运行时的大体流程:
附:在windows平台上访问hadoop时改变自身身份标识的方法之二:
流量统计
对日志数据中的上下行流量信息汇总
# 时间戳 手机号 00-FD-07-A4-72-B8:CMCC 访问网站IP 访问网站域名 发送的数据包 接收的数据包 上行流量(byte) 下行流量(byte) 响应码
1363157993044 18212349876 94-71-AC-CD-E6-18:CMCC-EASY 120.196.100.99 iface.qiyi.com 视频网站 15 12 1527 2106 200
1363157995074 15209872345 5C-0E-8B-8C-E8-20:7DaysInn 120.197.40.4 122.72.52.12 20 16 4116 1432 200
1363157993055 13560436666 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 18 15 1116 954 200
1363157993056 13577776666 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 18 15 1116 954 200
问题
-
统计每个用户(手机号)所耗费的总上行、下行流量,总流量
分析:
- map读取一行,切分字段;抽取手机号、上行流量、下行流量;context.write(手机号,bean(上下行流量))
- reduce 以手机号进行统计
https://gitee.com/SoFeelLove/hadoop_itcast/tree/master/src/main/java/com/lxf/bigdata/mr/flowsum - 计算后的结果为
#手机号 总上行流量 总下行流量 总流量
13760778710 10 200 210
13726238888 300 150 450
- 将统计结果按总流量倒序排列
mapreduce框架默认按输出的key排序(未指定排序规则的按hashCode,指定的按自定义的)后传给reducer
1、分两步进行操作,在问题一的结果上进行mapper计算
2、对结果以FlowBean为key,手机号为value<FlowBean,Phone>
输出,并实现WritableComparable<FlowBean>
比较方法
3、不同的对象hashcode不同,所以每个分区最终只有一组数据,进行reducer计算
mapreduce排序
分析:
基本思路:实现自定义的bean来封装流量信息,并将bean作为map输出的key来传输
MR程序在处理数据的过程中会对数据排序(map输出的kv对传输到reduce之前,会排序),排序的依据是map输出的key。所以,我们如果要实现自己需要的排序规则,则可以考虑将排序因素放到key中,让key实现接口:WritableComparable
,然后重写key的compareTo
方法
代码
FlowBean 对象实现WritableComparable<FlowBean>
接口
/**
* 流量对象,总流量倒序排列
* @author lxf
* @date 2018/04/02
*/
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
/**
* 通过反射生成对象,具有有参构造函数,需要手动提过无参构造函数
*/
public FlowBean() {
}
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
public void set(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow ;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
upFlow = dataInput.readLong();
downFlow = dataInput.readLong();
sumFlow = dataInput.readLong();
}
/**
* 倒序排列
* @param o
* @return
*/
@Override
public int compareTo(FlowBean o) {
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
Mapper
/**
* 流量统计map
* 获取每个电话号码的:总上行流量、总下行流量
* @author lxf
*/
public class FlowCountSortMapper extends Mapper<LongWritable,Text,FlowBean,Text> {
FlowBean flowBean = new FlowBean();
Text phoneNbrText = new Text();
/**
* 每行数据执行一次,若每次都创建一个flowBean对象,当数据量太大时,内存资源占用太大,
* mapreduce框架默认按输出的key排序(未指定排序规则的按hashCode,指定的按自定义的)后传给reducer
* reducer只能定义一个,全分区排序
* @param key
* @param value
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//拿到的是上一个统计程序的输出结果,已经是各手机的总流量信息
String line = value.toString();
String[] fields = line.split("\t");
String phoneNbr = fields[0];
long upFlow = Long.parseLong(fields[1]);
long downFlow = Long.parseLong(fields[2]);
flowBean.set(upFlow,downFlow);
phoneNbrText.set(phoneNbr);
/**
* write方法把所传入的参数对象序列化成了文件,
* 所以尽管指针指向同一个对象,但每次修改对象堆内存数据后,对象即被序列化,
* 所以序列化后的文件中存储的同一对象的内容不同
* 避免了同一类型对象被连续创建,占用内存资源
*/
context.write(flowBean,phoneNbrText);
}
Reducer
public class FlowCountSortReducer extends Reducer<FlowBean,Text,Text,FlowBean> {
/**
* 按key来调,传过来的量对象,每个对象hashCode都是不一样的,所以每个对象都调用一次reduce方法
* <bean1,phoneNbr1>
* <bean2,phoneNbr2>
* <bean3,phoneNbr3>
* @param key
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
context.write(values.iterator().next(),key);
}
}
- 将统计结果按手机归属地不同省份输出到不同文件
- map读取一行,切分字段;抽取手机号、上行流量、下行流量;context.write(手机号,bean(上下行流量))
- 根据需求对产生结果进行分区(partitioner)若:18个省份,分为18个区(重写partitioner)让相同的归属地号码返回相同的分区号int,默认使用HashPartitioner
Mapreduce中的分区Partitioner
分析:
Mapreduce中会将map输出的kv对,按照相同key分组,然后分发给不同的reducetask
默认的分发规则为:根据key的hashcode%reducetask数来分发
所以:如果要按照我们自己的需求进行分组,则需要改写数据分发(分组)组件Partitioner
- 自定义一个CustomPartitioner继承抽象类:
Partitioner
, - 然后在job对象中,设置自定义
partitioner
:job.setPartitionerClass(CustomPartitioner.class)
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public void configure(JobConf job) {}
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K2 key, V2 value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
- 使用18个reducer接收对应分区的数据
关键代码
/**
* 自定义分区方法
* Text,FlowBean对应的为mapper输出的kv类型
* @author lxf
* @version v1.0
* @date 2018/4/4 9:50
*/
public class ProvincePartitioner extends Partitioner<Text,FlowBean> {
private static HashMap<String,Integer> provinceDict = new HashMap<String, Integer>();
static {
provinceDict.put("136",0);
provinceDict.put("137",1);
provinceDict.put("138",2);
provinceDict.put("139",3);
}
/**
* 每行map数据执行一次,加载外部数据逻辑应该在类加载时加载一次即可
* @param text
* @param flowBean
* @param i
* @return
*/
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
String prefix = text.toString().substring(0,3);
Integer provinceId = provinceDict.get(prefix);
return provinceId == null ? 4 : provinceId;
}
}
/**
* 流量统计
* @author lxf
* @data 2018/04/02
*/
public class ProvinceFlowCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(ProvinceFlowCountDriver.class);
job.setMapperClass(FlowCountMapper.class);
//指定自定义的数据分区器
job.setPartitionerClass(ProvincePartitioner.class);
job.setReducerClass(FlowCountReducer.class);
//同时指定相应"分区"数量的reduceTask
job.setNumReduceTasks(5);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean wait = job.waitForCompletion(true);
System.exit(wait ? 0 : 1);
}
}
mapreduce数据压缩
这是mapreduce
的一种优化策略:通过压缩编码对mapper
或者reduce
r的输出进行压缩,以减少磁盘IO
,提高MR
程序运行速度(但相应增加了cpu
运算负担)
Mapreduce
支持将map
输出的结果或者reduce
输出的结果进行压缩,以减少网络IO
或最终输出数据的体积- 压缩特性运用得当能提高性能,但运用不当也可能降低性能
- 基本原则:
运算密集型的job
,少用压缩
IO
密集型的job
,多用压缩
MR支持的压缩编码
Reducer输出压缩
在配置参数或在代码中都可以设置reduce的输出压缩
- 在配置参数中设置
mapreduce.output.fileoutputformat.compress=false
mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.DefaultCodec
mapreduce.output.fileoutputformat.compress.type=RECORD
- 在代码中设置
Job job = Job.getInstance(conf);
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, (Class<? extends CompressionCodec>) Class.forName(""));
Mapper输出压缩
在配置参数或在代码中都可以设置reduce的输出压缩
- 在配置参数中设置
mapreduce.map.output.compress=false
mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.DefaultCodec
- 在代码中设置:
conf.setBoolean(Job.MAP_OUTPUT_COMPRESS, true);
conf.setClass(Job.MAP_OUTPUT_COMPRESS_CODEC, GzipCodec.class, CompressionCodec.class);
压缩文件的读取
Hadoop自带的InputFormat类内置支持压缩文件的读取,比如TextInputformat类,在其initialize方法中:
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
start = split.getStart();
end = start + split.getLength();
final Path file = split.getPath();
// open the file and seek to the start of the split
final FileSystem fs = file.getFileSystem(job);
fileIn = fs.open(file);
//根据文件后缀名创建相应压缩编码的codec
CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
if (null!=codec) {
isCompressedInput = true;
decompressor = CodecPool.getDecompressor(codec);
//判断是否属于可切片压缩编码类型
if (codec instanceof SplittableCompressionCodec) {
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
//如果是可切片压缩编码,则创建一个CompressedSplitLineReader读取压缩数据
in = new CompressedSplitLineReader(cIn, job,
this.recordDelimiterBytes);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else {
//如果是不可切片压缩编码,则创建一个SplitLineReader读取压缩数据,并将文件输入流转换成解压数据流传递给普通SplitLineReader读取
in = new SplitLineReader(codec.createInputStream(fileIn,
decompressor), job, this.recordDelimiterBytes);
filePosition = fileIn;
}
} else {
fileIn.seek(start);
//如果不是压缩文件,则创建普通SplitLineReader读取数据
in = new SplitLineReader(fileIn, job, this.recordDelimiterBytes);
filePosition = fileIn;
}
MapReduce特点
mapRecuce编程模型只能包含一个map阶段和一个reduce阶段,如果用户业务逻辑非常复杂只能用多个mp程序串行运行
MapReduce原理深解析
MapReduce原理全解析
mapreduce的shuffle机制
- mapreduce中,map阶段处理的数据如何传递给reduce阶段,是mapreduce框架中最关键的一个流程,这个流程就叫shuffle;
- shuffle: 洗牌、发牌——(核心机制:数据分区,排序,缓存);
- 具体来说:就是将maptask输出的处理结果数据,分发给reducetask,并在分发的过程中,对数据按key进行了分区和排序;
主要流程
Shuffle缓存流程:
shuffle是MR处理流程中的一个过程,它的每一个处理步骤是分散在各个map task和reduce task节点上完成的,整体来看,分为3个操作:
- 分区partition
- Sort根据key排序
- Combiner进行局部value的合并
详细流程
- maptask收集我们的map()方法输出的kv对,放到内存缓冲区中
- 从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢出过程中,及合并的过程中,都要调用partitoner进行分组和针对key进行排序
- reducetask根据自己的分区号,去各个maptask机器上取相应的结果分区数据
- reducetask会取到同一个分区的来自不同maptask的结果文件,reducetask会将这些文件再进行合并(归并排序)
- 合并成大文件后,shuffle的过程也就结束了,后面进入reducetask的逻辑运算过程(从文件中取出一个一个的键值对group,调用用户自定义的reduce()方法)
Shuffle中的缓冲区大小会影响到mapreduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快
缓冲区的大小可以通过参数调整, 参数:io.sort.mb 默认100M
详细流程示意图
MAPREDUCE中的序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系。。。。),不便于在网络中高效传输;
所以,hadoop自己开发了一套序列化机制(Writable),精简,高效
Jdk序列化和MR序列化之间的比较
简单代码验证两种序列化机制的差别:
public class TestSeri {
public static void main(String[] args) throws Exception {
//定义两个ByteArrayOutputStream,用来接收不同序列化机制的序列化结果
ByteArrayOutputStream ba = new ByteArrayOutputStream();
ByteArrayOutputStream ba2 = new ByteArrayOutputStream();
//定义两个DataOutputStream,用于将普通对象进行jdk标准序列化
DataOutputStream dout = new DataOutputStream(ba);
DataOutputStream dout2 = new DataOutputStream(ba2);
ObjectOutputStream obout = new ObjectOutputStream(dout2);
//定义两个bean,作为序列化的源对象
ItemBeanSer itemBeanSer = new ItemBeanSer(1000L, 89.9f);
ItemBean itemBean = new ItemBean(1000L, 89.9f);
//用于比较String类型和Text类型的序列化差别
Text atext = new Text("a");
// atext.write(dout);
itemBean.write(dout);
byte[] byteArray = ba.toByteArray();
//比较序列化结果
System.out.println(byteArray.length);
for (byte b : byteArray) {
System.out.print(b);
System.out.print(":");
}
System.out.println("-----------------------");
String astr = "a";
// dout2.writeUTF(astr);
obout.writeObject(itemBeanSer);
byte[] byteArray2 = ba2.toByteArray();
System.out.println(byteArray2.length);
for (byte b : byteArray2) {
System.out.print(b);
System.out.print(":");
}
}
}
自定义对象实现MR中的序列化接口
如果需要将自定义的bean放在key中传输,则还需要实现comparable接口,因为mapreduce框中的shuffle过程一定会对key进行排序,此时,自定义的bean实现的接口应该是:
public class FlowBean implements WritableComparable
需要自己实现的方法是:
/**
* 反序列化的方法,反序列化时,从流中读取到的各个字段的顺序应该与序列化时写出去的顺序保持一致
*/
@Override
public void readFields(DataInput in) throws IOException {
upflow = in.readLong();
dflow = in.readLong();
sumflow = in.readLong();
}
/**
* 序列化的方法
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upflow);
out.writeLong(dflow);
//可以考虑不序列化总流量,因为总流量是可以通过上行流量和下行流量计算出来的
out.writeLong(sumflow);
}
@Override
public int compareTo(FlowBean o) {
//实现按照sumflow的大小倒序排序
return sumflow>o.getSumflow()?-1:1;
}
大量小文件优化策略
默认情况下,TextInputFormat对任务的切片机制是按文件规划切片,不管文件多少,都会是一个单独的切片都会交给一个maptask,这样,如果有大量的小文件,就会产生大量的maptask,处理效率极其低下
优化策略
- 最好的办法:在数据处理系统的最前端(预处理、采集),就将文件合并成大文件,再上传到HDFS上做后续分析
- 补救措施:如果已经是大量小文件在hdfs中,可以使用另一种InputFormat做切片(CombineFileInputFormat),它的切片逻辑跟FileInputFormat不同;它可以将多个小文件从逻辑上规划到一个切片中,这样多个小文件就可以交给一个maptask进行处理
//如果不设置InputFormat,默认使用TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
// 4M
CombineTextInputFormat.setMaxInputSplitSize(job,4194304);
// 2M
CombineTextInputFormat.setMinInputSplitSize(job,2097152);
MAPREDUCE中的Combiner
- combiner是MR程序中Mapper和Reducer之外的一种组件
- combiner组件的父类就是Reducer
- combiner和reducer的区别在于运行的位置:
Combiner是在每一个maptask所在的节点运行
Reducer是接收全局所有Mapper的输出结果; - combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量
具体实现步骤:- 自定义一个combiner继承Reducer,重写reduce方法
- 在job中设置: job.setCombinerClass(CustomCombiner.class)
- combiner能够应用的前提是不能影响最终的业务逻辑
而且,combiner的输出kv应该跟reducer的输入kv类型要对应起来
Combiner的使用要非常谨慎
因为combiner在mapreduce过程中可能调用也肯能不调用,可能调一次也可能调多次
所以:combiner使用的原则是:有或没有都不能影响业务逻辑
MapReduce与YARN
YARN概述
Yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,而mapreduce等运算程序则相当于运行于操作系统之上的应用程序
YARN的重要概念
- yarn并不清楚用户提交的程序的运行机制
- yarn只提供运算资源的调度(用户程序向yarn申请资源,yarn就负责分配资源)
- yarn中的主管角色叫ResourceManager
- yarn中具体提供运算资源的角色叫NodeManager
- 这样一来,yarn其实就与运行的用户程序完全解耦,就意味着yarn上可以运行各种类型的分布式运算程序(mapreduce只是其中的一种),比如mapreduce、storm程序,spark程序,tez ……
- 所以,spark、storm等运算框架都可以整合在yarn上运行,只要他们各自的框架中有符合yarn规范的资源请求机制即可
- Yarn就成为一个通用的资源调度平台,从此,企业中以前存在的各种运算集群都可以整合在一个物理集群上,提高资源利用率,方便数据共享
Yarn中运行运算程序的示例
mapreduce程序的调度过程,如下图