Hadoop-Mapreduce
源码及资料:
链接:https://pan.baidu.com/s/1RLwEjkLLmlZ2bZgFOVfFyQ
提取码:nbqm
离线笔记:https://download.csdn.net/download/qq_38454176/12355515
1. MapReduce 介绍
MapReduce思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。
- Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
- Reduce负责“合”,即对map阶段的结果进行全局汇总。
- MapReduce运行在yarn集群
- ResourceManager
- NodeManager
这两个阶段合起来正是MapReduce思想的体现。
还有一个比较形象的语言解释MapReduce:
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就更快。
现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。
经典案例分析:统计文件中每个单词个数
1.1. MapReduce 设计构思
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。为程序员提供一个抽象和高层的编程接口和框架。程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现.Map和Reduce,MapReduce处理的数据类型是<key,value>键值对。
-
Map:
(k1; v1) → [(k2; v2)]
-
Reduce:
(k2; [v2]) → [(k3; v3)]
MapReduce 键值对转换流程分析:统计单词个数
一个完整的mapreduce程序在分布式运行时有三类实例进程:
MRAppMaster
负责整个程序的过程调度及状态协调MapTask
负责map阶段的整个数据处理流程ReduceTask
负责reduce阶段的整个数据处理流程
2. MapReduce 编程规范
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为 2 个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为 2 个步骤
Map 阶段 2 个步骤
- 设置 InputFormat 类, 将数据切分为 Key-Value**(K1和V1)** 对, 输入到第二步
- 自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2) 对, 输出结果
Shuffle 阶段 4 个步骤
- 对输出的 Key-Value (k2-v2)对进行分区
- 对不同分区的数据按照相同的 Key 排序
- (可选) 对分组过的数据初步规约, 降低数据的网络拷贝
- 对数据进行分组, 相同 Key 的 Value 放入一个集合中
Reduce 阶段 2 个步骤
- 对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑, 对输入的 Key-Value 进行处理, 转为新的 Key-Value(K3和V3)输出
- 设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据
单词统计实例分析
3. WordCount
需求: 在一堆给定的文本文件中统计输出每一个单词出现的总次数
分析
Step 1. 数据格式准备
- 创建一个新的文件
cd /export/servers
vim wordcount.txt
- 向其中放入以下内容并保存
hello,world,hadoop
hive,sqoop,flume,hello
kitty,tom,jerry,world
hadoop
- 上传到 HDFS
hdfs dfs -mkdir /wordcount/
hdfs dfs -put wordcount.txt /wordcount/
Step 2. Mapper
package cn.wbslz.mapreduce;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* 4 个泛型解释:
* KEYIN: k1的类型
* VALUEIN:v1的类型
*
* KEYOUT: k2的类型
* VALUEOUT:v2的类型
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
//LongWritable 对Long的封装
//Text 对byte[] 的封装
/**
* map方法将<k1,v1>转为<k2,v2>
*
* @param key k1 偏移量
* @param value v1 每一行的文本数据
* @param context 上下文对象
* @throws IOException
* @throws InterruptedException
*/
/*
如何将<k1,v1>转为<k2,v2>
k1 v1
0 hello,world,hadoop
23 hive,sqoop,flume,hello
39 kitty,tom,jerry,world
56 hadoop
==================================
k2 v2
hello 1
world 1
hadoop 1
................
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
Text text = new Text();
LongWritable longWritable = new LongWritable();
//1.将一行的文本数据进行拆分
String[] split = value.toString().split(",");
//2.遍历数组,组装<k2,v2>
for(String word : split){
//3.将<k2,v2>写入上下文中
text.set(word);
longWritable.set(1);
context.write(text,longWritable);
}
}
}
Step 3. Reducer
package cn.wbslz.mapreduce;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* 4 个泛型解释:
* KEYIN: k2的类型
* VALUEIN:v2的类型
*
* KEYOUT: k3的类型
* VALUEOUT:v3的类型
*/
public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
/**
* 将新的<k2,v2>转为</k3,v3>,将<k3,v3>写入上下文中
* @param key 新k2
* @param values 集合:新v2
* @param context 表示上下文对象
* @throws IOException
* @throws InterruptedException
*/
/*
如何将新的<k2,v2>转为</k3,v3>
新 k2 v2
hello <1,1,1,>
world <1,1>
hadoop <1,1>
hive <1,1>
==========================
k3 v3
hello 3
world 2
hadoop 2
hive 2
..............
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
LongWritable longWritable = new LongWritable();
//1.遍历集合,将集合中的数字相加,得到v3
for (LongWritable value : values) {
count += value.get();
}
longWritable.set(count);
//2.将<k3,v3>写入上下文中
context.write(key,longWritable);
}
}
Step 4. 定义主类, 描述 Job 并提交 Job
package cn.wbslz.mapreduce;
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.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 {
/**
* 该方法用于指定一个job任务
* @param strings
* @return
* @throws Exception
*/
@Override
public int run(String[] strings) throws Exception {
//1.创建一个job任务对象
Job job = Job.getInstance(super.getConf(), "wordcount");
//2.配置job任务对象(8个步骤)
//第一步,指定文件的读取方式和读取路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("hdfs://node01:8020/wordcount"));
//第二步:指定map阶段的处理方式和数据类型
job.setMapperClass(WordCountMapper.class);
//设置map阶段k2的类型
job.setMapOutputKeyClass(Text.class);
//设置map阶段v2的类型
job.setMapOutputValueClass(LongWritable.class);
//第三,四,五,六步,采用默认方式,不做处理
//第七步:指定Reduce阶段的处理方式和数据类型
job.setReducerClass(WordCountReducer.class);
//设置k3类型
job.setOutputKeyClass(Text.class);
//设置v3类型
job.setOutputValueClass(LongWritable.class);
//第八步:设置输出类型
job.setOutputFormatClass(TextOutputFormat.class);
//设置输出路径
TextOutputFormat.setOutputPath(job,new Path("hdfs://node01:8020/wordcount.out"));
//等待任务结束
boolean bl = job.waitForCompletion(true);
return bl ? 0 : 1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
//启动job任务
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}
常见错误
如果遇到如下错误
Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.security.AccessControlException): Permission denied: user=admin, access=WRITE, inode="/":root:supergroup:drwxr-xr-x
直接将hdfs-site.xml当中的权限关闭即可
<property>
<name>dfs.permissions</name>
<value>false</value>
</property>
最后重启一下 HDFS 集群
小细节
本地运行完成之后,就可以打成jar包放到服务器上面去运行了,实际工作当中,都是将代码打成jar包,开发main方法作为程序的入口,然后放到集群上面去运行
4. MapReduce 运行模式
本地运行模式
- MapReduce 程序是被提交给 LocalJobRunner 在本地以单进程的形式运行
- 处理的数据及输出结果可以在本地文件系统, 也可以在hdfs上
- 怎样实现本地运行? 写一个程序, 不要带集群的配置文件, 本质是程序的
conf
中是否有mapreduce.framework.name=local
以及yarn.resourcemanager.hostname=local
参数 - 本地模式非常便于进行业务逻辑的
Debug
, 只要在IDEA
中打断点即可
configuration.set("mapreduce.framework.name","local");
configuration.set(" yarn.resourcemanager.hostname","local");
TextInputFormat.addInputPath(job,new Path("file:///D:\IdeaWorkSpace\bigDate\data\wordcount\input"));
TextOutputFormat.setOutputPath(job,new Path("file:///D:\IdeaWorkSpace\bigDate\data\wordcount\output"));
结果:
集群运行模式
- 将 MapReduce 程序提交给 Yarn 集群, 分发到很多的节点上并发执行
- 处理的数据和输出结果应该位于 HDFS 文件系统
- 提交集群的实现步骤: 将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动
hadoop jar hadoop_hdfs_operate-1.0-SNAPSHOT.jar cn.wbslz.mapreduce.JobMain
结果:
5. MapReduce 分区
分区概述
在 MapReduce 中, 通过我们指定分区, 会将同一个分区的数据发送到同一个 Reduce 当中进行处理
分区运行原理
例如: 为了数据的统计, 可以把一批类似的数据发送到同一个 Reduce 当中, 在同一个 Reduce 当中统计相同类型的数据, 就可以实现类似的数据分区和统计等
其实就是相同类型的数据, 有共性的数据, 送到一起去处理
Reduce 当中默认的分区只有一个
需求:将以下数据进行分开处理
详细数据参见partition.csv 这个文本文件,其中第五个字段表示开奖结果数值,现在需求将15以上的结果以及15以下的结果进行分开成两个文件进行保存
分区步骤:
Step 1. 定义 Mapper
这个 Mapper 程序不做任何逻辑, 也不对 Key-Value 做任何改变, 只是接收数据, 然后往下发送
package cn.wbslz.partition;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* k1:行偏移量 LongWritable
* v1:行文本数据 Text
*
* k2: 行文泵数据 Text 既(19 0 1 2017-07-31 23:50:10 837263 18 6+7+5=18 大,双 0 0.00 0.00 1 0.00 1 1)
* v2: NullWirtable
*/
public class PartitionMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
/**
* map方法将<k1,v1>转为<k2,v2>
* @param key
* @param value
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
context.write(value,NullWritable.get());
}
}
Step 2. 定义 Reducer 逻辑
这个 Reducer 也不做任何处理, 将数据原封不动的输出即可
package cn.wbslz.partition;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* k2: Text
* v2: NullWritable
*
* k3: Text
* v3: NullWritable
*/
public class PartitionerReducer extends Reducer<Text, NullWritable,Text,NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key,NullWritable.get());
}
}
Step 3. 自定义 Partitioner
主要的逻辑就在这里, 这也是这个案例的意义, 通过 Partitioner 将数据分发给不同的 Reducer
package cn.wbslz.partition;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class MyPartitioner extends Partitioner<Text, NullWritable> {
/**
* 1.定义分区规则
* 2.返回对应分区编号
* @param text
* @param nullWritable
* @param i
* @return
*/
@Override
public int getPartition(Text text, NullWritable nullWritable, int i) {
//1.拆分行文本数据(k2),获取中奖字段的值
String[] split = text.toString().split("\t");
String numStr = split[5];
//2.判断中奖字段的值和15的关系,然后返回对应的分区编号
if (Integer.parseInt(numStr) > 15){
return 1;
}else {
return 0;
}
}
}
Step 4. Main 入口
package cn.wbslz.partition;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
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 {
@Override
public int run(String[] strings) throws Exception {
//1:创建job任务对象
Job job = Job.getInstance(super.getConf(), "partition_maperduce");
//2.对job任务进行配置(八个步骤)
//第一步:设置输入类和输入路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("hdfs://node01:8020/input"));
//第二步:设置map类和数据类型(k2,v2)
job.setMapperClass(PartitionMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//第三步:指定分区类
job.setPartitionerClass(MyPartitioner.class);
// 四,五,六步使用默认
//第七步:指定Reducer类和数据类型(k3,v3)
job.setReducerClass(PartitionerReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置ReduceTask的个数
job.setNumReduceTasks(2);
//第八步:指定输出类和输出路径
job.setOutputFormatClass(TextOutputFormat.class);;
TextOutputFormat.setOutputPath(job,new Path("hdfs://node01:8020/out/partition_out"));
//3.等待任务结束
boolean bl = job.waitForCompletion(true);
return bl ? 0 : 1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
//启动job任务
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}
集群运行:
hadoop jar original-day05_mapreduce-1.0-SNAPSHOT.jar cn.wbslz.partition.JobMain
结果:
第一个文件都是小于15的数据
第二个文件都是大于15的数据
6 MapReduce 中的计数器
计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计。计数器还可辅助诊断系统故障。如果需要将日志信息传输到 map 或 reduce 任务, 更好的方法通常是看能否用一个计数器值来记录某一特定事件的发生。对于大型分布式作业而言,使用计数器更为方便。除了因为获取计数器值比输出日志更方便,还有根据计数器值统计特定事件的发生次数要比分析一堆日志文件容易得多。
hadoop内置计数器列表
MapReduce任务计数器 | org.apache.hadoop.mapreduce.TaskCounter |
---|---|
文件系统计数器 | org.apache.hadoop.mapreduce.FileSystemCounter |
FileInputFormat计数器 | org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
FileOutputFormat计数器 | org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter |
作业计数器 | org.apache.hadoop.mapreduce.JobCounter |
每次mapreduce执行完成之后,我们都会看到一些日志记录出来,其中最重要的一些日志记录如下截图
所有的这些都是MapReduce的计数器的功能,既然MapReduce当中有计数器的功能,我们如何实现自己的计数器???
需求:以以上分区代码为案例,统计map接收到的数据记录条数
第一种方式
第一种方式定义计数器,通过context上下文对象可以获取我们的计数器,进行记录
通过context上下文对象,在map端使用计数器进行统计
在刚才的PartitionMapper 中修改
public class PartitionMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
//map方法将K1和V1转为K2和V2
@Override
protected void map(LongWritable key, Text value, Context context) throws Exception{
//方式1:定义计数器
Counter counter = context.getCounter("MR_COUNTER", "partition_counter");
//每次执行该方法,则计数器变量加一
counter.increment(1L);
context.write(value,NullWritable.get());
}
}
运行程序之后就可以看到我们自定义的计数器在map阶段读取了15213条数据
第二种方式
通过enum枚举类型来定义计数器
统计reduce端数据的输入的key有多少个
public class PartitionerReducer extends Reducer<Text,NullWritable,Text,NullWritable> {
public static enum Counter{
MY_INPUT_RECOREDS,MY_INPUT_BYTES
}
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//方式二:使用枚举来定义计数器
context.getCounter(Counter.MY_INPUT_RECOREDS).increment(1L);
context.write(key, NullWritable.get());
}
}
结果相同: