MapReduce 思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
1、Map 负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
2、Reduce 负责“合”,即对 map 阶段的结果进行全局汇总。
3、MapReduce 运行在 yarn 集群
这两个阶段合起来正是 MapReduce 思想的体现。
还有一个比较形象的语言解释 MapReduce:
我们要数图书馆中的所有书。你数 1 号书架,我数 2 号书架。这就是“Map”。我们人越多,数书就更快。现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。
MapReduce 是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在 Hadoop集群上。
既然是做计算的框架,那么表现形式就是有个输入(input),MapReduce 操作这个输入(input),通过本身定义好的计算模型,得到一个输出(output)。对许多开发者来说,自己完完全全实现一个并行计算程序难度太大,而MapReduce 就是一种简化并行计算的编程模型,降低了开发并行应用的入门门槛。
Hadoop MapReduce 构思体现在如下的三个方面:
1、如何对付大数据处理:分而治之
对相互间不具有计算依赖关系的大数据,实现并行最自然的办法就是采取分而治之的策略。并行计算的第一个重要问题是如何划分计算任务或者计算数据以便对划分的子任务或数据块同时进行计算。不可分拆的计算任务或相互间有依赖关系的数据无法进行并行计算!
2、构建抽象模型:Map 和 Reduce
MapReduce 借鉴了函数式语言中的思想,用 Map 和 Reduce 两个函数提供了高层的并行编程抽象模型。
Map: 对一组数据元素进行某种重复式的处理;
Reduce: 对 Map 的中间结果进行某种进一步的结果整理。
MapReduce 中定义了如下的 Map 和 Reduce 两个抽象的编程接口,由用户去编程实现:
Map 和 Reduce 为程序员提供了一个清晰的操作接口抽象描述。通过以上两个编程接口,大家可以看出 MapReduce 处理的数据类型是<key,value>键值对。
3、MapReduce 框架结构 一个完整的 mapreduce 程序在分布式运行时有三类实例进程:
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为 2 个步骤,Shuffle 阶段
4 个步骤,Reduce 阶段分为 2 个步骤
1、Map 阶段 2 个步骤:
2、Shuffle 阶段 4 个步骤:
3、Reduce 阶段 2 个步骤:
为了开发我们的 MapReduce 程序,一共可以分为以上八个步骤,其中每个步骤都是一个 class 类,我们通过 job 对象将我们的程序组装成一个任务提交即可。为了简化我们的 MapReduce 程序的开发,每一个步骤的 class 类,都有一个既定的父类,让我们直接继承即可,因此可以大大简化我们的 MapReduce 程序的开发难度,也可以让我们快速的实现功能开发。
MapReduce 编程当中,其中最重要的两个步骤就是我们的 Mapper 类和 Reducer类
Mapper 抽象类的基本介绍
在 hadoop2.x 当中 Mapper 类是一个抽象类,我们只需要覆写一个 java 类,继承自 Mapper 类即可,然后重写里面的一些方法,就可以实现我们特定的功能,接下来我们来介绍一下 Mapper 类当中比较重要的四个方法
Reducer 抽象类基本介绍
同样的道理,在我们的 hadoop2.x 当中,reducer 类也是一个抽象类,抽象类允许我们可以继承这个抽象类之后,重新覆写抽象类当中的方法,实现我们的逻辑的自定义控制。接下来我们也来介绍一下 Reducer 抽象类当中的四个抽象方法
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
node01 服务器执行以下命令,准备数,数据格式准备如下
cd /export/servers
vim wordcount.txt
#添加以下内容:
hello hello
world world
hadoop hadoop
hello world
hello flume
hadoop hive
hive kafka
flume storm
hive oozie
将数据文件上传到 hdfs 上面去
hdfs dfs -mkdir /wordcount/
hdfs dfs -put wordcount.txt /wordcount/
1、定义一个 mapper 类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
// mapper 程序: 需要继承 mapper 类, 需要传入 四个类型:
/* 在 hadoop 中, 对 java 的类型都进行包装, 以提高传输的效率 writable
keyin : k1 Long ---- LongWritable
valin : v1 String ------ Text
keyout : k2 String ------- Text
valout : v2 Long -------LongWritable
*/
public class MapTask extends Mapper<LongWritable,Text,Text,LongWritable> {
/**
*
* @param key : k1
* @param value v1
* @param context 上下文对象 承上启下功能
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1. 获取 v1 中数据
String val = value.toString();
//2. 切割数据
String[] words = val.split(" ");
Text text = new Text();
LongWritable longWritable = new LongWritable(1);
//3. 遍历循环, 发给 reduce
for (String word : words) {
text.set(word);
context.write(text,longWritable);
}
}
}
2、定义一个 reducer 类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* KEYIN : k2 -----Text
* VALUEIN : v2 ------LongWritable
* KEYOUT : k3 ------ Text
* VALUEOUT : v3 ------ LongWritable
*/
public class ReducerTask extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context)throws IOException, InterruptedException {
//1. 遍历 values 获取每一个值
long v3 = 0;
for (LongWritable longWritable : values) {
v3 += longWritable.get(); //1
}
//2. 输出
context.write(key,new LongWritable(v3));
}
}
3、定义一个主类,用来描述 job 并提交 job
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.nativeio.NativeIO;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
// 任务的执行入口: 将八步组合在一起
public class JobMain extends Configured implements Tool {
// 在 run 方法中编写组装八步
@Override
public int run(String[] args) throws Exception {
Job job = Job.getInstance(super.getConf(), "JobMain");
//如果提交到集群操作. 需要添加一步 : 指定入口类
job.setJarByClass(JobMain.class);
//1. 封装第一步: 读取数据
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("hdfs://node01:8020/wordcount.txt
"));
//2. 封装第二步: 自定义 map 程序
job.setMapperClass(MapTask.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//3. 第三步 第四步 第五步 第六步 省略
//4. 第七步: 自定义 reduce 程序
job.setReducerClass(ReducerTask.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//5) 第八步 : 输出路径是一个目录, 而且这个目录必须不存在的
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path("hdfs://node01:8020/output"));
//6) 提交任务:
boolean flag = job.waitForCompletion(true); // 成功 true 不成功 false
return flag ? 0 : 1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
JobMain jobMain = new JobMain();
int i = ToolRunner.run(configuration, jobMain, args); //返回值 退出码
System.exit(i); // 退出程序 0 表示正常 其他值表示有异常 1
}
}
提醒:代码开发完成之后,就可以打成 jar 包放到服务器上面去运行了,实际工作当中,都是将代码打成 jar 包,开发 main 方法作为程序的入口,然后放到集群上面去运行
本地运行模式
TextInputFormat.addInputPath(job,new Path(“file:///D:\wordcount\input”));
TextOutputFormat.setOutputPath(job,new Path(“file:///D:\wordcount\output”));
集群运行模式
整个 Map 阶段流程大体如上图所示。
简单概述:
inputFile 通过 split 被逻辑切分为多个 split 文件,通过 Record按行读取内容给 map(用户自己实现的)进行处理,数据被 map 处理结束之后交给 OutputCollector 收集器,对其结果 key 进行分区(默认使用 hash 分区),然后写入 buffer,每个 map task 都有一个内存缓冲区(环形缓冲区),存储着 map 的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个 map task 结束后再对磁盘中这个 map task 产生的所有临时文件做合并,生成最终的正式输出文件,然后等待 reduce task 来拉数据
总结:加载文件,文件切割,按行读取,map拆分转换,map结果输出mapTask的环形缓冲区(100m)(临时存放),80m触发线程溢写到磁盘,在此期间会进行 排序,预combiner(可选)(结果预合并,减少写磁盘数据量),溢写文件聚合
详细步骤
读取数据组件 InputFormat (默认 TextInputFormat) 会通过 getSplits 方法
对输入目录中文件进行逻辑切片规划得到 block, 有多少个 block 就对应启动
多少个 MapTask
将输入文件切分为 block 之后, 由 RecordReader 对象 (默认是LineRecordReader) 进行读取, 以 \n 作为分隔符, 读取一行数据, 返回 <key,value>. Key 表示每行首字符偏移值, Value 表示这一行文本内容
读取 block 返回 <key,value>, 进入用户自己继承的 Mapper 类中,执行用户重写的 map 函数, RecordReader 读取一行这里调用一次
Mapper 逻辑结束之后, 将 Mapper 的每条结果通过 context.write 进行collect 数据收集. 在 collect 中, 会先对其进行分区处理。
默认使用=HashPartitionerMapReduce 提供 Partitioner 接口, 它的作用就是根据 Key 或 Value 及Reducer 的数量来决定当前的这对输出数据最终应该交由哪个 Reduce task 处理, 默认对 Key Hash 后再以 Reducer 数量取模. 默认的取模方式只是为了平均Reducer 的处理能力, 如果用户自己对 Partitioner 有需求, 可以订制并设置到Job 上
接下来, 会将数据写入内存, 内存中这片区域叫做环形缓冲区, 缓冲区的作用是批量收集 Mapper 结果, 减少磁盘 IO 的影响. 我们的 Key/Value 对以及Partition 的结果都会被写入缓冲区. 当然, 写入之前,Key 与 Value 值都会被序列化成字节数组
环形缓冲区(其实是一个数组, 数组中存放着 Key, Value 的序列化数据和Key,Value 的元数据信息, 包括 Partition, Key 的起始位置, Value 的起始位置以及Value 的长度. 环形结构是一个抽象概念)。
缓冲区是有大小限制, 默认是 100MB. 当 Mapper 的输出结果很多时, 就可能会撑爆内存, 所以需要在一定条件下将缓冲区中的数据临时写入磁盘, 然后重新利用这块缓冲区. 这个从内存往磁盘写数据的过程被称为 Spill, 中文可译为溢写. 这个溢写是由单独线程来完成, 不影响往缓冲区写 Mapper 结果的线程. 溢写线程启动时不应该阻止 Mapper 的结果输出, 所以整个缓冲区有个溢写的比例spill.percent. 这个比例默认是0.8, 也就是当缓冲区的数据已经达到阈值buffer size * spill percent = 100MB * 0.8 = 80MB, 溢写线程启动, 锁定这 80MB的内存, 执行溢写过程. Mapper 的输出结果还可以往剩下的 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 执行效率有帮助, 反之会影响 Reducer 的最终结果
合并溢写文件, 每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有Combiner), 如果 Mapper 的输出结果真的很大, 有多次这样的溢写发生, 磁盘上相应的就会有多个临时文件存在. 当整个数据处理结束之后开始对磁盘中的临时文件进行 Merge 合并, 因为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件, 以记录每个 reduce 对应数据的偏移量
mapTask 的一些基础设置配置:
Reduce 大致分为 copy、sort、reduce 三个阶段,重点在前两个阶段。
copy 阶段
包含一个 eventFetcher 来获取已完成的 map 列表,由 Fetcher 线程去copy 数据,在此过程中会启动两个 merge 线程,分别为 inMemoryMerger 和onDiskMerger,分别将内存中的数据 merge 到磁盘和将磁盘中的数据进行合并merge。
待数据 copy 完成之后,copy 阶段就完成了,开始进行 sort 阶段,sort阶段主要是执行 finalMerge 操作,纯粹的 sort 阶段,完成之后就是 reduce阶段,调用用户定义的 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 文件中。
map 阶段处理的数据如何传递给 reduce 阶段,是 MapReduce 框架中最关键的一个流程,这个流程就叫 shuffle
shuffle: 洗牌、发牌 ——(核心机制:数据分区,排序,分组,规约,合并等
过程)
shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce阶段。一般把从 Map 产生输出开始到 Reduce 取得数据作为输入之前的过程称作 shuffle。
处理流程:
Collect 阶段:将 MapTask 的结果输出到默认大小为 100M 的环形缓冲区,保存的是 key/value,Partition 分区信息等。
Spill 阶段:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了 combiner,还会将有相同分区号和 key 的数据进行排序。
Merge 阶段:把所有溢出的临时文件进行一次合并操作,以确保一个 MapTask 最终只产生一个中间数据文件。
Copy 阶段:ReduceTask 启动 Fetcher 线程到已经完成 MapTask 的节点上复制一份属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达
到一定的阀值的时候,就会将数据写到磁盘之上。
Merge 阶段:在 ReduceTask 远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作。
Sort 阶段:在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对数据进行了局部的排序,ReduceTask 只需保证 Copy 的数据的最终整体有效性即可。
Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓
冲区越大,磁盘 io 的次数越少,执行速度就越快
缓冲区的大小可以通过参数调整, 参数:mapreduce.task.io.sort.mb 默认 100M
转载自:五分钟学大数据