Hadoop-MapReduce
概念
MapReduce 是一个分布式计算引擎,采用分而治之的思想,将数据的处理过程拆分成Map跟Reduce两个操作函数,解决了人们在最初面临海量数据束手无策的问题。
特点与局限性
- 特点:易于编程;扩展性;高容错性;适合海量数据的离线处理
- 局限性:实时计算性能差;不能进行流式计算
思想
- MapReduce分成两个大阶段,分别是Map阶段(分)和Reduce阶段(合)
- MapReduce划分的小任务之间不能有依赖关系
- MapReduce整个的处理过程采用的键值对模型:(K1,V1),(K2,V2),(K2,[V2]),(K3,V3)
- MapRduce框架大部分大代码都开发完了,我们只需要填充一些指定的代码即可
- MapReduce只是一段API代码,它的运行所需要的内存、cpu都是由Yarn来分配
- MapReduce的API并没有使用Java的类型,而是自己封装了一套数据类型
架构规范
一个完整的MapReduce程序在运行时,有三类实例进程:MRAppMaster(程序调度协调)、MapTask(map阶段数据处理)、ReduceTask(reduce阶段的数据处理)
MapReduce 运算过程需要分成两个阶段,一个是Map阶段,一个是Reduce阶段,Map阶段对应的是MapTask,完全是并行执行,Reduce阶段对应的ReduceTask并发实例,需要依赖于Map阶段的数据处理结果。用户需要编写Mapper,Reducer,Driver三个部分,其中一个编程模型只能对应一个Map和Reduce,如需多个,需串行运行。在整个MapReduce的程序中,数据都是以KV键值对的方式来流转的。
执行流程
整个MapReduce的工作流程可以分为三个阶段:Map(对数据进行初步整理,整理成kv的形式),Shuffle(对数据进行分区,排序,分组的操作),Reduce(对shuffle的数据进行聚合处理)
序列化机制
序列化 (Serialization)是将结构化对象转换成字节流以便于进行网络传输或写入持久存储的过程。
反序列化(Deserialization)是将字节流转换为一系列结构化对象的过程,重新创建该对象。
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
Hadoop中没有使用java默认的序列化机制,而是实现自己独有的序列化制。Hadoop通过Writable接口实现的序列化机制,不过没有提供比较功能,所以和java中的Comparable接口合并,提供一个接口WritableComparable。(自定义比较)。
package org.apache.hadoop.io;
public interface Writable {
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}
数据类型:
Hadoop 数据类型 | Java****数据类型 | 备注 |
---|---|---|
BooleanWritable | boolean | 标准布尔型数值 |
ByteWritable | byte | 单字节数值 |
IntWritable | int | 整型数 |
FloatWritable | float | 浮点数 |
LongWritable | long | 长整型数 |
DoubleWritable | double | 双字节数值 |
Text | String | 使用UTF8格式存储的文本 |
MapWritable | map | 映射 |
ArrayWritable | array | 数组 |
NullWritable | null | 当<key,value>中的key或value为空时使用 |
入门案例
WordCount需求,即对单词进行统计
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itcast</groupId>
<artifactId>test-mapreduce</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass></mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写Mapper
public class WordCountMapper extends Mapper<LongWritable, Text,Text,LongWritable> {
//Mapper输出kv键值对 <单词,1>
private Text keyOut = new Text();
private final static LongWritable valueOut = new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//将读取的一行内容根据分隔符进行切割
String[] words = value.toString().split("\\s+");
//遍历单词数组
for (String word : words) {
keyOut.set(word);
//输出单词,并标记1
context.write(new Text(word),valueOut);
}
}
}
编写Reducer
public class WordCountReducer extends Reducer<Text, LongWritable,Text,LongWritable> {
private LongWritable result = new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
//统计变量
long count = 0;
//遍历一组数据,取出该组所有的value
for (LongWritable value : values) {
//所有的value累加 就是该单词的总次数
count +=value.get();
}
result.set(count);
//输出最终结果<单词,总次数>
context.write(key,result);
}
}
编写Driver
public class WordCountDriver_v1 {
public static void main(String[] args) throws Exception {
//配置文件对象
Configuration conf = new Configuration();
// 创建作业实例
Job job = Job.getInstance(conf, WordCountDriver_v1.class.getSimpleName());
// 设置作业驱动类
job.setJarByClass(WordCountDriver_v1.class);
// 设置作业mapper reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 设置作业mapper阶段输出key value数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//设置作业reducer阶段输出key value数据类型 也就是程序最终输出数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
// 配置作业的输入数据路径
FileInputFormat.addInputPath(job, new Path(args[0]));
// 配置作业的输出数据路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//判断输出路径是否存在 如果存在删除
FileSystem fs = FileSystem.get(conf);
if(fs.exists(new Path(args[1]))){
fs.delete(new Path(args[1]),true);
}
// 提交作业并等待执行完成
boolean resultFlag = job.waitForCompletion(true);
//程序退出
System.exit(resultFlag ? 0 :1);
}
}
程序运行
MapReduce的运行模式讲的是单机运行或者分布式运行,取决于如下参数:(如果不指定,默认是Local模式:mapred-default.xml
中有配置)
mapreduce.framework.name=yarn 集群模式
mapreduce.framework.name=local 本地模式
本地模式运行
mapreduce程序是被提交给LocalJobRunner在本地以单进程的形式运行。而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上
本质是程序的conf中是否有mapreduce.framework.name=local
本地模式非常便于进行业务逻辑的debug。
右键直接运行main方法所在的主类即可。
集群模式运行
将mapreduce程序提交给yarn集群,分发到很多的节点上并发执行。处理的数据和输出结果应该位于hdfs文件系统
提交集群的实现步骤:
将程序打成jar包,然后在集群的任意一个节点上用命令启动
hadoop jar wordcount.jar cn.itcast.bigdata.mrsimple.WordCountDriver args
yarn jar wordcount.jar cn.itcast.bigdata.mrsimple.WordCountDriver args
流程整理
输入输出
默认读取数据的组件叫做TextInputFormat,若读取一个文件,则以文件为主体,若读取一个文件夹,则将文件夹中所有文件为主体
默认输出的组件叫做TextOutputFotmat,输出的文件路径不能提前存在,否则会报错
执行流程
Map阶段
- 首先把输入目录下的文件进行逻辑切片,默认切片大小等于分块大小,即128M
- 对切片中的内容按照一定规则解析成kv 的键值对。默认规则是吧每一行的文本内容解析成键值对,其中,每一行的起始位置为key,单位是字节,本行的文本内容为value,通过TextInputFormat实现
- 针对解析出来的每一个kv对,分别调用编写Mapper中的map方法,输出一个新的键值对
Shuffle阶段
- 按照一定的规则对map输出的键值对进行分区,默认是只有一个区。分区的数量就是Reducer任务运行的数量。默认只有一个Reducer任务。
- 对分区中的键值对进行排序,先按照键进行排序,键相同的按照值进行排序
- 若有Compainer阶段,则通过此阶段对数据进行局部聚合处理。
Reduce阶段
- Reduce主动从Mapper任务复制输出的键值对,Reducer会复制多个Mapper的输出
- 把复制到的数据进行合并,然后对合并后的数据进行排序
- 对排序后的数据调用Reduce方法,最后把输出的键值对写入到HDFS中
编程模型
在默认的情况下,不管map阶段有几个并行的mapTask,到reduce阶段都只有一个reduceTask执行,可以通job提供的方法:job.setNumReduceTasks()
来指定reduceTask的个数。当MapReduce中有多个reducetask执行的时候,此时maptask的输出就会面临一个问题:究竟将自己的输出数据交给哪一个reducetask来处理,这就是所谓的数据分区(partition)问题。MapReduce默认分区规则是HashPartitioner。跟map输出的数据key有关。用户也可以自定分区规则,例如:
public class StatePartitioner extends Partitioner<Text, Text> {
public static HashMap<String, Integer> stateMap = new HashMap<String, Integer>();
static{
stateMap.put("Alabama", 0);
stateMap.put("Arkansas", 1);
stateMap.put("California", 2);
stateMap.put("Florida", 3);
stateMap.put("Indiana", 4);
}
@Override
public int getPartition(Text key, Text value, int numPartitions) {
Integer code = stateMap.get(key.toString());
if (code != null) {
return code;
}
return 5;
}
}
在MapReuce 的编程中,核心是牢牢把握住每个阶段的输入输出key是什么,因为mr中很多默认的行文都跟key相关。
并行度机制
Map的并行度机制
MapTask的并行度指的是map阶段有多少个并行的task共同处理任务;一个MapReducejob的map阶段并行度由客户端在提交job时决定,即客户端提交job之前会对待处理数据进行逻辑切片。切片完成会形成切片规划文件(job.split),每个逻辑切片最终对应启动一个maptask。
Reduce 的并行度机制
reducetask并行度同样影响整个job的执行并发度和执行效率,与maptask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置:job.setNumReduceTasks(4);
如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜。
工作流程详解
Map阶段
-
首先,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划得到splits,有多少个split就对应启动多少个MapTask。split与block的对应关系默认是一对一。
-
将输入文件切分为splits之后,由RecordReader对象(默认LineRecordReader)进行读取,以\n作为分隔符,读取一行数据,返回<key,value>。Key表示每行首字符偏移值,value表示这一行文本内容。
-
读取split返回<key,value>,进入用户自己继承的Mapper类中,执行用户重写的map函数。RecordReader读取一行这里调用一次。
Shuuffle阶段
- map逻辑完之后,将map的每条结果通过context.write进行collect数据收集。在collect中,会先对其进行分区处理,默认使用HashPartitioner。
MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。
- 接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
环形缓冲区其实是一个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,包括partition、key的起始位置、value的起始位置以及value的长度。环形结构是一个抽象概念。
缓冲区是有大小限制,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size \* spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写,互不影响。*
- 当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。如果job设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。
那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
- 每次溢写会在磁盘上生成一个临时文件(写之前判断是否有combiner),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行merge合并,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。 至此map整个阶段结束。
Reduce阶段
- Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
- Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
- 把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
- 对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
高阶部分
计数器
Hadoop内置的计数器功能收集作业的主要统计信息,可以帮助用户理解程序的运行情况,辅助用户诊断故障。
Hadoop内置计数器根据功能进行分组。每个组包括若干个不同的计数器,分别是:MapReduce任务计数器(Map-Reduce Framework)、文件系统计数器(File System Counters)、作业计数器(Job Counters)、输入文件任务计数器(File Input Format Counters)、输出文件计数器(File Output Format Counters)。
Map-Reduce Framework Counters
计数器名称 | 说明 |
---|---|
MAP_INPUT_RECORDS | 所有mapper已处理的输入记录数 |
MAP_OUTPUT_RECORDS | 所有mapper产生的输出记录数 |
MAP_OUTPUT_BYTES | 所有mapper产生的未经压缩的输出数据的字节数 |
MAP_OUTPUT_MATERIALIZED_BYTES | mapper输出后确实写到磁盘上字节数 |
COMBINE_INPUT_RECORDS | 所有combiner(如果有)已处理的输入记录数 |
COMBINE_OUTPUT_RECORDS | 所有combiner(如果有)已产生的输出记录数 |
REDUCE_INPUT_GROUPS | 所有reducer已处理分组的个数 |
REDUCE_INPUT_RECORDS | 所有reducer已经处理的输入记录的个数。每当某个reducer的迭代器读一个值时,该计数器的值增加 |
REDUCE_OUTPUT_RECORDS | 所有reducer输出记录数 |
REDUCE_SHUFFLE_BYTES | Shuffle时复制到reducer的字节数 |
SPILLED_RECORDS | 所有map和reduce任务溢出到磁盘的记录数 |
CPU_MILLISECONDS | 一个任务的总CPU时间,以毫秒为单位,可由/proc/cpuinfo获取 |
PHYSICAL_MEMORY_BYTES | 一个任务所用的物理内存,以字节数为单位,可由/proc/meminfo获取 |
VIRTUAL_MEMORY_BYTES | 一个任务所用虚拟内存的字节数,由/proc/meminfo获取 |
File System Counters Counters
计数器名称 | 说明 |
---|---|
BYTES_READ | 程序从文件系统中读取的字节数 |
BYTES_WRITTEN | 程序往文件系统中写入的字节数 |
READ_OPS | 文件系统中进行的读操作的数量(例如,open操作,filestatus操作) |
LARGE_READ_OPS | 文件系统中进行的大规模读操作的数量 |
WRITE_OPS | 文件系统中进行的写操作的数量(例如,create操作,append操作) |
Job Counters
计数器名称 | 说明 |
---|---|
Launched map tasks | 启动的map任务数,包括以“推测执行”方式启动的任务 |
Launched reduce tasks | 启动的reduce任务数,包括以“推测执行”方式启动的任务 |
Data-local map tasks | 与输人数据在同一节点上的map任务数 |
Total time spent by all maps in occupied slots (ms) | 所有map任务在占用的插槽中花费的总时间(毫秒) |
Total time spent by all reduces in occupied slots (ms) | 所有reduce任务在占用的插槽中花费的总时间(毫秒) |
Total time spent by all map tasks (ms) | 所有map task花费的时间 |
Total time spent by all reduce tasks (ms) | 所有reduce task花费的时间 |
File Input|Output Format Counters
计数器名称 | 说明 |
---|---|
读取的字节数(BYTES_READ) | 由map任务通过FilelnputFormat读取的字节数 |
写的字节数(BYTES_WRITTEN) | 由map任务(针对仅含map的作业)或者reduce任务通过FileOutputFormat写的字节数 |
Join 操作
整个的MapReduce 的join分为两类,分别是Map Side Join 和 Reduce Side
Map Side Join
在map阶段执行join 操作通过mapReduce 的分布式缓存来实现,处理流程如下:
1、首先分析join处理的数据集,使用分布式缓存技术将小的数据集进行分布式缓存
2、MapReduce框架在执行的时候会自动将缓存的数据分发到各个maptask运行的机器上
3、程序只运行mapper,在mapper初始化的时候从分布式缓存中读取小数据集数据,然后和自己读取的大数据集进行join关联,输出最终的结果。
4、整个join的过程没有shuffle,没有reducer。
Reduce Side Join
针对仅含map的作业)或者reduce任务通过FileOutputFormat写的字节数 |
Join 操作
整个的MapReduce 的join分为两类,分别是Map Side Join 和 Reduce Side
Map Side Join
在map阶段执行join 操作通过mapReduce 的分布式缓存来实现,处理流程如下:
1、首先分析join处理的数据集,使用分布式缓存技术将小的数据集进行分布式缓存
2、MapReduce框架在执行的时候会自动将缓存的数据分发到各个maptask运行的机器上
3、程序只运行mapper,在mapper初始化的时候从分布式缓存中读取小数据集数据,然后和自己读取的大数据集进行join关联,输出最终的结果。
4、整个join的过程没有shuffle,没有reducer。
Reduce Side Join
在reduce阶段执行join关联操作,通过shuffle就可以将相关的数据分到相同的分组中,但是reduce端join 容易出现数据倾斜的问题