MapReduce
MapReduce
MapReduce概述
MapReduce定义
- Mapreduce是一个分布式运算框架。
- 核心功能是将用户编写的业务逻辑和自带默认组件整合成一个完整的分布式运算程序部署在hadoop上。
Mapreduce的优缺点
优点:
- 1.MapReduce易于编程,只需要实现一些简单的接口。
- 2.良好的扩展性,可以通过增加机子来增强性能。
- 3.高容错性,一台节点挂了,并不会影响整个集群执行命令。
- 4.适合PB级别以上海量数据的离线处理。
缺点:
- 1.不擅长实时计算
- 2.不擅长流式计算
- 3.不擅长DAG计算
Mapreduce的核心思想
- 1)分布式的运算程序往往需要分成至少2个阶段。
- 2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。
- 3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
- 4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
总结:分析WordCount数据流走向深入理解MapReduce核心思想。
MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
1.Mapper:
- 1.用户自定义Mapper继承父类
- 2.Mapper的输入输出都是<K,V>形式的
- 3.Mapper的业务逻辑写在map()方法中
- 4.map()方法对每一个<K,V>调用一次
2.Reduce:
- 1.用户自定义Reducer要继承自己的父类
- 2.Reducer输入是Mapper的输出
- 3.reduce的业务逻辑写在reduce()中
- 4.ReduceTask对每一组调用一次reduce()
3.Driver
- 相当于Yarn集群的客户端,用于提交到Yarn集群,封装了mapreduce相关的job参数
Hadoop序列化
- 序列化就是将内存中的对象转换为字节序列,方便在网络中传输使用。
- 反序列化就是将传过来的字节序列,解析为对象实体。
- 序列化的作用是可以将内存中的对象进行网络传输。
java序列化和Writable的区别
- java序列化很重,携带很多额外信息(校验信息,Header等等),不便于在网络中高校传输。
- Hadoop序列化特征
- 1.紧凑:高效使用存储空间
- 2.快速:读写数据的额外开销小1
- 3.可扩展:随着协议升级
- 4.互操性:支持多语言交互
自定义bean对象实现序列化接口(Writable)
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。
具体实现bean对象序列化步骤如下7步。
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
(3)重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
(4)重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
MapReduce框架原理
切片与MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
Job提交流程源码和切片源码详解
// 1建立连接
connect();
// 1)创建提交Job的代理
new Cluster(getConfiguration());
// (1)判断是本地yarn还是远程
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
FileInputFormat 切片源码:
- 程序先找到你数据存储的目录
- 开始遍历处理(规划切片)目录下的每一个文件
- 遍历第一个文件
- 获取文件的大小fs.sizeOf(ss.txt)
- 计算切片大小:computeSplitSize(minSize,Math.Min(maxSize,blocksize))
- 一般情况下切片大小等于block大小(128M)
- 最后一块切片可以是切片大小的1.1倍
- 将切片信息写入切片规划文件中(起始位置,偏移量,数据位置等等等等)
- 整个切片核心过程在getSplit()方法中完成
- 提交切片规划文件到Yarn中,Yarn中的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。
FileInputFormat 切片机制:
-
1.切片机制
- 简单的按照文件的内容进行切片
- 切片大小默认为Block大小
- 切片是不考虑整个数据集,而是考虑单独的文件单独切片
-
2.案例分析
- 输入的文件有两个:f1(300M),f2(10M)
- 切片结果:f1切成(0-128,128-256,256-300),f2(0-10)总共4片
FileInputFormat的切片机制
- 源码计算公式:Max.max(minSize,Math.Min(maxSize,blocksize)) ;
- minSize默认值为1
- maxSize默认为Long.MaxSize,922亿亿多
- 参数调整:
- maxsize:切片最大值
- minsize:切片最小值
- 可以通过context.getInputSplit() 方法查看切片信息
CombineTextInputFormat切片机制
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
- 作用:CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
- 虚拟存储切片最大值设置:CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
CombineTextInputFormat切片机制:
- 虚拟存储过程
-将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较- 如果不大于设置的最大值,逻辑上划分一个块。
- 如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块
- 当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)
- 切片过程
- 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
- 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
FileInputFormat实现类
- TextInputFormat
- k(LongWritable):当前行起始值的偏移量
- v(Text):当前行的数据
- KeyValueTextFormat:每行是一条记录,通过设定的分隔符分割
- k(Text):分隔符前的文本
- v(Text):分隔符后的文本
- NlineInputFormat:按照指定的行数进行切片,N代表切片数量,每个切片是Line/N行
- 当前行起始值的偏移量
- 当前行的数据
- CombineTextInputFormat:上面讲了
- 自定义Format:详见下文
自定义InputFormat
步骤
- 自定义一个类继承FileInputFormat
- 改写RecordReader,完成一次读取一歌完整文件封装为KV
- 在输出的时候使用SequenceFileOutputFormat输出合并文件
- SequenceFilekey一定程度上可以处理小文件问题, 它内部存储的形式为文件路径+名称为key,文件内容为value。
MapReduce工作流程(重点!!!)
Map工作流程:
- 客户端submit前获得待处理数据信息,根据配置参数形成任务规划文件
- 提交信息[Job.split,***.jar,Job.xml]
- 根据切片规划文件计算出MapTask数量
- MapTask通过规定的InputFormat去读取文件,并形成KV格式的数据
- 执行Mapper中的map方法进行逻辑和业务操作,并且通过context.write()方法写出
- 通过outputCollector将<k,v>数据写入环形缓冲区.
- 环形缓冲区默认大小是100m
- 环形缓冲区分为两个部分,索引区和数据区
- 索引区存放数据的元数据,比如索引,分区,keystart,valuestart等
- 数据区存放数据的kv值
- 从一点分开往两个方向写,写到80%的时候分区,排序
- 随后溢写到磁盘,同时之后的数据从剩下的20%中随意找一个点开始往两个方向写。
- 溢写到磁盘时,可以进行combiner进行合并,然后再进行归并排序
- 归并排序之后可以再进行combiner
- 最后溢写到磁盘上。
Reduce工作流程:
- MapTask任务完成后MrappMaster启动相应数量的ReduceTask,并告知ReduceTask处理的数据分区。
- 将不同MapTask产生的相同分区的数据下载到同一个ReduceTask中(Reduce是主动的)
- 将下载下来的文件进行合并,归并排序
- 一次读取一组数据放到Reducer中进行合并处理
- 可以进行分组操作
- 根据OutPutFormat调用RecordWriter,将Reduce之后的数据写到磁盘上。
Shuffle机制
Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
- 写入环形缓冲区,分为两部分index区域和data区域。
- 到达80%,对每个分区内进行快排,Combiner(可选),然后溢写到磁盘上。
- 当数据处理完,对磁盘上所有文件进行归并排序,Combiner(可选),压缩。
- 生成map输出文件落到磁盘上,文件特点就是分区,且分区内有序
- 将map输出文件读到内存缓存中,但是只读自己reduceTask指定的分区,内存不够就溢写到磁盘。
- 如果磁盘上的文件数量达到一定阈值,则进行一次归并排序,整合文件。
- 每个ReduceTask最后将所有map输出文件(内存中+磁盘上)进行归并排序,然后分组放入Reduce方法。
Partition分区
- Hadoop分区默认采取Hash分区
//分区规则
public int getPartition(K2 key, V2 value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
- 自定义分区步骤
- 继承Partitioner类,重写getPartition()方法
- job驱动中设置自定义Partition()
job.setPartitionClass(***.class)
- 自定义Partition后,要根据自定义Partition的逻辑设置相应数量的ReduceTask
job.setNumReduceTask()
WritableComparable排序
- MapTask和ReduceTask默认Key进行排序的,该操作是Hadoop默认操作。
- 默认排序是按照字典排序,排序方法是快速排序
- 排序分类
- 部分排序:根据输入记录的键进行排序,保证每个输出的文件内部有序
- 全排序:最终输出一个文件,且文件内部有序(没意义)
- 辅助排序:GroupingComparator分组->如果key是bean对象时,按照bean对象,想让某些相同属性的key进入同一个reduce。
- 二次排序:自定义排序中GroupingComparator判断条件为两个。
自定义排序WritableComparable
- bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。
@Override
public int compareTo(FlowBean o) {
int result;
// 按照总流量大小,倒序排列
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
Combiner合并
- 相当于对每一个reduce进行一次预处理
- 父类是Reducer
- 使用Combiner的前提是不能影响最终的业务逻辑
- 实现方法:
- 自定义一个Combiner继承Reducer,重写Reduce方法
- job.setCombinerClass(WordcountCombiner.class);
GroupingComparator分组(辅助排序)
- 对Reduce阶段的数据根据某一个或几个字段进行分组。
- 分组排序步骤:
- 自定义类继承WritableComparator
- 重写compare()方法
- 创建一个构造将比较对象的类传给父类
MapTask工作机制
-
(1)Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value。
-
(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
-
(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
-
(4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
- 步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
- 步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
- 步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
-
(5)Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
-
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
ReduceTask工作机制
(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。
设置ReduceTask并行度(个数)
- ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
OutputFormat数据输出
OutputFormat接口实现类
- TextOutputFormat:把记录转换成文本行
- SequenceFileOutputFormat:结构紧凑,容易被压缩
- 自定义OutputFormat:用户自定义
自定义OutputFormat
- 创建一个类继承OutputFormat
- 重写RecordWriter方法,在里面创建一个类,继承RecordWriter
- 进行数据流操作
- Driver中设置job.setOutputFormatClass(FilterOutputFormat.class);
Join多种应用
Reduce Join:
Map端的主要工作:为来自不同表的
Map Join:
1.使用场景:Map Join适用于一张表十分小、一张表很大的场景。
2.优点:在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
3.具体办法:采用DistributedCache
- (1)在Mapper的setup阶段,将文件读取到缓存集合中。
- (2)在驱动函数中加载缓存。// 缓存普通文件到Task运行节点。
job.addCacheFile(new URI(“file://e:/cache/pd.txt”));
Hadoop数据压缩
- 能够较明显的优化Hadoop的运行效率
- 采用压缩能够提高IO速度,但是会加大CPU压力
- IO密集型的job:多用压缩
- 运算密集型的job:少用压缩
MR支持的压缩编码
压缩方式选择
- Gzip压缩
- 优点:压缩率比较高,而且压缩解压速度也比较快。
- 缺点:不支持split
- Bzip2压缩
- 优点:支持split,也有较高的压缩率
- 缺点:解压/压缩速度慢
- Lzo压缩
- 优点:压缩/解压比较快,支持split,可以在Linux系统下安装lzop命令
- 缺点:压缩率比GZip低一点,本身hadoop不支持,需要单独安装
- Snappy压缩:
- 优点:高速压缩和较为不错的压缩率
- 缺点:不支持split;压缩率比Gzip低,Hadoop本身不支持
压缩位置选择
- 压缩可以在MapReduce作用的任意阶段启用
Yarn资源调度器
YARN主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成。
- ResourceManager:
- 1)处理客户端请求
- 2)监控NodeManager
- 3)启动或监控ApplicationMaster
- 4)资源的分配和调度
- NodeManager:
- 1)管理单个节点上的资源·
- 2)处理来自ResoucreManager的命令
- 3)处理来自ApplicationMaster的命令
- ApplicationMaster:
- 1)负责数据的切分
- 2)为应用程序申请资源并分给内部的任务
- 3)任务的监控和容错
- Container:
- Container是YARN中的资源抽象,它封装了某个节点上的多维度资源,比如分配到的内存,CPU,磁盘和网络等。
Yarn工作机制
工作机制详解
(1)MR程序提交到客户端所在的节点。
(2)YarnRunner向ResourceManager申请一个Application。
(3)RM将该应用程序的资源路径返回给YarnRunner。
(4)该程序将运行所需资源提交到HDFS上。
(5)程序资源提交完毕后,申请运行mrAppMaster。
(6)RM将用户的请求初始化成一个Task。
(7)其中一个NodeManager领取到Task任务。
(8)该NodeManager创建容器Container,并产生MRAppmaster。
(9)Container从HDFS上拷贝资源到本地。
(10)MRAppmaster向RM 申请运行MapTask资源。
(11)RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。
(12)MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
(13)MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
(14)ReduceTask向MapTask获取相应分区的数据。
(15)程序运行完毕后,MR会向RM申请注销自己。
作业提交全过程
作业提交全过程详解
- (1)作业提交
- 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。
- 第2步:Client向RM申请一个作业id。
- 第3步:RM给Client返回该job资源的提交路径和作业id。
- 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。
- 第5步:Client提交完资源后,向RM申请运行MrAppMaster。
- (2)作业初始化
- 第6步:当RM收到Client的请求后,将该job添加到容量调度器中。
- 第7步:某一个空闲的NM领取到该Job。
- 第8步:该NM创建Container,并产生MRAppmaster。
- 第9步:下载Client提交的资源到本地。
- (3)任务分配
- 第10步:MrAppMaster向RM申请运行多个MapTask任务资源。
- 第11步:RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。
- (4)任务运行
- 第12步:MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
- 第13步:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
- 第14步:ReduceTask向MapTask获取相应分区的数据。
- 第15步:程序运行完毕后,MR会向RM申请注销自己。
- (5)进度和状态更新
YARN中的任务将其进度和状态(包括counter)返回给应用管理器, 客户端每秒(通过mapreduce.client.progressmonitor.pollinterval设置)向应用管理器请求进度更新, 展示给用户。 - (6)作业完成
除了向应用管理器请求作业进度外, 客户端每5秒都会通过调用waitForCompletion()来检查作业是否完成。时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后, 应用管理器和Container会清理工作状态。作业的信息会被作业历史服务器存储以备之后用户核查。
资源调度器
- FIFO资源调度器:按照到达时间排序,先到先服务
- 容量调度器:按照到达时间排序,先到先服务
- 1.支持多个队列,没一个队列配置一定资源量,每个队列采用FIFO策略
- 2.为防止同一个用户的作业单独占队列中的资源,该调度器会对同一用户提交的作业所占资源进行限定。
- 3.首先计算每一个队列中正在运行的任务数,与其应该分得的计算资源的比值,选最小的。
- 4.按照作业优先级和提交时间序列,同时考虑用户资源量吸纳之和内存限制队列内任务排序。
- 5.所有队列并行执行
- 公平调度器:按照缺额排序,缺额大者优先
任务的推测执行
-
作业完成时间取决于最慢的任务完成时间
-
**推测执行机制:**发现拖后腿的任务,比如某个任务运行速度远慢于任务平均速度。为拖后腿任务启动一个备份任务,同时运行。谁先运行完,则采用谁的结果。
-
执行推测任务的前提条件
- (1)每个Task只能有一个备份任务
- (2)当前Job已完成的Task必须不小于0.05(5%)
- (3)开启推测执行参数设置。mapred-site.xml文件中默认是打开的。
-
不能启用推测执行机制情况
- (1)任务间存在严重的负载倾斜;
- (2)特殊任务,比如任务向数据库中写数据。