系列博客
1、大数据技术之Hadoop完全分布式集群搭建+Centos7配置连通外网和主机
2、大数据技术之Hadoop编译源码
3、大数据技术之Hadoop分布式文件系统HDFS系统知识整理(从入门到熟练操作)
4、大数据技术之Hadoop分布式计算框架MapReduce系统知识整理(从入门到熟练操作)
文章目录
- 一、MapReduce概述
-
- 1.1 MapReduce定义
- 1.2 MapReduce优缺点
-
- 1.3 MapReduce核心思想
- 1.4 MapReduce进程
- 1.5 常用数据序列化类型
- 1.6 MapReduce编程规范
-
- 1.6.1 Mapper阶段
- 1.6.2 MapReduce阶段
- 1.6.3 Driver阶段
- 1.7 WordCount案例实操
- 二、Hadoop序列化
-
- 2.1 序列化概述
-
- 2.1.1 什么是序列化
- 2.1.2 为什么要序列化
- 2.1.3 为什么不用Java的序列化
- 2.1.4 Hadoop序列化特点
- 2.2 自定义bean对象实现序列化接口
- 三、MapReduce框架原理
-
- 3.1 InputFormat数据输入
-
- 3.1.1 切片与MapTask并行度决定机制
- 3.1.2 Job提交流程和切片
- 3.1.3 FileInputFormat切片机制
- 3.1.4 CombineTextInputFormat切片机制
- 3.1.5 FileInputFormat实现类
- 3.2 MapReduce工作流程
-
- 3.2.1 MapReduce工作流程图解
- 3.2.2 MapReduce工作流程详解
- 3.3 Shuffle机制
-
- 3.3.1 Shuffle机制图解
- 3.3.2 Partition分区
- 3.3.3 WritableComparable排序
- 3.3.4 Combiner合并
- 3.4 MapTask工作机制
- 3.5 ReduceTask工作机制
- 3.6 OutputFormat数据输出
- 3.7 Join多种应用
-
- 3.7.1 Reduce Join
- 3.7.2 Map Join
- 四、Hadoop数据压缩
-
- 4.1 概述
-
- 4.2 MR支持的压缩编码
- 4.3 压缩方式选择
-
- 4.3.1 Gzip压缩
- 4.3.2 Bzip2压缩
- 4.3.3 Lzo压缩
- 4.3.4 Snappy压缩
- 4.4 压缩位置选择
- 五、Yarn资源调度器
-
- 5.1 Yarn基本架构
- 5.2 Yarn工作机制
-
- 5.2.1 Yarn运行机制图解
- 5.2.2 Yarn工作机制详解
- 5.2.3 Yarn作业提交全过程详解
- 5.2.4 MapReduce作业提交全过程图解
- 5.3 资源调度器
-
- 5.3.1 先进先出调度器(FIFO)
- 5.3.2 容量调度器(Capacity Scheduler)
- 5.3.3 公平调度器(Fair Scheduler)
- 六、Hadoop优化
-
- 6.1 MapReduce跑得慢的原因
- 6.2 MapReduce优化方法
-
- 6.2.1 数据输入
- 6.2.2 Map阶段
- 6.2.3 Reduce阶段
- 6.2.4 I/O传输
- 6.2.5 数据倾斜问题
- 6.3 HDFS小文件优化方法
-
- 6.3.1 HDFS小文件弊端
- 6.3.2 HDFS小文件解决方案
一、MapReduce概述
1.1 MapReduce定义
- MapReduce是一个分布式运算程序的编程框架,是用户开发"基于Hadoop的数据分析应用”的核心框架
- MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上
1.2 MapReduce优缺点
1.2.1 优点
- MapReduce易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一摸一样的。就是因为这个特点使得MapReduce编程变得非常流行
- 良好的拓展性
当你的计算资源不能得到满足得时候,你可以通过简单的增加机器来扩展它的计算能力
- 高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是Hadoop内部完成的
- 适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力
1.2.2 缺点
- 不擅长事实计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果
- 不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化,因为MapReduce自身的设计特点决定了数据源必须是静态的
- 不擅长DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下
1.3 MapReduce核心思想
- 这是大数据领域经典的案例——WordCount,我们可以看到分布式的运算程序往往需要分成至少2个阶段
- 第一个阶段的MapTask并发实例,完全并行运行,互不相干
- 第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出
- MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,就只能多个MapReduce程序,串行运行
1.4 MapReduce进程
一个完整的MapReduce程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度以及状态协调
- MapTask:负责Map阶段的整个数据处理流程
- ReduceTask:负责Reduce阶段的整个数据处理流程
1.5 常用数据序列化类型
Java类型 |
Hadoop Writable类型 |
boolean |
BooleanWritable |
byte |
ByteWritable |
int |
IntWritable |
float |
FloatWritable |
long |
LongWritable |
double |
DoubleWritable |
String |
Text |
map |
MapWritable |
array |
ArrayWritable |
1.6 MapReduce编程规范
1.6.1 Mapper阶段
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper中的业务逻辑写在map()方法中
- Mapper的输出数据是KV对的形式(KV的类型可自定义)
- map()方法(MapTask进程)对每一个调用一次
1.6.2 MapReduce阶段
- 用户自定义的Reducer要继承自己的父类
- Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- ReduceTask进程对每一组相同k的组调用一次reduce()方法
1.6.3 Driver阶段
- 相当于Yarn集群的客户端,用于提交我们整个程序到Yarn集群,提交的是封装了MapReduce程序相关运行参数的job对象
1.7 WordCount案例实操
需求:在给定的文本文件中统计输出每一个单词出现的总次数
- 输入数据
IronmanJay IronmanJay
ss ss
cls cls
jiao
banzhang
xue
hadoop
- 创建Maven工程
3. 在pom.xml文件中添加如下依赖
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-commonartifactId>
<version>2.7.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>2.7.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-hdfsartifactId>
<version>2.7.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>2.3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
<archive>
<manifest>
<mainClass>WordCount.WordCountDrivermainClass>
manifest>
archive>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
- 在项目的src/main/resources目录下,新建一个文件,命名为“log4j.properties”,在文件中填入
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
- 编写Mapper类
package WordCount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
System.out.println(k.toString());
String line = value.toString();
String[] words = line.split(" ");
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
- 编写Reducer类
package WordCount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
v.set(sum);
context.write(key,v);
}
}
- 编写Driver驱动类
package WordCount;
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;
import java.io.IOException;
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{
"D:/a.txt", "D:/test"};
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setCombinerClass(WordCountReducer.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
- 本地测试
大数据技术之Hadoop分布式文件系统HDFS系统知识整理(从入门到熟练操作),请参阅以上博客安装本地Hadoop环境,然后在Eclipse或者Idea上运行程序
- 集群测试
依赖上面已经添加,直接打包成jar包拷贝到Hadoop集群中,然后执行WordCount程序
hadoop jar wc.jar WordCount.WordCountDriver /user/IronmanJay/input /user/IronmanJay/output
二、Hadoop序列化
2.1 序列化概述
2.1.1 什么是序列化
- 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输
- 反序列化就是将接收到的字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象
2.1.2 为什么要序列化
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另一台计算机。然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机
2.1.3 为什么不用Java的序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。Hadoop自己开发了一套序列化机制(Writable)
2.1.4 Hadoop序列化特点
- 紧凑:高效使用存储空间
- 快速:读写数据的额外开销小
- 可拓展:随着通信协议的升级而可升级
- 互操作:支持多语言的交互
2.2 自定义bean对象实现序列化接口
-
必须实现Writable接口
-
反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
- 重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
- 重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
-
注意反序列化的顺序和序列化的顺序完全一致
-
要想把结果显示在文件中,需要重写toString(),可用“\t”分开,方便后续使用
-
如果需要将自定义的bean放在key中传输,则还现需要实现Comparable接口,因为MapReduce框架中的Shuffle过程要求对key必须能排序
@Override
public int compareTo(FlowBean o) {
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
三、MapReduce框架原理
3.1 InputFormat数据输入
3.1.1 切片与MapTask并行度决定机制
- 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
- 每一个Split切片分配一个MapTask并行实例处理
- 默认情况下,切片大小=BlockSize
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
3.1.2 Job提交流程和切片
- 程序先找到数据存储的目录
- 开始遍历处理(规划切片)目录下的每一个文件
- 遍历第一个文件
①:获取文件大小
②:计算切片大小
③:默认情况下,切片大小=blocksize
④:开始切,形成第一个切片,0-128M;第二个切片128-256M;第三个切片256-300M(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
⑤:将切片信息写到一个切片规划文件中
⑥:整个切片的核心过程在getSplit()方法中完成
⑦:InputSplit只记录了切片的元数据信息,比如起始位置,长度以及所在的节点列表等
- 提交切片规则文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
3.1.3 FileInputFormat切片机制
- 切片机制
①:简单的按照文件的内容长度进行切片
②:切片大小,默认等于Block大小
③:切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
- 源码中计算切片大小的公式
①:Math.max(minSize,Math.min(maxSize,blockSize));
②:mapreduce.input.fileinputformat.split.minsize = 1; 默认值为1
③:mapreduce.input.fileinputformat.split.maxsize = LongMaxValue; 默认值为LongMaxValue
④:因此,默认情况下,切片大小=blocksize
- 切片大小设置
①:maxsize(切片最大值):参数如果调的比blockSize小,则会让切片变小,而且就等于配置的这个参数的值
②:minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大
- 获取切片信息API
①:String name = inputSplit.getPath().getName(); 获取切片的文件名称
②:FileSplit inputSplit = (FileSplit) context.getInputSplit(); 根据文件类型获取切片信息
3.1.4 CombineTextInputFormat切片机制
框架默认的TextInputFormat切片机制是对任务按文件规则切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率及其低下
- 应用场景
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理
- 虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job,4194304),注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值
- 切片机制
①:将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)
②:例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02文件切分成(2.01和2.01两个文件
- 切片过程
①:判断虚拟存储的文件的大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片
②:如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片
③:测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M、以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M),最终会形成3个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
3.1.5 FileInputFormat实现类
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
- TextInputFormat
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起视字节偏移量,LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型
- KeyValueTextInputFormat
每一行均为一条记录,被分隔符分割为key,value。可以通过在驱动类种设置conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t");来设定分隔符。默认分隔符是tab(\t)
- NLineInputFormat
如果使用NLineInputFormat,代表每个map进程处理的InputSplit不再按Block块去划分,而是按NLineInputFormat指定的行数N来划分,即输入文件的总行数/N=切片数,如果不整除,切片数=商+1
3.2 MapReduce工作流程
3.2.1 MapReduce工作流程图解
3.2.2 MapReduce工作流程详解
上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解如下:
- MapTask收集map()方法输出的kv对,放到内存缓冲区中
- 从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢出过程及合并过程中,都要调用Partitioner进行分区和针对key进行排序
- ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
- ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
- 合并成大文件后,Shuffle的过程就结束了,后面进入ReduceTask逻辑运算过程(从文件中取出键值对Group,调用自定义reduce()方法)
- 注意
①:Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘IO的次数越少,执行的速度就越快
②:缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M
3.3 Shuffle机制
3.3.1 Shuffle机制图解
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle,如图所示
3.3.2 Partition分区
- 问题引出
要求将统计结果按照条件输出到不同的文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同的文件中(分区)
- 默认Partitioner分区
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区
- 自定义Partitioner步骤
①:自定义类继承Partitioner,重写getPartition()方法
②:在Job驱动中,设置自定义Partitioner
③:自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
- 分区总结
①:如果ReduceTask的数量>getPartition的结果数,则会多产生几个空的输出文件part-r-000xx
②:如果1 ③:如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件part-t-00000
④:分区号必须从零开始,逐一累加
3.3.3 WritableComparable排序
- 排序概述
①:排序是MapReduce框架中最重要的操作之一
②:MapTask和ReduceTask会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要
③:默认排序是按照字典顺序排序,且实现该排序的方法是快速排序
④:对于MapTask它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序
⑤:对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则存储在内存中。如果磁盘上的文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件。如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序
- 排序分类
①:部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序
②:全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构
③:辅助排序(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序
④:二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序
- 自定义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;
}
3.3.4 Combiner合并
- Combiner是MR程序中Mapper和Reduce之外的一种组件
- Combiner组件的父类就是Reduce
- Combiner和Reduce的区别在于运行的位置。Combiner是在每一个MapTask所在的节点运行,Reduce是接收全局所有Mapper的输出结果
- Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
- Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reduce的输入kv类型要对应起来
3.4 MapTask工作机制
- Read阶段
MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value
- Map阶段
该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value
- Collect收集阶段
在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中
- Spill阶段
即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作
- Combine阶段
当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
3.5 ReduceTask工作机制
- Copy阶段
ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中
- Merge阶段
在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多
- Sort阶段
按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可
- Reduce阶段
reduce()函数将计算结果写到HDFS上
3.6 OutputFormat数据输出
- 文本输出TextOutputFormat
默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把他们转换为字符串
- SequenceFileOutputFormat
将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这便是一种好的输出格式,因为他的格式紧凑,更容易被压缩
- 自定义OutputFormat
①:自定义一个类继承FileOutputFormat
②:改写RecordWriter,具体改写输出数据的方法write()
3.7 Join多种应用
3.7.1 Reduce Join
- Map端的主要工作
给来自不同表或文件的键值对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后输出
- Reduce端的主要工作
在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志),最后进行合并就可以了
- Reduce Join缺点及解决方案
①:缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负荷则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜
②:解决方案:Map端实现数据合并
3.7.2 Map Join
- 使用场景
Map Join适用于一张表十分小,一张表很大的场景
- 优点
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜
四、Hadoop数据压缩
4.1 概述
4.1.1 压缩概述
- 压缩技术能够有效减少底层存储系统(HDFS)读写字节数。压缩提高了网络带宽和磁盘空间的效率。在运行MR程序时,I/O操作、网络数据传输、Shuffle和Merge要花大量的时间,尤其是数据规模很大和工作负载密集的情况下,因此,使用数据压缩显得非常重要
- 鉴于磁盘I/O和网络带宽时Hadoop的宝贵资源,数据压缩对于节省资源、最小化磁盘I/O和网络传输非常有帮助。可以在任意MapReduce阶段启用压缩。不过,尽管压缩与解压操作的CPU开销不高,其性能的提升和资源并非没有代价
4.1.2 压缩策略和原则
- 压缩是提高Hadoop运行效率的一种优化策略
- 通过对Mapper、Reducer运行过程的数据进行压缩 ,以减少磁盘I/O,提高MR程序运行速度
- 采用压缩技术减少了磁盘I/O,但同时增加了CPU运算负担。所以,压缩特性运用得当能提高性能,但运用不当也可能降低性能
- 压缩基本原则
①:运算密集型的job,少用压缩
②:I/O密集型的job,多用压缩
4.2 MR支持的压缩编码
- 压缩格式
压缩格式 |
是否Hadoop自带 |
算法 |
文件拓展名 |
是否可切分 |
换成压缩格式后,原来的程序是否需要修改 |
DEFLATE |
是,直接使用 |
DEFLATE |
.deflate |
否 |
和文本处理一样,不需要修改 |
Gzip |
是,直接使用 |
DEFLATE |
.gz |
否 |
和文本处理一样,不需要修改 |
bzip2 |
是,直接使用 |
bzip2 |
.bz2 |
是 |
和文本处理一样,不需要修改 |
LZO |
否,需要安装 |
LZO |
.lzo |
是 |
需要建索引,还需要指定输入格式 |
Snappy |
否,需要安装 |
Snappy |
.snappy |
否 |
和文本处理一样,不需要修改 |
- 编码/解码器
压缩格式 |
对应的编码/解码器 |
DEFLATE |
org.apache.hadoop.io.compress.DefaultCodec |
gzip |
org.apache.hadoop.io.compress.GzipCodec |
bzip2 |
org.apache.hadoop.io.compress.BZip2Codec |
LZO |
com.hadoop.compression.lzo.LzopCodec |
Snappy |
org.apache.hadoop.io.compress.SnappyCodec |
- 压缩性能比较
压缩算法 |
原始文件大小 |
压缩文件大小 |
压缩速度 |
解压速度 |
gzip |
8.3GB |
1.8GB |
17.5MB/s |
58MB/s |
bzip2 |
8.3GB |
1.1GB |
2.4MB/s |
9.5MB/s |
LZO |
8.3GB |
2.9GB |
49.3MB/s |
74.6MB/s |
4.3 压缩方式选择
4.3.1 Gzip压缩
- 优点
压缩率比较高,而且压缩/解压速度也比较快;Hadoop本身支持,在应用中处理Gzip格式的文件就和直接处理文本一样;大部分Linux系统都自带Gzip命令,使用方便
- 缺点
不支持Split
- 应用场景
当每个文件压缩之后在130M以内的(一个块大小内),都可以考虑使用Gzip压缩格式。例如说一天或者一个小时的日志压缩成一个Gzip文件
4.3.2 Bzip2压缩
- 优点
支持Split;具有很高的压缩率,比Gzip压缩率都高;Hadoop本身自带,使用方便
- 缺点
压缩/解压速度慢
- 应用场景
适合对速度要求不高,但需要较高的压缩率的时候;或者输出之后的数据比较大,处理之后的数据需要压缩存档减少磁盘空间并且以后数据用得比较少的情况;或者对单个很大的文本文件想压缩减少存储空间,同时又需要支持Split,而且兼容之前的应用程序的情况
4.3.3 Lzo压缩
- 优点
压缩/解压速度也比较快,合理的压缩率;支持Split,是Hadoop中最流行的压缩格式;可以在Linux系统下安装lzop命令,使用方便
- 缺点
压缩率比Gzip要低一些;Hadoop本身不支持,需要安装;在应用中对Lzo格式的文件需要做一些特殊处理(为了支持Split需要建索引,还需要指定InputFormat为Lzo格式)
- 应用场景
一个很大的文本文件,压缩之后还大于200M以上的可以考虑,而且单个文件越大,Lzo优点越明显
4.3.4 Snappy压缩
- 优点
高速压缩速度和合理的压缩率
- 缺点
不支持Split;压缩率比Gzip要低;Hadoop本身不支持,需要安装
- 应用场景
当MapReduce作业的Map输出的数据比较大的时候,作为Map到Reduce的中间数据的压缩格式;或者作为一个MapReduce作业的输出和另一个MapReduce作业的输入
4.4 压缩位置选择
压缩可以在MapReduce作用的任意阶段启用,如图所示
五、Yarn资源调度器
Yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,而MapReduce等运算程序则相当于运行于操作系统之上的应用程序
5.1 Yarn基本架构
Yarn主要由ResourceManager、NodeManager、ApplicationMaster和Container等组件构成,如图所示
- ResourceManager(RM)主要作用如下
①:处理客户端请求
②:监控NodeManager
③:启动或监控ApplicationMaster
④:资源的分配与调度
- NodeManager(NM)主要作用如下
①:管理单个节点上的资源
②:处理来自ResourceManager的命令
③:处理来自ApplicationMaster的命令
- ApplicationMaster(AM)主要作用如下
①:负责数据的切分
②:为应用程序申请资源并分配给内部的任务
③:任务的监控与容错
- Container
Container是Yarn中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等
5.2 Yarn工作机制
5.2.1 Yarn运行机制图解
5.2.2 Yarn工作机制详解
- MR程序提交到客户端所在的节点
- YarnRunner向ResourceManager申请一个Application
- RM将该应用程序的资源路经返回给YarnRunner
- 该程序将运行所需资源提交到HDFS上
- 程序资源提交完毕后,申请运行mrAppMaster
- RM将用户的请求初始化成一个Task
- 其中一个NodeManager领取到Task任务
- 该NodeManager创建容器Container,并产生MRAppmaster
- Container从HDFS上拷贝资源到本地
- MRAppmaster向RM申请运行MapTask资源
- RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器
- MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序
- MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask
- ReduceTask向MapTask获取相应分区的数据
- 程序运行完毕后,MR会向RM注销自己
5.2.3 Yarn作业提交全过程详解
- 作业提交
①:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业
②:Client向RM申请一个作业id
③:RM给Client返回该job资源的提交路径和作业id
④:Client提交jar包、切片信息和配置文件到指定的资源提交路径
⑤:Client提交完资源后,向RM申请运行MrAppMaster
- 作业初始化
①:当RM收到Client的请求后,将该job添加到容量调度器中
②:某一个空闲的NM领取到该Job
③:该NM创建Container,并产生MRAppmaster
④:下载Client提交的资源到本地
- 任务分配
①:MrAppMaster向RM申请运行多个MapTask任务资源
②:RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器
- 任务运行
①:MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序
②:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask
③:ReduceTask向MapTask获取相应分区的数据
④:程序运行完毕后,MR会向RM申请注销自己
- 进度和状态更新
YARN中的任务将其进度和状态(包括counter)返回给应用管理器, 客户端每秒向应用管理器请求进度更新, 展示给用户
- 作业完成
除了向应用管理器请求作业进度外, 客户端每5秒都会通过调用waitForCompletion()来检查作业是否完成。时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后, 应用管理器和Container会清理工作状态。作业的信息会被作业历史服务器存储以备之后用户核查
5.2.4 MapReduce作业提交全过程图解
5.3 资源调度器
目前Hadoop作业调度器主要有三种:FIFO、Capacity Scheduler和Fair Scheduler。Hadoop2.7.2默认的资源调度器是Capacity Scheduler
5.3.1 先进先出调度器(FIFO)
5.3.2 容量调度器(Capacity Scheduler)
- 支持多个队列,每个队列可配置一定的资源量,每个队列采用FIFO调度策略
- 为了防止同一个用户的作业独占队列中的资源,该调度器会对同一用户的作业所占资源量进行限定
- 首先,计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值,选择一个该比值最小的队列——最闲的
- 其次,按照作业优先级和提交时间顺序,同时考虑用户资源量限制和内存限制对队列内任务排序
- 三个队列同时按照任务的先后顺序依次执行。比如:job11、job21和job31分别排在队列最前面,先运行,也是并行运行
5.3.3 公平调度器(Fair Scheduler)
- 支持多队列多用户,每个队列中的资源量可以配置,同一队列中的作业公平共享队列中所有资源
- 比如有三个队列,如上图,每个队列中的job按照优先级分配资源,优先级越高分配的资源越多,但是每个job都会分配到资源以确保公平
- 在资源有限的情况下,每个job理想情况下获得的计算资源与实际获得的计算资源存在一种差距,这个差距就叫缺额
- 在同一个队列中,job的资源缺额越大,越先获得资源优先执行。作业是按照缺额的高低来先后执行的,可以看到上图有多个作业同时运行
六、Hadoop优化
6.1 MapReduce跑得慢的原因
- 计算机性能
①:CPU、内存、磁盘健康、网络
- I/O数据优化
①:数据倾斜
②:Map和Reduce数量设置不合理
③:Map运行时间太长, 导致Reduce等待过久
④:小文件过多
⑤:大量的不可分块的超大文件
⑥:Spill次数过多
⑦:Merge次数过多等
6.2 MapReduce优化方法
6.2.1 数据输入
- 合并小文件。
在执行MR任务前将小文件进行合并,大量的小文件会产生大量的Map任务,增大Map任务装载次数,而任务的装载比较耗时,从而导致MR运行较慢
- 使用CombineTextInputFormat
采用CombineTextInputFormat来作为输入,解决输入端大量小文件场景
6.2.2 Map阶段
- 减少溢写(Spill)次数
通过调整io.sort.mb以及sort.spill.percent参数值,增大触发Spill的内存上限减少Spill次数,从而减少磁盘I/O
- 减少合并(Merge)次数
通过调整io.sort.factor参数,增大Merge的文件数目,减少Merge的次数,从而缩短MR处理时间
- Map后进行Combine处理
在Map之后,不影响业务逻辑的前提下,先进行Combine处理,减少I/O
6.2.3 Reduce阶段
- 合理设置Map和Reduce数量
两个都不能设置太少,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致Map、Reduce任务间竞争资源,造成处理超时等错误
- 设置Map、Reduce共存
调整slowstart.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,减少Reduce的等待时间
- 规避使用Reduce
因为Reduce在用于连接数据集的时候将会产生大量的网络消耗
- 合理设置Reduce端的Buffer
默认情况下,数据达到一个阈值的时候,Buffer中的数据就会写入磁盘,然后Reduce会从磁盘中获得所有的数据。也就是说,Buffer和Reduce是没有直接关联的,中间多次写磁盘->读粗盘的过程,既然有这个弊端,那么就可以通过参数来配置,使得Buffer中的一部分数据可以直接输送到Reduce,从而减少I/O开销
6.2.4 I/O传输
- 采用数据压缩的方式,减少网络I/O的时间。安装Snappy和Lzo压缩编码器
- 使用SequenceFile二进制文件
6.2.5 数据倾斜问题
- 数据倾斜现象
①:数据频率倾斜——某一个区域的数据量远远大于其他区域
②:数据大小倾斜——部分记录的大小远远大于平均值
- 减少数据倾斜的方法
①:抽样和范围分区
可以通过对原始数据进行抽样得到的结果来预设分区边界值
②:自定义分区
基于输出键的背景知识进行自定义分区。例如,如果Map输出键的单词来源于一本书,且其中某几个专业词汇较多,那么就可以自定义分区将这些专业词汇发送给固定的一部分Reduce实例,而将其他的都发送给剩余的Reduce实例
③:Combine
使用Combine可以大量地减少数据倾斜,在可能的情况下,Combine的目的就是聚合并精简数据
④:采用Map Join,尽量避免Reduce Join
6.3 HDFS小文件优化方法
6.3.1 HDFS小文件弊端
HDFS上每个文件都要在NameNode上建立一个索引,这个索引的大小约为150byte,这样当小文件比较多的时候,就会产生很多的索引文件,一方面会大量占用NameNode的内存空间,另一方面就是索引文件过大使得索引速度变慢
6.3.2 HDFS小文件解决方案
- 基础方式
①:在数据采集的时候,就将小文件或小批数据合并成大文件再上传HDFS
②:在业务处理之前,在HDFS上使用MapReduce程序对小文件进行合并
③:在MapReduce处理时,可采用CombineTextInputFormat提高效率
- Hadoop Archive
这是一个高效的将小文件放入HDFS块中的文件存档工具,它能够将多个小文件打包成一个HAR文件,这样就减少了NameNode的内存使用
- Sequence File
Sequence File由一系列的二进制key/value组成,如果key为文件名,value为文件内容,则可以将大批小文件合并成一个大文件
- CombineFileInputFormat
CombineFileInputFormat是一种新的InputFormat,用于将多个文件合并成一个单独的Spilt,另外,它会考虑数据的存储位置
- 开始JVM重用
对于大量的小文件Job,可以开启JVM重用会减少45%运行时间。JVM重用原理:一个Map运行在一个JVM上,开启重用的话,该Map在JVM上运行完毕后,JVM继续运行其他Map