目录
Mapper阶段----编写Mapper类,即MapTask任务
Reducer阶段----编写Reducer类,即ReduceTask任务
Driver阶段----关联MapTask和ReduceTask任务,并且提交运行
单词计数案例
需求
在一堆给定的文本文件中统计输出每一个单词出现的总次数
环境准备
在 /opt/test 目录下创建一个文件 wordcount.txt ,里面键入几个单词,并用空格分隔开
Java实现
package com.zyd;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: ZYD
* @Date: 2021/7/30 下午 21:28
*/
/**
* 如果使用Java实现这个单词计数存在的问题:
* 1. 操作的数据是HDFS分布式集群,可能这个文件有几个G的大小,如果使用Java,那么我们必须把这些数据全部拉取到JVM内存中去运行,可能会导致Java内存崩溃
* 2. 如果我们不把这些数据全部拉取到本地操作,那么可能会把这个Java程序运行在不同的电脑上执行,只需要处理当前机器上的block块的数据即可
* 但是这样也会存在问题:
* Java代码需要分布式的运行在不同的电脑上,那么处理结束之后将面临如何将处理结果汇总起来、如何把控每一个节点上程序有没有运行结束这些问题
*/
public class WordCountJava {
public static void main(String[] args) {
// 1. 连接HDFS集群
Configuration conf = new Configuration();
FileSystem fs = null;
try {
fs = FileSystem.get(new URI("hdfs://192.168.218.55:9000"), conf, "root");
/**
* 2. 读取文件数据 ----- IO流
* 四个抽象父类:
* 字节流:InputStream、OutputStream
* 字符流:Reader、Writer
*/
// 创建单词计数文件的输入流
FSDataInputStream open = fs.open(new Path("/test/wordcount.txt"));
// 字节转字符流
InputStreamReader isr = new InputStreamReader(open);
BufferedReader br = new BufferedReader(isr);
String line;
// 准备一个map集合存放数据,数据格式:单词,单词出现的次数
Map<String, Integer> map = new HashMap<>();
// 通过BufferedReader字符缓冲流的readline方法依次读取一行数据
// 将每一行数据按空格分隔,统计次数
/**
* 为什么要用(line = br.readLine()) != null 而不用下面的方法?
* 之所以使用(line = br.readLine()) != null判断,是因为readline()方法的特性:
* readline是用来判断当前行有没有数据,如果有数据的话,那么将这一行的数据赋值给一个String类型的变量,然后将这个指针下移
* 用下面的方法的话,意味着调用了两次readline方法,再调用这个readline方法的时候,他返回的就不是当前行的数据了,而是下一行的数据
* 此时也就代表 line 这个字符串里存储的是第二行的数据
*/
while (((line = br.readLine()) != null)) {
// while (br.readLine() != null) {
// line = br.readLine();
System.out.println(line);
String[] words = line.split(" ");
/**
* 如果单词还没有在map集合添加,name在map集合加入 word,1
* 如果出现,那么在map集合添加 word,以前的次数+1
*/
for (String word: words) {
if (map.get(word) == null) {
map.put(word, 1);
} else {
map.put(word, map.get(word) + 1);
}
}
}
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fs != null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Java操作注意事项
- (面试题)通过代码的编写可以发现,在写while循环时,使用了(line = br.readLine()) != null 判断,而不是while (br.readLine() != null) {line = br.readLine(); ……},这是为什么呢?
原因:之所以使用(line = br.readLine()) != null判断,是因为readline()方法的特性:
* readline是用来判断当前行有没有数据,如果有数据的话,那么将这一行的数据赋值给一个String类型的变量,然后将这个指针下移
* 用下面的方法的话,意味着调用了两次readline方法,再调用这个readline方法的时候,他返回的就不是当前行的数据了,而是下一行的数据
* 此时也就代表 line 这个字符串里存储的是第二行的数据
- 使用Java实现这个单词计数存在的问题:
- 操作的数据是HDFS分布式集群,可能这个文件有几个G的大小,如果使用Java,那么我们必须把这些数据全部拉取到JVM内存中去运行,可能会导致Java内存崩溃
- 如果我们不把这些数据全部拉取到本地操作,那么可能会把这个Java程序运行在不同的电脑上执行,只需要处理当前机器上的block块的数据即可。但是这样也会存在问题:Java代码需要分布式的运行在不同的电脑上,那么处理结束之后将面临如何将处理结果汇总起来、如何把控每一个节点上程序有没有运行结束这些问题
MapReduce实现
编写MR程序过程:
- 编写Mapper阶段:继承Mapper类,定义好数据的输入和输出类型
- 编写Reducer阶段:继承Reducer类,定义好数据的输入和输出类型
- 编写Driver类:程序的运行入口,将Mapper阶段和Reducer阶段关联圈起来,并且定义输入文件和输出文件地址
源代码:
1. WordCountMapper.java
package MapReduce;
/**
* @Author: ZYD
* @Date: 2021/8/1 下午 22:44
*/
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* 第一步:继承Mapper类(定义数据的输入格式和数据的输出形式)
* 如果要使用MR程序,我们要指定数据输入的kye-value键值对类型
* 同时要指定数据输出的key-value键值对类型
* Mapper阶段----一行数据执行一次Mapper
* Mapper阶段的输入key-value键值对格式很固定:LongWritable Text
* 其中LongWritable 是 long 基本数据类型的hadoop序列化的类,一般情况下Map阶段的key代表的是文件的偏移量----理解为行号
* Text 是String类型的hadoop序列化类,代表的是每一行的数据
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
/**
* mapTask的核心处理逻辑:
* 每一行文件数据需要走一个map方法
* @param key ----- Mapper阶段输入的key值-----行号
* @param value -----Mapper阶段输入的value值-----当前行对应的字符串数据
* @param context ------上下文对象,主要功能是为了实现将Map阶段的数据输入到Reduce阶段
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 第一步:将每一行数据value按照空格切割,然后将每一个得到的单词当做 key,1当做value,将他们输出到Reduce阶段
String line = value.toString();
String[] words = line.split(" ");
for (String word: words) {
// 将每一行数据空格切割后的单词以单词为key,1为value输出到reduce阶段
// 到了reduce阶段,会根据key值将value给聚合起来
context.write(new Text(word), new LongWritable(1));
}
}
}
2. WordCountReducer.java
package MapReduce;
/**
* @Author: ZYD
* @Date: 2021/8/1 下午 23:01
*/
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.Iterator;
/**
* 继承Reducer类
* Reducer类也粗腰指定输入的key-value的数据类型,也需要指定输出的key-value数据类型
*
* 此时Reducer阶段的输入的key-value键值对类型不能随便指定,它应该是Mapper阶段的输出数据类型
*/
public class WordCountReduce extends Reducer<Text, LongWritable, Text, LongWritable> {
/**
* reduceTask核心处理业务逻辑的方法
* 他是每一组key值相同的数据执行一次
* @param key ------ map阶段输出的key值 ---- 单词
* @param values ------ values是一个类似于集合的数据,里面放的是key相同的数据的所有value值的集合
* @param context ------ 上下文对象,将结果以<key, value>键值对的形式输出到最终的结果文件中
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
/**
* 思路:将values中的数据累加起来 ,这样的话单词对应出现的次数就确定了
*/
Iterator<LongWritable> iterator = values.iterator();
long result = 0L;
while (iterator.hasNext()) {
LongWritable next = iterator.next();
result += next.get();
}
context.write(key, new LongWritable(result));
}
}
3. WordCountDriver.java
package MapReduce;
/**
* @Author: ZYD
* @Date: 2021/8/1 下午 23:01
*/
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.Iterator;
/**
* 继承Reducer类
* Reducer类也粗腰指定输入的key-value的数据类型,也需要指定输出的key-value数据类型
*
* 此时Reducer阶段的输入的key-value键值对类型不能随便指定,它应该是Mapper阶段的输出数据类型
*/
public class WordCountReduce extends Reducer<Text, LongWritable, Text, LongWritable> {
/**
* reduceTask核心处理业务逻辑的方法
* 他是每一组key值相同的数据执行一次
* @param key ------ map阶段输出的key值 ---- 单词
* @param values ------ values是一个类似于集合的数据,里面放的是key相同的数据的所有value值的集合
* @param context ------ 上下文对象,将结果以<key, value>键值对的形式输出到最终的结果文件中
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
/**
* 思路:将values中的数据累加起来 ,这样的话单词对应出现的次数就确定了
*/
Iterator<LongWritable> iterator = values.iterator();
long result = 0L;
while (iterator.hasNext()) {
LongWritable next = iterator.next();
result += next.get();
}
context.write(key, new LongWritable(result));
}
}
为什么要使用MapReduce
- 海量数据在单机上处理因为硬件资源限制,无法胜任
- 而一旦将单机版程序(普通程序)扩展到集群在分布式运行,将极大增加程序的复杂度和开发难度
- 引入MapReduce框架(本身就是分布式)后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理
分布式方案考虑的问题:
- 运行逻辑要不要先分后合(先把代码运行在不同的服务器上,最终将结果合并起来)
- 程序如何分配运算任务(切片)
- 两个阶段的程序如何启动?如何协调?
- 整个程序运行过程中的监控?容错?重试?
分布式方案需要考虑很多问题,但是我们可以将分布式程序中的公共功能封装成框架,让开发人员将精力集中在业务逻辑上,而MapReduce就是这样一个分布式程序的通用框架
MapReduce核心思想-----分而治之,先分后合
分布式的运算程序需要分成至少两个阶段:
- 将程序分布在不同的节点(电脑)上,将不同节点的程序相互运算出结果(完全并行运行),互不相干 -------- maptask阶段
- 并行运行,这一阶段数据需要依赖于上一阶段的并发实例的输出(即maptask阶段全部执行完毕才可以开始这一阶段)-------- reducetask阶段'
MapReduce编程模型只能包含一个map阶段和一个reduce阶段,如果用户业务逻辑非常复杂,那就只能使用多个MapReduce程序串行运行
Notes:Reduce Task的个数由业务来决定'
MapReduce进程
一个完整的mapreduce程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度及状态协调(YARN),是程序的管理者,管理Map和Reduce任务
- MapTask:负责map阶段的整个数据处理流程(只分,不合),将程序运行在不同节点上,每个节点只负责一部分数据, 并行运行,互不干扰
- ReduceTask:负责reduce阶段的整个数据处理流程,将Mapper阶段处理完成的数据合并起来处理,也可以有多个reduce,并且多个reduce之间是并行运行,互不干扰的,但是reduce的执行需要依赖map阶段的数据
MapReduce编程规范(八股文)
用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)------三步编程法
Mapper阶段----编写Mapper类,即MapTask任务
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper中的业务逻辑写在map()方法中
- Mapper的输出数据是KV对的形式(KV的类型可自定义)
- map()方法(maptask进程)对每一个<K,V>调用一次(即一行调用一次map方法)
Reducer阶段----编写Reducer类,即ReduceTask任务
- 用户自定义的Reducer要继承自己的父类
- Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法(一个key调用一次reduce方法)
Driver阶段----关联MapTask和ReduceTask任务,并且提交运行
整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象
Notes:
- MapReduce程序处理的数据大部分都是HDFS上的文件数据
- Map阶段和Reduce阶段数据都需要通过key-value键值对形式进行输入和输出
- 一般情况下Map阶段的输出的key-value的键值对就是Reducer阶段输出的key-value键值对
- 一般情况下一个MapReduce程序只能有一个Map阶段和一个Reduce阶段,如果程序复杂,需要编写多个MR程序串行运行
MapReduce程序运行流程
首先将存储在文件中的数据分片(假设分为三片),之后三个maptask同时作业,当所有maptask任务完成后,启动相应数量的ReduceTask,并告知ReduceTask处理数据的范围(数据分区)
1) 在MapReduce程序读取文件的输入目录上存放相应的文件。客户端程序在submit()方法执行前,获取待处理的数据信息,然后根据集群中参数的配置形成一个任务分配规划。
2) 客户端提交job.split、jar包、job.xml等文件给yarn,yarn中的resourcemanager启动MRAppMaster。
3) MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程。
4) maptask利用客户指定的inputformat来读取数据,形成输入KV对。
5) maptask将输入KV对传递给客户定义的map()方法,做逻辑运算
6) map()运算完毕后将KV对收集到maptask缓存。
7) maptask缓存中的KV对按照K分区排序后不断写到磁盘文件
8) MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据分区。
9) Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算。
10) Reducetask运算完毕后,调用客户指定的outputformat将结果数据输出到外部存储。