Hadoop2.x-基础(MapReduce)
MapReduce简介
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架
MapReduce核心功能是将用户编写的业务代码和自带默认组件整合成一个完整的分布式运算程序,并运行再一个Hadoop集群上
优缺点
优点:
- MapReduce易于编程:只需要简单实现一些接口,就可以完成一个分布式程序,如果你要编写一个分布式程序,你只需要根据MapReduce规范编写一个简单的串行程序一模一样,最终由MapReduce帮你完成在分布式下的资源调度与计算你只需专注与业务,这使得MapReduce编程变的非常流行
- 良好的扩展性:当你计算机资源不满足当前运算的实现,只需要通过简单的增加机器来扩展它的计算能力
- 高容错性:MapReduce设计初衷就是使程序能够不是在廉价的PC机器上,这一特点使得MapReduce具备高容错性,比如一台机器挂了,它可以把上面的计算任务转移到路外一个节点上运行,不至于这个任务运行失败,而且这个过程无需人工参与,由Hadoop内部完成的
- 适合PB级以上海量数据的离线处理:可以实现上千服务器并发工作
缺点:
- 不擅长实时计算:MapReduce无法像Mysql一样在毫秒或秒级返回结果
- 不擅长流式计算:流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化,这是因为MapReduce自身的设计特点决定了数据源必须是静态的
- 不擅长DAG(有向图)计算):多个应用存在依赖关系,后一个应用程序的输入为前一个的输出,在这种情况下,MapReduce并不是不能做,而是使用后每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常地下
核心思想
MapReduce的核心思想
- MapReduce运算程序一般需要分成2个阶段Map阶段和Reduce阶段
- Map阶段的并发MapTask,完全并行运行,互不相干
- Reduce阶段的并发ReduiceTask,完全不相干但是它们的数据依赖上一个阶段的所有MapTask并发实例输出
- MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序串行执行
工作进程
一个完整的MapReduce程序在分布式运行时有三类实例进程
- MrAppMaster:负责整个程序的过程调度及状态协调
- MapTask:负责Map阶段的整个数据处理流程
- ReduceTask:负责Reduce阶段的整个数据处理流程
数据序列化类型
java中数据类型对于Hadoop中数据序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
folat | FolatWritable |
long | LongWritable |
double | DoubleWritable |
String | TextWritable |
map | MapWritable |
array | ArrayWritable |
编码规范
编写MapReduce程序,需要按照hadoop提供的一套固定的编码规范
Mapper阶段
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper的业务逻辑写在map()方法中
- Mapper的输出数据的KV对的形式(KV的类型可自定义)
- map()方法(MapTask进程)对每一个<K,V>调用一次
Reducer阶段
- 用户自定义的Reducer要继承自己的父类
- Reducer的输入类型对应Mapper的输出类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- ReduceTask进程对每一组相同k的<K,V>组调用一次reduce()方法
Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序刀YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
实现单词统计
在之前都是使用官方案例完成单词统计的,现在我们根据Hadoop的规范自己别一个单词统计的案例
本文章所有案例仓库:https://gitee.com/smallpage/big-data-demo.git
实例代码
pom文件
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
编写Map类
根据编码规范Hadoop程序最新执行的是Map类,创建一个WordCountMapper继承Mapper类
编写Reducer类
根据编码规范Hadoop程序Map阶段完毕后会执行Reducer,创建一个WordCountReducer继承Reducer类
编写Driver类
Driver是整个MapReducer程序的加载器,它负责创建一个job并且提交
测试
编写Map、Reduce、Driver后可以执行main方法进行本地测试,传入输入输出路径
可以看到程序成功执行,并且在指定的输出位置生成了相应的文件
发布到Hadoop集群
修改pom
在pom文件中添加 如下打包参数
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>top.jolyoulu.wordcount.WordCountDriver</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包完毕会看到有2个jar包,with-dependencies结尾是附带着hadoop依赖的可以直接运行,因为我们在hadoop集群是带有hadoop依赖的所有只需要使用不带with-dependencies包即可
上传jar包
将jar包上传到hadoop集群中
执行程序
在hdfs的/test文件夹下准备一个需要测试的文件,如果执行如下命令对该文件的单词进行统计
hadoop jar word-count1-1.0-SNAPSHOT.jar top.jolyoulu.wordcount.WordCountDriver /test/t1.txt /test/t1_out.txt
执行完毕后将结果文件下载后可以看到,统计结果
Hadoop序列化
由于Java自带的序列化Serializable,对象序列化后附带信息较多不便于高效的在网络中传输,所有Hadoop自己实现了一套序列化机制(Writale)
特点
- 紧凑:高效利用存储空间
- 快速:读写数据的额外开销小
- 可扩展:随着通信协议的升级而升级
- 互操作:支持多语言的交互
自定义序列化
在前面讲到的Hadoop提供的数据序列化类型,在一些复杂业务上是无法满足需求的,Hadoop提供了自定义的序列化类型,分为如下步骤
实现Writable接口
生成空参构造,反序列化时,需要反射调用空参构造函数
重写序列化方法
重写反序列化方法
要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用
如果需要将自定义的bean对象放到key中传输,则需要实现Comparable接口,因为MapReduce中的Shuffle过程要对key进行排序
注意:序列化与放序列化的顺序需要完全一致
//1.实现Writable接口
public class FlowBean implements Writable {
private long upFlow; //上行流量
private long downFlow; //下行流量
private long sumFlow; //总流量
//2.生成空参构造
public FlowBean() {
}
//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.重写toString()
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
//以下省略get/set方法
}
自定义序列化案例
以下是利用自定义序列化对,指定的手机下载与上传流量进行分组合并统计,统计的数据格式如下
编写Map类
Map接收每一行的字符串数据,进行切割提取转换为自定义的FlowBean对象,key为手机号码
编写Reducer类
Reducer阶段对所有的key相同的FlowBean的上传与下载流量进行合计最后输出结果
编写Driver类
测试
测试结果如下
MapReduce原理
InputFormat数据输入
FileInputFormat切片
FileInputFormat是Hadoop的默认切片机制,即对文件流进行切割
切片过程
在Hadoop执行的前期阶段会生成相应的job,在生成job时就会有
FileInputFormat#getSplits
对数据进行切片,切片的过程如下
先找到难度数据存储目录
开始遍历整个目录的每一个文件,进行规划切片
遍历每个文件时的工作如下
获取文件大小
fs.szieOf()
计算切片大小
computeSplitSize(blockSize, minSize, maxSize)
核心代码:
Math.max(minSize, Math.min(maxSize, blockSize))
minSize = 1
maxSize = mapreduce.input.fileinputformat.split.maxsize 配置的大小
blockSize= hdfs中的block大小 (默认情况下的值)
开始切片,形成第1个切片:xxx.txt—0:128M、形成第2个切片:xxx.txt—128:256M、形成第3个切片:xxx.txt—256:300M
每次进行切片时都会判断当前剩余的快大小是否大于切片大小的1.1倍,大于就进行切
将切片信息写到一个切片规划文件中
整个切片的核心过程在getSplit()方法中完成
InputSplit只记录切片的源数据信息,比如起始位置、长度以及所在的节点列表等
提交到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
切片机制
- 简单按照文件的内容长度进行切片
- 切片大小,默认等于block大小
- 切片不考虑数据集整体,而是逐个针对每个文件单独切片
切片参数
从源码中可以得到切片的计算公式如下
Math.max(minSize, Math.min(maxSize, blockSize))
minSize = mapreduce.input.fileinputformat.split.minsize 配置的大小
maxSize = mapreduce.input.fileinputformat.split.maxsize 配置的大小
blockSize= hdfs中的block大小 (默认情况下的值)
参数控制
可以通过调整maxSize 和 minSize 控制切片规则
maxSize(切片最大值):参数如果设置的比blockSize小,则会使用maxSize作为切片大小
minSize(切片最小值):参数如果设置的比blockSize大,则会使用minSize作为切片大小
获取切片信息api
//根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
//获取切片的文件名称
inputSplit.getPath().getName();
CombineTextInputFormat切片
框架默认使用的是TextInputFormat按文件规划切片,不管文件大小都会单独切一个片提交给MapTask,如果有大量的小文件就会产生大量的MapTask,处理效率急剧下降
应用场景
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件逻辑上规划到一个切片中,这样多个小文件就可以一起交给一个MapTask处理
参数设置
设置4M
CombineTextInputFormat.setMaxInputSlitSize(job,1024 * 1024 * 4);
虚拟存储切片最大设置最后根据实际的小文件大小情况来设置
切片机制
加入setMaxInputSlitSize值为4M,当前有4个文件小文件,切片流程如下
- 首先会进行虚拟存储
- 如 文件<4M,形成一个切片
- 如 4M<文件<4M * 2,将文件/2,形成2个切片
- 如 文件>4M * 2,文件划分出一块4M大小做虚拟存储,剩余的文件继续继续判断划分直到划分完毕为止
- 将切换的块依次从上往下合并,直到>4M就生成一个切片
最终4个小文件被切成4个切片
实现过程
如果需要修改InputFormat只需要在Driver类中添加如下代码即可
//如果不设置InputFormat默认使用的是TextInputFormat.class
job.setInputFormatClass(CombineFileInputFormat.class);
//虚拟存储切片最大值设置4M,一般根据实际环境设置通常设置128M
CombineFileInputFormat.setMaxInputSplitSize(job,1024 * 1024 * 4);
FileInputFormat实现类
在运行MapReduce程序时,输入的文本格式可以为日志文件、二进制文件、数据库表等针对不同的文件有不同的FileInputFormat实现类实现读取逻辑
实现类
类名 | 说明 |
---|---|
TextInputFormat | 纯文本文件处理,以换行符做为切割拆分读取 |
KeyValueTextInputFormat | 纯文本文件处理,以换行符做为切割,行由分隔符字节分为键和值部分,如果不存在这样的字节,则键将是整行,值将为空 |
NLineInputFormat | 根据行数切片,按照设定n继续数据切片 |
CombineTextInputFormat | 对小文件继续合并 |
自定义InputFormat | 自己实现InputFormat |
TextInputFormat
TextInputFormat是默认的FileInputFormat实现类,按行读取每条记录读取完毕后形成的KV内容如下
key:存储该行在整个文件中的起始偏移量,LongWritable类型
value:存储该行的内容,不保存终止符(换行符或回车符),Text类型_
KeyValueTextInputFormat
KeyValueTextInputFormat每一行一条记录,利用分隔符分割key与value,可以通过在驱动添加如下配置
//设置用空格作为间隔符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR," ");
//使用KV格式化
job.setInputFormatClass(KeyValueTextInputFormat.class);
NLineInputFormat
NLineInputFormat代表每个map进程处理的InputSplit不再按Block块去划分,而是按NLineInputFormat指定的行数N来划分,即输入文件的总数/N=切片数,如果不整除则切片数=商+1
注意NLineInputFormat与CombineTextInputFormat控制的是切片规则
//设置切片 与 2行1个切片规则
job.setInputFormatClass(NLineInputFormat.class);
NLineInputFormat.setNumLinesPerSplit(job,2);
自定义InputFormat
Hadoop也提供了自定义的InputFormat可以根据特定情况编写特定是切片规则
自定义InputFormat步骤如下
- 自定义继承FileInputFormat
- 改写RecordReader,实现一次性读取一个完整文件封装成KV
- 再输出时使用SequenceFileOutPutFormat输出合并文件
案例代码
在前面介绍过合并小文件可以使用CombineTextInputFormat,我们也可以通过自定义的InputFormat来实现一个小文件合并,将多个小文件合并成一个SequenceFile文件,SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件个数,SequenceFile里面保存着多个文件,存储形式为文件路径+名称key,文件内容是value
需求:当前有3个文件one.txt、two.txt、three.txt,因为3个都是小文件希望将它们合并到一个文件内容里面去,key为文件路径,value为文件内容
实现步骤
- 继承一个FileInputFormat
- 重写isSplitable()方法,返回false不可切割
- 重写creatRecordReader(),创建自定义的RecordReader对象,并初始化
- 改写RecordReader,实现一次读取一个完整文件封装为KV
- 采用IO流一次读取一个文件输出到value中,因为设置了不可切片,最终把所有文件都封装到value中
- 获取文件路径信息+名称,并设置key
- 设置Driver
- job.setInputFormatClass(自定义的class)
- job.setOutputFormatClass(自定义的class)
WholeFileInputFormat
编写WholeFileInputFormat继承FileInputFormat实现createRecordReader方法,在读取到一个切片时最先会触发该方法,该方法需要返回一个RecordReader对象,Mapper通过读取不断的读取该对象中的key,value触发map方法
WholeRecordReader
WholeRecordReader是一个非常重要的类,因为分片信息在读取后会给到该对象,然后该对象会返回给Mapper,Mapper通过不断的调用nextKeyValue()方法获取key与value以及,该方法返回的boolean也是判断该分片的map阶段是否已经完成的依据
由于我们是把整个小文件的数据以key与value返回所有key是文件的路径,value是整个分片文件的内容,只需要读取一次所有首先nextKeyValue()方法返回true,接下来就设置false表示该分片已经全部读取完毕了
SequenceFileMapper
SequenceFileMapper继承Mapper,接收key与value参数与WholeRecordReader一致
由于WholeRecordReader中一个分片只会有一个key和value所有map阶段不用处理直接将内容输出给reducer阶段
SequenceFileReducer
SequenceFileReducer继承Reducer,接收key与value参数与SequenceFileMapper输出一致
在Reducer要将3个Map的内容全部合并到一个文件中,所有需要遍历所有values并且输出即可
SequenceDriver
最后编写驱动类,驱动类基本结构与之前的一样就是需要多添加2个配置
设置inputFormat和outputFormat
运行测试
准备3个小文件,因为这样可以确保每个文件不会大于128M,即每个小文件都会被当做一个切片进行
运行测试后可以看到输出结果文件中保存形式,key为文件路径,value为文件内容,这样就将多个小文件合并到一个文件中,当下次需要读取时只需要读取该结果文件就可以获取到3个文件的地址以及数据
Shuffle机制
Map方法后,Reduce方法之前的数据处理过程称为Shuffle
Partition分区
在一些业务下会需要将统计结果按照条件输出到不同的文件中(分区),比如:根据商品不同分类输出到不同文件中,默认情况下Map后的数据会根据key的hashcdoe对reduceTask大个数进行取模得到分区,用户无法控制那个key对应那个分区
自定义Partition
默认的分区是根据hash进行分区的,如果需要根据自己所需进行分区那么只需要
- 继承Partitioner重写getPartition方法
- 在Job驱动中设置自定义的Partition
- 自定义Partition后,要根据自定义的Partition的逻辑设置相应数量的ReduceTask
需求:在Hadoop序列化章节中我们做过对手机号码合计流量的案例,下面我们在那个案例的基础上啊对手机号码开头3位进行分类,分别将154,155,156开头的手机分3个放假输出,其余手机放到相同文件中
CustomPartitioner
继承Partitioner重写getPartition方法并且根据需求返回不同的numPartitions,就可以输出到指定的Partitioner文件中
FlowCountDriver
修改驱动类,在原有的基础上关联上自定义的Partitioner类以及设置ReduceTasks数量,ReduceTasks数量要和你要输出的分区总数一致
测试
执行测试后可以看到输出结果被拆分成多个分区,并且手机号码根据自定义的分区规则都被存放到指定的文件下