MapReduce(分布式计算框架)
1 MapReduce引言
1.1 MapReduce概念
MapReduce:是Hadoop体系下的一种分布式计算框架(计算模型|编程模型)。简单的说,就是通过代码对存储在HDFS上的数据进行计算处理的框架。
大数据的出现带来了海量数据存储的问题外,同时还带来了海量数据的计算问题。当数据到达一定量级后,单机性能再好,也无法在人类可接收的范围内完成数据处理,此时就必须要使用分布式的计算框架,调度多台机器并行计算处理数据,提高数据的处理速度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TIIIm6Up-1631064667569)(MapReduce笔记.assets/image-20210217233526993.png)]
分布式计算:
- 将一个大的文件,分散到多个服务器上存储–分布式存储
- 将一个大的计算任务,分散到多个服务器并行计算–分布式计算
1.2 MapReduce编程思想
MapReduce的核心思想:分而治之 和 计算向数据移动
- 将要计算的大数据拆分成多个小的数据
- 将计算任务分配到数据所在的节点(计算向数据移动),对局部数据进行局部计算获取局部结果
- 将多个小任务的结果,进行合并汇总处理
总结:集合多台服务器的计算能力,并行处理,提高任务的处理速度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FURwXjRm-1631064667571)(MapReduce笔记.assets/image-20210218145144056.png)]
MapReduce核心编程概念:
- MapTask:局部的计算任务(也是可并行任务)
- ReduceTask:汇总计算任务
- Job:(任务,作业)一次完整的计算任务,由多个MapTask和ReduceTask组成
2 Yarn架构分析
Yarn:(Yet Another Resource Negotiator的缩写)是hadoop集群资源管理器系统,用来执行MapReduce分布式计算。
MapReduce是一套分布式计算框架,在运行时需要调度多台服务器的资源。而yarn则可以调度多个服务器的计算资源(cpu、内存等),为MapReduce提供运行所需的资源,一套运行环境。
Yarn的结构分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf7I1dza-1631064667572)(MapReduce笔记.assets/image-20210715232933810.png)]
Yarn从整体上还是属于master/slave模型,主要依赖于三个组件来实现功能:
-
ResourceManager
类似:管理者,只做管理,不负责具体任务
-
Yarn集群的管理者(Master)
监控管理集群中所有NodeManager的资源
-
任务调度
- 接收job任务
- 为job任务在集群中分配计算资源
-
-
NodeManager
类似:具体干活的人
-
Yarn集群的从机(Slave)
管理本机的计算资源(cpu、内存、网络、硬盘)
定期通过心跳向ResourceManager汇报节点资源状态
-
执行计算任务
接收ResourceManager分配的具体计算任务
MapTask(局部计算任务)
ReduceTask(汇总计算任务)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDb6UDg1-1631064667574)(MapReduce笔记.assets/image-20210218161327597.png)]
注意:NodeManager和DataNode是同1个节点
-
-
ApplicationMaster
类似:监工
- 监控Yarn集群每个计算任务(MapTask、ReduceTask)的运行状态
任务名字 执行进度 结果 成功失败 消耗的资源 - 和ResourceManager通信,协调运行资源
根据任务的执行情况,申请更多或者释放资源,甚至说任务失败重试。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5kwyxVD-1631064667575)(MapReduce笔记.assets/image-20210218161409889.png)]
- 监控Yarn集群每个计算任务(MapTask、ReduceTask)的运行状态
3 Yarn伪分布式环境安装
Hadoop包含HDFS和Yarn。
-
首先准备好HDFS环境
#验证HDFS环境 [root@hadoop10 hadoop-2.9.2]#start-dfs.sh #查看进程信息 [root@hadoop10 hadoop-2.9.2]# jps 10951 SecondaryNameNode 10648 NameNode 16539 Jps 10748 DataNode #关闭HDFS [root@hadoop10 hadoop-2.9.2]# stop-dfs.sh
-
配置Yarn
Hadoop包含HDFS和Yarn,安装过Hadoop后直接配置Yarn即可,
配置文件还是在hadoop文件夹/etc/hadoop
目录里-
mapred-site.xml
配置Hadoop的资源调用器为yarn。
注意:需要将mapred-site.xml.template改名为mapred-site.xml<configuration> <!-- 配置MapReduce框架的资源调度器为yarn--> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> </configuration>
-
yarn-site.xml
配置Yarn集群主机,ResourceManager的ip以及MapReduce的策略:mapreduce_shuffle
<!--配置resourcemanager的主机ip--> <property> <name>yarn.resourcemanager.hostname</name> <value>hadoop10</value> </property> <!-- mapreduce计算服务方法。 --> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <!--关闭物理内存检查(测试使用,实战不用)--> <property> <name>yarn.nodemanager.pmem-check-enabled</name> <value>false</value> </property> <!--关闭虚拟内存检查(测试使用,实战不用)--> <property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property>
-
slaves
配置NodeManager的ip(也是DataNode的ip)
hadoop10
-
-
启动HDFS和Yarn集群
#启动HDFS集群 start-dfs.sh #启动Yarn集群 start-yarn.sh 说明:stop-yarn.sh #关闭Yarn集群
-
验证
jps[root@hadoop10 hadoop]# jps 17570 Jps 16984 NameNode 17530 NodeManager #局部计算节点 17085 DataNode 17277 SecondaryNameNode 17439 ResourceManager #资源调度节点
访问Yarn的web服务:http://resourcemanager所在节点ip:8088
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpNzgCON-1631064667576)(MapReduce笔记.assets/image-20210218172651751.png)]
4 MapReduce编程
4.1 编程思路
案例需求:统计一个文件中所有单词出现的次数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gDa62S3h-1631064667577)(MapReduce笔记.assets/image-20210218210557553.png)]
- 将一个大文件拆分成多个小文件,并且每个文件的数据拆分成
<偏移量-行数据>
的键值对 - 在不同的NodeManager上将
<偏移量-行数据>
格式的键值对转换为<单词-次数>
的键值对 - MapTask阶段执行后,下载多个临时结果,分组合并成一个文件
- 然后汇总计算单词和次数的关系,将汇总结果输出到文件中
大体流程分析后,我们结合相关Java API再分析一下编程时需要关注的部分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4kCjrju-1631064667577)(MapReduce笔记.assets/image-20210218214340292.png)]
-
MapTask阶段
-
TextInputFormat:
- 将原有的数据拆分
- 辅助MapTask程序读取
<偏移量-行数据>
的键值对数据
-
Mapper:
-
程序员需要自定义局部计算(处理k-v的代码逻辑)
-
map方法每调用1次处理一个键值对 ,将
<偏移量-行数据>
转换为<单词-次数1>
的键值对 -
MapTask不断读取文件,解析为
<偏移量-行数据>
,然后不断调用map方法伪代码 while(读取下一个kv){ mapper.map(k,v,context); }
-
说明:读取文件拆分,以及循环调用map方法已经在MapReduce框架中实现好,编程时在此阶段只需要关注如何定义map方法实现即可。
-
-
ReduceTask阶段
-
Reducer:
-
程序员需要自定义汇总计算的代码逻辑
-
reduce方法每调用1次,处理分组合并后的一个键值对,将
<单词-[次数,次数]>
转换为<单词-出现总次数>
的键值对 -
ReduceTask不断读取
k-vs
,循环调用reduce方法while(读取下一个k-vs){ reducer.reduce(k,vs); }
-
-
TextOutputFormat:
将reducer计算的k-v结果,写到文件中,并将文件传入到HDFS中
说明:循环调用reduce方法以及输出到HDFS这部分已经在MapReduce框架中实现好,编程时在次阶段只需要关注如何定义reduce方法实现即可。
-
-
Job:
一个MapReduce计算任务就是一个Job。
Job=MapTask+ReduceTask,编程时需要一个Job对象统领Mapper和Reducer。
4.2 第1个MapReduce程序(统计单词出现次数)
-
准备log4j.properties和MapReduce依赖
<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>2.9.2</version> </dependency>
-
编码
-
Mapper开发
public class WordCountJob { //注意:Mapper写成了静态内部类 /* mapper的作用: <偏移量-行数据> ==> <单词-出现的次数(默认1)> Mapper的四个泛型按顺序:KEYIN、VALUEIN、KEYOUT、VALUEOUT KEYIN: 输入的键值对键的类型(偏移量的类型),在Java中是Long,Hadoop中使用LongWritable VALUEIN:输入的键值对值的类型(行数据的类型),在Java中是String,Hadoop中使用Text KEYOUT:输出结果的键的类型(单词的类型),在Java中是String,Hadoop中使用Text VALUEOUT:输出结果的值的类型(次数的类型),在Java中是Integer,Hadoop中使用IntWritable */ public static class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override /* 执行时机:每读取文件的一行,调用1次map方法 key:行数据的偏移量 (这里没啥用) value:一行的数据 (需要拆分成多个单词) context:输出结果的工具 */ protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] words = value.toString().trim().split(" "); //拆分之后,得到的每个单词出现了1次 for (String word : words) { context.write(new Text(word),new IntWritable(1)); } } } }
-
Reducer开发
/* reducer的作用: <单词-出现次数的列表> ==> <单词,总次数> Reducer的四个泛型按顺序:KEYIN、VALUEIN、KEYOUT、VALUEOUT KEYIN: 输入的键值对键的类型(单词的类型),在Java中是String,Hadoop中使用Text VALUEIN:输入的键值对值的类型(次数的类型),Java中是Integer,Hadoop中使用IntWritable KEYOUT:输出结果的键的类型(单词的类型),在Java中是String,Hadoop中使用Text VALUEOUT:输出结果的值的类型(总次数的类型),在Java中是Integer,Hadoop中使用IntWritable */ public static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> { @Override /* key: 单词 values: 出现的次数的集合 context: 输出结果的工具 */ protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { //累加values中的次数,即得到某一个单词的总次数 int sum = 0; for (IntWritable value : values) { sum += value.get(); } context.write(key,new IntWritable(sum)); } }
-
Job开发
//要求:继承Configured类 实现Tool接口,重写run方法 public class WordCountJob extends Configured implements Tool { public static void main(String[] args) throws Exception { System.setProperty("HADOOP_USER_NAME", "root");//设置下用户名 ToolRunner.run(new WordCountJob(),args); } @Override public int run(String[] strings) throws Exception { //1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS","hdfs://hadoop10:9000"); //2 创建Job对象 Job job = Job.getInstance(configuration); job.setJarByClass(WordCountJob.class); //3 组装Job:设置读取文件和输出结果的工具类型 job.setInputFormatClass(TextInputFormat.class); job.setOutputFormatClass(TextOutputFormat.class); //4 组装Job:设置要处理的文件路径和输出结果的路径 TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo1/word.txt")); TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo1/out"));//输出文件如果已经存在会报错 //5 组装Job: 设置mapper和reducer的相关信息 job.setMapperClass(WordCountMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); job.setReducerClass(WordCountReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); //6 启动Job boolean completion = job.waitForCompletion(true);//true:打印日志信息 return completion ? 1:0; } ... }
-
-
查看结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XbVJBLtm-1631064667578)(MapReduce笔记.assets/image-20210218223406726.png)]
5 MR程序提交详解
5.1 Yarn本地提交MR程序
直接在idea中运行mapreduce程序,有一部分是在windows中运行,实际生产环境一定是完全linux环境,需要将程序打包后部署到linux中运行。
-
将Java程序打包 xxx.jar
-
pom.xml配置打包方式和编解码集
<packaging>jar</packaging> <properties> <project.build.sourceEncoding>utf-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
-
执行打包命令
mvn clean package -DskipTests
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pC4oCppd-1631064667579)(MapReduce笔记.assets/image-20210219112152682.png)]
也可以通过idea的maven工具完成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5hARtHMq-1631064667579)(MapReduce笔记.assets/image-20210219171209213.png)]
-
-
将jar包上传到hadoop服务器上,在服务器上执行下jar包
-
找到生成的jar包(在项目的target目录下生成)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMAlH1xn-1631064667580)(MapReduce笔记.assets/image-20210219112456868.png)]
-
将jiar包上传到hadoop服务器上(直接使用xshell或者MobaXterm上传文件),然后执行
hadoop jar jar包名 job全类名 示例: hadoop jar hadoop-test-1.0-SNAPSHOT.jar com.baizhi.mapreduce.WordCountJob
补充:
#1 设置Maven打包jar包的名字 pom.xml添加如下配置 <build> <finalName>wordcount</finalName> </build> #2 实战场景 需求:MR处理商城平台,每天产生的商品日志信息,统计每天每个商品的访问次数? 场景:mr的jar程序,每天定时运行一次。 定时器:每天半夜0:00开始执行 hadoop jar /opt/app/wordcount.jar com.baizhi.mapreduce.WordCountJob
-
5.2 自动远程部署jar包
手动部署jar包到Hadoop服务器上比较麻烦,可以借助maven的插件,自动部署jar包。
-
pom.xml添加插件的ssh扩展和wagon(货车)插件
<build> <finalName>wordcount</finalName> <!-- 配置ssh扩展--> <extensions> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ssh</artifactId> <version>2.8</version> </extension> </extensions> <!-- 配置wagon的copy插件--> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>1.0</version> <configuration> <!--上传的本地jar的位置(固定写法)--> <fromFile>target/${project.build.finalName}.jar</fromFile> <!--远程拷贝的地址 scp://用户名:密码@ip:/opt/app--> <url>scp://root:root@hadoop10:/opt/app</url> </configuration> </plugin> </plugins> </build>
-
执行插件命令,上传文件
mvn clean package #重新编译(不是必须) mvn wagon:upload-single #自动上传jar包到hadoop服务器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9ytA7Kk-1631064667580)(MapReduce笔记.assets/image-20210219170648205.png)]
或者在idea的maven界面,单击插件的功能完成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XO29pzx-1631064667581)(MapReduce笔记.assets/image-20210219171320849.png)]
-
在Hadoop服务器上,运行jar包
hadoop jar /opt/app/wordcount.jar com.baizhi.mapreduce.WordCountJob
5.3 Yarn添加JobHistroy日志聚合服务
JobHistoryServer:
- 负责将各个节点上的日志文件集中到HDFS中,便于管理。—日志聚合。
- 提供查看日志信息web管理界面。
-
修改yarn-site.xml
<!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 --> <property> <name>yarn.log-aggregation-enable</name> <value>true</value> </property> <!-- 设置日志保存时间,单位秒 --> <property> <name>yarn.log-aggregation.retain-seconds</name> <value>106800</value> </property>
-
修改mapred-site.xml
<!-- 设置日志服务器的远程传输日志信息的端口和地址 -->
<property>
<name>mapreduce.jobhistory.address</name>
<value>hadoop10:10020</value>
</property>
<!-- 设置日志服务器的web访问的地址和端口 -->
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>hadoop10:19888</value>
</property>
-
启动jobhistory(在日志服务器所在节点执行)
# 启动 [root@hadoop10 ~]# mr-jobhistory-daemon.sh start historyserver # 关闭 [root@hadoop10 ~]# mr-jobhistory-daemon.sh stop historyserver
-
验证:在windows中通过浏览器访问JobHistoryServer
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jw4jn7O2-1631064667581)(MapReduce笔记.assets/image-20210219225306642.png)]
6 Hadoop序列化和反序列化
6.1 概念
Hadoop序列化和反序列化:MapReduce流程中k-v对象数据在内存和磁盘中数据的过程。
序列化:对象数据从内存到输出保存到磁盘中的过程。
反序列化:从磁盘中读取文件的数据,在内存中恢复成对象的过程。
MapReduce流程中的key-value对象,需要反复在磁盘和内存中进行传输,要求Key-Value要序列化和反序列化。
Hadoop序列化的时机:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AcbdgwyV-1631064667582)(MapReduce笔记.assets/image-20210219220354222.png)]
6.2 Hadoop和Java序列化对比
Hadoop并没有使用Java序列化的方式,而是自定义了一套序列化方式。
Java序列化数据保存的信息比较全:包名、类型、继承关系等等,导致额外的带宽占用,降低传输效率。
Hadoop序列化:尽量序列化数据内容,数据紧凑,降低带宽占用,性能高。
Hadoop和Java序列化对比
区别 | Java序列化 | Hadoop序列化 |
---|---|---|
特点 | 信息量大:package名、class类名、父类、递归的父类信息、数据值等 | 紧凑:仅序列化数据,大数据情况下减少数据带宽占用、提高传输性能。 |
实现方式 | 实现Serializable接口 | 实现Writable接口 |
Hadoop中内置的序列化类型
Java类型 | Hadop需要实现Writable |
---|---|
boolean | BooleanWritable |
byte | ByteWritable |
short | ShortWritable |
int | IntWritable |
long | LongWritable |
float | FloatWritable |
double | DoubleWritable |
String | Text |
arry | ArrayWritable |
map | MapWritable |
null | NullWritable |
6.3 自定义序列化类型
自定义Hadoop序列化类型要求:
- 实现Writable接口
- 实现接口中的序列化和反序列化方法
public class DataFlowWritable implements Writable {
private Integer up;
private Integer down;
@Override
//序列化方法:通过DataOutput输出对象中的属性数据
public void write(DataOutput dataOutput) throws IOException {
/*
序列化调用的方法,必须跟属性类型保持一致
writeInt 输出int类型
writeLong 输出long类型
writeUTF 输出String类型
*/
dataOutput.writeInt(up);
dataOutput.writeInt(down);
}
@Override
//反序列化方法:通过DataInput读取数据,为属性赋值
public void readFields(DataInput dataInput) throws IOException {
/*
反序列化调用的方法,必须跟属性类型一致
读取属性的顺序要跟输出的顺序一致
*/
this.up = dataInput.readInt();
this.down = dataInput.readInt();
}
public Integer getUp() {
return up;
}
public void setUp(Integer up) {
this.up = up;
}
public Integer getDown() {
return down;
}
public void setDown(Integer down) {
this.down = down;
}
@Override
public String toString() {
return "DataFlowWritable{" +
"up=" + up +
", down=" + down +
'}';
}
}
注意:
- 序列化和反序列化的属性顺序保持一致
- 序列化方法和反序列化方法要跟属性类型保持一致
- 反序列化后的值,必须赋值给当前对象的属性
7 案例:app流量分析
需求介绍:
手机使用APP的流量数据,每次手机上网记录一条信息。 需求:统计每个手机号的 上传总流量 下载总流量 总流量
数据格式说明:
# 案例数据
时间戳 手机号 mac地址 ip地址 上传包 下载包 上传流量 下载流量 HTTP状态码
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
# 期望结果
13726230503 上传流量:4962 下载流量:49362 总数据流量: 54324
13826544101 上传流量:528 下载流量:0 总数据流量: 528
13926251106 上传流量:480 下载流量:0 总数据流量: 480
13926435656 上传流量:264 下载流量:3024 总数据流量: 3288
思路分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2GXylUeK-1631064667582)(MapReduce笔记.assets/image-20210228122550796.png)]
-
自定义PhoneFlowWritable封装上传流量和下载流量
public static class PhoneFlowWritable implements Writable{ private Integer up; private Integer down; @Override public void write(DataOutput dataOutput) throws IOException { dataOutput.writeInt(up); dataOutput.writeInt(down); } @Override public void readFields(DataInput dataInput) throws IOException { this.up = dataInput.readInt(); this.down = dataInput.readInt(); } public Integer getUp() { return up; } public void setUp(Integer up) { this.up = up; } public Integer getDown() { return down; } public void setDown(Integer down) { this.down = down; } @Override public String toString() { return "PhoneFlowWritable{" + "up=" + up + ", down=" + down + '}'; } }
-
Mapper开发
public static class PhoneFlowMapper extends Mapper<LongWritable,Text, Text,PhoneFlowWritable>{ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { /* key: 偏移量 value:一行的数据,对value拆分可以得到 手机号 上传流量 下载流量 等等数据 */ String[] splits = value.toString().trim().split("\t"); String phone = splits[1]; String upFlowStr = splits[6]; String downFlowStr = splits[7]; PhoneFlowWritable flow = new PhoneFlowWritable(); flow.setUp(Integer.parseInt(upFlowStr)); flow.setDown(Integer.parseInt(downFlowStr)); context.write(new Text(phone),flow); } }
-
Reducer开发
public static class PhoneFlowReducer extends Reducer<Text, PhoneFlowWritable, Text, Text> { @Override protected void reduce(Text key, Iterable<PhoneFlowWritable> values, Context context) throws IOException, InterruptedException { /* key:手机号 values: 同1个手机号的所有上传下载流量 */ // 遍历values统计一个手机号所有的上传和下载流量 int upSum = 0; int downSum = 0; for (PhoneFlowWritable flow : values) { upSum += flow.getUp(); downSum += flow.getDown(); } context.write(key,new Text("上传流量:"+upSum+" 下载流量:"+downSum+" 总数据流量:"+(upSum+downSum))); } }
-
Job开发
public class PhoneFlowJob extends Configured implements Tool { public static void main(String[] args) throws Exception { System.setProperty("HADOOP_USER_NAME", "root"); ToolRunner.run(new PhoneFlowJob(),args); } @Override public int run(String[] strings) throws Exception { // 1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS","hdfs://hadoop10:9000"); //2 创建job Job job = Job.getInstance(configuration); job.setJarByClass(PhoneFlowJob.class); //3 组装job:设置读取数据和输出数据的工具 job.setInputFormatClass(TextInputFormat.class); job.setOutputFormatClass(TextOutputFormat.class); //4 组装job:设置读取的文件路径和输出的结果路径 TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo2/phone.log")); TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo2/out")); //5 组装job:设置mapper和reducer相关的信息 job.setMapperClass(PhoneFlowMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(PhoneFlowWritable.class); job.setReducerClass(PhoneFlowReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); //6 启动job return job.waitForCompletion(true) ? 1 : 0; } ... }
8 案例:数据清洗
8.1 实战案例
需求介绍
背景介绍:通常情况下,大数据平台获得原始数据文件中,存在大量无效数据和缺失数据,需要再第一时间,对数据进行清洗,获得符合后续处理需求的数据内容和格式
典型需求:对手机流量原始数据,将其中的手机号为"null"和不完整的数据去除
数据格式说明:
# 原数据
id 手机号 手机mac ip地址 上传 下载 HTTP状态码
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157995052 13826544109 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0
1363157995052 null 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 240 0 200
1363157991076 13926435659 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 null null null
# 期望结果【删除其中手机号不符合要求、上传流量确实和下载流量缺失的数据,并仅保格式正确的数据。】
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
...
思路分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGmOFKaU-1631064667583)(MapReduce笔记.assets/image-20210228125748536.png)]
编码:
# 重点:
1.Map阶段将格式正确的数据做成key输出,而value则不需要输出任何数据(NullWritable)
2.MapReduce整个流程中可以取消reduce阶段的程序执行,map输出的会直接作为结果输出到HDFS文件中。
# 编码实现
1. 删除job中有关reducer的相关设置:reducer类和输出的key value类型。
2. 手动设置reducetask的个数为0
job.setNumReduceTasks(0);//取消reducer
-
Mapper开发
public static class FlowDataMapper extends Mapper<LongWritable, Text, Text,NullWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { /* value 一行数据,判断value数据格式是否正确,如果正确就输出,否则跳过 */ if (check(value.toString().trim())) { context.write(value,NullWritable.get()); } } private boolean check(String data){ // 如果拆分后元素个数少于9个,return false String[] splits = data.split(" "); if(splits.length < 9){ return false; } // 手机号为空,return false String phone = splits[1]; if (phone.isEmpty() || phone.equalsIgnoreCase("null")) { return false; } // 上传流量或者下载流量为空,return false String upFlow = splits[6]; if (upFlow.isEmpty() || upFlow.equalsIgnoreCase("null")) { return false; } String downFlow = splits[7]; if (downFlow.isEmpty() || downFlow.equalsIgnoreCase("null")) { return false; } return true; } }
-
Job开发
public class FlowDataCleanJob extends Configured implements Tool { public static void main(String[] args) throws Exception { System.setProperty("HADOOP_USER_NAME", "root"); ToolRunner.run(new FlowDataCleanJob(),args); } @Override public int run(String[] strings) throws Exception { //1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", "hdfs://hadoop10:9000"); //2 创建Job Job job = Job.getInstance(configuration); job.setJarByClass(FlowDataCleanJob.class); //3 组装Job: 设置读取数据和数据结果的工具 job.setInputFormatClass(TextInputFormat.class); job.setOutputFormatClass(TextOutputFormat.class); //4 组装Job: 设置读取的文件路径和输出的文件路径 TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo3/dataclean.log")); TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo3/out")); //5 组装Job:设置Mapper相关的信息 job.setMapperClass(FlowDataCleanMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(NullWritable.class); job.setNumReduceTasks(0);//不再执行reducer //6 启动Job return job.waitForCompletion(true) ? 1:0; } ... }
8.2 计数器Counter
计数器是MapReduce框架内置的日志工具,可以用于统计特定代码的执行次数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3DsXzlTO-1631064667583)(MapReduce笔记.assets/image-20210220225124056.png)]
用途:通常可以使用Counter统计自定义代码的执行次数
# 编码
context.getCounter("自定义计数器组名","自定义计数器名").increment(1L);
需求:
统计清洗的数据总条数、有效数据条数和无效数据条数
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
/*
value 一行数据,判断value数据格式是否正确,如果正确就输出,否则跳过
*/
context.getCounter("dataclean","全部数据").increment(1L);
if (check(value.toString().trim())) {
context.write(value,NullWritable.get());
context.getCounter("dataclean","有效数据").increment(1L);
}else{
context.getCounter("dataclean","无效数据").increment(1L);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZsfzv2Z-1631064667584)(MapReduce笔记.assets/image-20210221132413200.png)]
9 排序
9.1 默认排序
案例:斗鱼主播日志数据按照观众人数升序序排序
# 案例
用户id 观众人数
团团 300
小黑 200
哦吼 400
卢本伟 100
八戒 250
悟空 100
唐僧 100
# 期望结果
卢本伟 100
悟空 100
唐僧 100
小黑 200
八戒 250
团团 300
哦吼 400
-
Mapper开发
public static class AscSortMapper extends Mapper<LongWritable,Text, IntWritable, Text> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { /* value表示一行数据,比如: 团团 300 ,要输出的结果为 300 团团 */ String[] splits = value.toString().split(" "); context.write(new IntWritable(Integer.parseInt(splits[1])),new Text(splits[0])); } }
-
Reducer开发
public static class AscSortReducer extends Reducer<IntWritable, Text, Text, IntWritable> { @Override protected void reduce(IntWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException { for (Text value : values) { context.write(value,key); } } }
-
Job开发
public class AscSortJob extends Configured implements Tool { public static void main(String[] args) throws Exception { System.setProperty("HADOOP_USER_NAME", "root"); ToolRunner.run(new AscSortJob(),args); } @Override public int run(String[] strings) throws Exception { //1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS", "hdfs://hadoop10:9000"); //2 创建Job Job job = Job.getInstance(configuration); job.setJarByClass(AscSortJob.class); //3 组装Job: 设置读取数据和输出结果的工具 job.setInputFormatClass(TextInputFormat.class); job.setOutputFormatClass(TextOutputFormat.class); //4 组装Job: 设置读取和输出的文件路径 TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo4/sort.log")); TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo4/out")); //5 组装Job:设置Mapper和Reducer的相关信息 job.setMapperClass(AscSortMapper.class); job.setMapOutputKeyClass(IntWritable.class); job.setMapOutputValueClass(Text.class); job.setReducerClass(AscSortReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); //6 启动Job return job.waitForCompletion(true) ? 1:0; } ... }
默认排序规则:
MapReduce流程中,默认根据mapper输出的Key-Value对,按照Key的大小升序排列
默认排序规则:
key如果是数字:从小到大排序
key如果是字符串:按照字典顺序排序(a<b<c)
流程分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-078AbqhO-1631064667584)(MapReduce笔记.assets/image-20210228161645680.png)]
默认排序时机:
Map阶段:map处理数据,写出到磁盘时会对数据进行排序输出(降低Reduce阶段的压力)
Reduce阶段:将来自不同MapTask的数据文件进行归并排序
9.2 自定义排序
案例:斗鱼主播日志数据按照观众人数降序排序
# 自定义排序
# 案例
团团 300
小黑 200
哦吼 400
卢本伟 100
八戒 250
悟空 100
唐僧 100
# 期望
哦吼 400
团团 300
八戒 250
小黑 200
卢本伟 100
悟空 100
唐僧 100
思路分析:
MapReduce框架会自动根据key进行排序,只需要修改Key的排序规则即可。
分析下IntWritable类的排序规则public interface WritableComparable<T> extends Writable, Comparable<T> { } public class IntWritable implements WritableComparable<IntWritable> { private int value; ... public int compareTo(IntWritable o) { int thisValue = this.value; int thatValue = o.value; return thisValue < thatValue ? -1 : (thisValue == thatValue ? 0 : 1); } }
通过分析,可知IntWritable实现Comparale接口,在compareTo方法中定义了排序规则。如果要重写Key的排序规则,就需要自定义Key类型,实现Comparable接口,重写compareTo方法。
compareTo规则:
要升序排列:this 和 that(形参接收) 比较,this<that 返回-1,否则返回0或1
要降序排列:this和 that(形参接收)比较,this<that 返回1,否则返回0或-1
编码:
/*
# 1. 需要自定义Mapper输出的key的类型,实现WritableComparable接口
# 2. 实现compareTo方法。
# 3. 补齐write和readFields的序列化相关方法
*/
public static class WatcherWritable implements WritableComparable<WatcherWritable>{
private int watcher;
public WatcherWritable() {
}
public WatcherWritable(int watcher) {
this.watcher = watcher;
}
public int getWatcher() {
return watcher;
}
public void setWatcher(int watcher) {
this.watcher = watcher;
}
@Override
public String toString() {
return Integer.toString(this.watcher);
}
@Override
public int compareTo(WatcherWritable o) {
if(this.watcher < o.watcher){
return 1;
}
if(this.watcher == o.watcher){
return 0;
}
return -1;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(this.watcher);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.watcher = dataInput.readInt();
}
}
9.3 二次排序
案例:主播按照观众人数降序排序,如果观众人数相同,再按直播时长排序。
# 案例数据
用户id 观众人数 直播时长
团团 300 1000
小黑 200 2000
哦吼 400 7000
卢本伟 100 6000
八戒 250 5000
悟空 100 4000
唐僧 100 3000
# 期望结果
哦吼 400 7000
团团 300 1000
八戒 250 5000
小黑 200 2000
卢本伟 100 6000
悟空 100 4000
唐僧 100 3000
思路分析:
MapReduce根据Key类型的compareTo方法决定如何排序。此时,需要先根据观看人数再根据直播时长进行比较,可以定义Key类型,封装 观看人数和直播时长 为属性,然后在compareTo方法中定义比较逻辑
编码:
public static class PlayWritable implements WritableComparable<PlayWritable>{
private int viewer;
private int length;
public PlayWritable() {
}
public PlayWritable(int viewer, int length) {
this.viewer = viewer;
this.length = length;
}
public int getViewer() {
return viewer;
}
public void setViewer(int viewer) {
this.viewer = viewer;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return this.viewer+ " "+this.length;
}
@Override
public int compareTo(PlayWritable o) {
if(o.viewer != this.viewer){
return o.viewer - this.viewer;
}
return o.length - this.length;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(this.viewer);
dataOutput.writeInt(this.length);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.viewer = dataInput.readInt();
this.length = dataInput.readInt();
}
}
总结:
# 总结MapReduce排序
1. 时机:
① MapTask阶段输出k-v后,局部排序[目的: 提前局部并行排序,提高排序速度,减轻reduce端排序压力]
② 在ReduceTask阶段的merge(分组)时候, 数据总体合并排序[归并排序,保证整体有序]
2. 排序依据:
MR对Mapper输出的key进行排序。
3. 排序规则:
MR排序的时候,调用Key.compareTo()决定排序规则。
# MR框架内置
1. 排序(Key)
2. 分组merge(Key)
10 MapReduce组件详解和优化
10.1 MapTask阶段
10.1.1 FileInputFormat
# 1. 作用:
① 对原始的HDFS文件进行逻辑切分。
② 指定mapreduce读取文件路径。
# 2. API讲解
①. 指定一个输入文件
FileInputFormat.addInputPath(job,new Path("/hdfs文件"));
②. 指定一个输入目录
FileInputFormat.addInputPath(job,new Path("/hdfs目录"));
③. 指定多个输入文件
job.setInputFormatClass(TextInputFormat.class);
FileInputFormat.addInputPath(job,new Path("/hdfs/文件1.txt"));
FileInputFormat.addInputPath(job,new Path("/hdfs/文件2.txt"));
FileInputFormat.addInputPath(job,new Path("/hdfs/文件3.txt"));
10.1.2 Split(逻辑切片,一段任务描述信息)
# 1. Split概念
MapReduce程序,FileInputFormat对HDFS源文件的逻辑拆分块。(并非真正的数据块)
通俗:就是一个MapTask处理的数据量描述信息:
start 从哪儿开始读数据
length 当前MapTask读取多少数据。
host 文件所在hdfs的节点位置。
# 2. 内容
start起始位置
length长度
hosts 数据本身所在的datanode节点位置
# 3. 默认大小---TextInputFormat 默认设置
默认 SplitSize == blockSize(实际大小)。
分析:过大或者过小,都会导致MapTask跨节点读取数据文件,导致数据传输速度降低。降低效率。
结论:
使得NodeManager中启动的MapTask尽可能读取本节点的数据,减少数据的跨网络传输。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SDKPDYU-1631064667585)(MapReduce笔记.assets/image-20210228194851505.png)]
为什么一般的SplitSize设置为BlockSize?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FfJLpXSi-1631064667585)(MapReduce笔记.assets/image-20210228195401315.png)]
尽可能的保证NodeManager在处理数据时直接从同1服务器中读取数据,避免跨节点读取数据。
11.1.3 MapTask并行度
# 为什么要并行MapTask ?
利用多个服务器节点的资源,并行处理数据,提高数据处理速度。
# MapTask并行度决定因素
Split的个数 ===> 启动MapTask个数
# MapTask并行度
每个Split数据,启动一个MapTask程序。
MapTask启动个数 == Split个数。
注意:
海量小文件,导致大量的block,导致大量的split,导致启动大量的MapTask,瞬间挤占服务器资源。 MapReduce不适合处理大量小文件数据。
# 场景:Hadoop不适合处理海量的小数据文件
原因:每个文件单独block拆分,海量小文件,导致海量小block,导致海量split,启动大量MapTask,占用过多内存空间。
解决:
1. HDFS:在HDFS中将多个业务含义相同的数据文件合并成1个文件。
hdfs dfs -getmerge /xxx.log /本地目录
2. MapReduce:干预Split 的计算规则,合并多个block为1个split。减少split个数,进而减少MapTask个数。降低服务器运行job的内存占用。
使用CombineTextInputFormat
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EdMM9mX-1631064667586)(MapReduce笔记.assets/image-20210228203553217.png)]
11.1.4 CombineTextInputFormat
# 1. 特点:
将多个小block合并成1个split处理,设置切片大小为10M。
# 2. 应用:
海量的小数据文件产生海量小block,合并成大的split,减少split数量,减少MapTask数量,提高MapReduce性能。
# 3. 代码:
job.setInputFormatClass(CombineTextInputFormat.class);// 设置格式化输出类。
CombineTextInputFormat.setMaxInputSplitSize(job,10485760);//10M,只要加起来不超过10M的block数据,都会合并成1个split处理。
CombineTextInputFormat.addInputPath(job,new Path("/hdfs/目录"));//设置读取文件的路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-loxkbdo5-1631064667586)(MapReduce笔记.assets/image-20210228214122604.png)]
11.1.5 Combiner
案例:金融平台消费日志数据统计
# 数据说明:
数据组成:每个月记录一个文件,文件中记录了用户的消费日志数据。
# 需求:统计每个用户的当月消费总金额?
案例数据:
# 测试案例(消费记录)
姓名 消费金额
张三 100
王五 200
张三 300
李四 300
李四 300
张三 400
王五 500
王五 500
张三 600
# 期望结果
李四 600
王五 1200
张三 1400
思路分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mf7DzEGD-1631064667587)(MapReduce笔记.assets/image-20210228220425935.png)]
编码:
-
Mapper开发
public static class ConsumptionStatistMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] values = value.toString().split(" "); context.write(new Text(values[0]),new IntWritable(Integer.parseInt(values[1]))); } }
-
Reducer开发
public static class ConsumptionStatistReducer extends Reducer<Text,IntWritable,Text,IntWritable>{ @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable value : values) { sum += value.get(); } context.write(key,new IntWritable(sum)); } }
-
Job开发
public class ConsumptionStatisticsJob extends Configured implements Tool { public static void main(String[] args) throws Exception { ToolRunner.run(new ConsumptionStatisticsJob(),args); } @Override public int run(String[] strings) throws Exception { //1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS","hdfs://hadoop10:9000"); //2 创建Job Job job = Job.getInstance(configuration); //3 设置原始数据类型 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("/a/b/consumption-statistics.txt")); //4 设置mapper类和map的输出类型 job.setMapperClass(ConsumptionStatistMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); //5 设置reducer类和reduce输出类型 job.setReducerClass(ConsumptionStatistReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); //6 设置输出结果的输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("/a/b/out")); //7 设置要执行的job的类型 job.setJarByClass(ConsumptionStatisticsJob.class); return job.waitForCompletion(true) ? 1:0; } public static class ConsumptionStatistMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] values = value.toString().split(" "); context.write(new Text(values[0]),new IntWritable(Integer.parseInt(values[1]))); } } public static class ConsumptionStatistReducer extends Reducer<Text,IntWritable,Text,IntWritable>{ @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable value : values) { sum += value.get(); } context.write(key,new IntWritable(sum)); } } }
存在的问题:
# 问题:
所有的累加的统计压力,都放在了Reduce一端。
MapTask有多个,且并行,但计算压力太小。
# 解决思路:
讲ReduceTask的部分计算压力前置到MapTask阶段
本质:对Mapper输出的key-value,执行 局部的Reduce操作(merge[排序 分组 合并],调用Reducer的reduce方法)
Combiner讲解
# 概念
发生在MapTask阶段的局部ReduceTask操作。
# 发生时机
Mapper输出key-value之后,数据在内存中经过 排序 分组 合并 并调用reducer.reduce方法,再输出到本地磁盘。
# 代码
job.setCombinerClass(XxxxReducer.class);
# 场景:
适合支持迭代的数据分析:求和、统计总数等。
不适合:求平均值场景。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jhkw3KDu-1631064667587)(MapReduce笔记.assets/image-20210228221723379.png)]
编码:
-
Mapper开发(和之前一样)
-
Reducer开发(和之前一样)
-
Job开发(比之前多了Combiner的设置)
public class ConsumptionStatisticsJob extends Configured implements Tool { public static void main(String[] args) throws Exception { ToolRunner.run(new ConsumptionStatisticsJob(),args); } @Override public int run(String[] strings) throws Exception { //1 初始化配置 Configuration configuration = new Configuration(); configuration.set("fs.defaultFS","hdfs://hadoop10:9000"); //2 创建Job Job job = Job.getInstance(configuration); //3 设置原始数据类型 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job,new Path("/a/b/consumption-statistics.txt")); //4 设置mapper类和map的输出类型 job.setMapperClass(ConsumptionStatistMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); //5 设置reducer类和reduce输出类型 job.setReducerClass(ConsumptionStatistReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); // 添加Combiner job.setCombinerClass(ConsumptionStatistReducer.class); //6 设置输出结果的输出路径 job.setOutputFormatClass(TextOutputFormat.class); TextOutputFormat.setOutputPath(job,new Path("/a/b/out")); //7 设置要执行的job的类型 job.setJarByClass(ConsumptionStatisticsJob.class); return job.waitForCompletion(true) ? 1:0; } //mapper 和 reducer 省略... }
10.2 ReduceTask阶段相关
10.2.1 ReduceTask并行度
案例:电商平台商品日志数据分析,统计每个商品的访问次数
数据:
# 测试案例:商品浏览日志
日期 域名 商品url 商品名 pid 驻留时间
2020年3月3日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 30
2020年3月3日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 60
2020年3月3日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 100
2020年3月3日 www.baizhiedu.com /product/detail/10002.html xps15 10002 10
2020年3月3日 www.baizhiedu.com /product/detail/10002.html xps15 10002 20
2020年3月3日 www.baizhiedu.com /product/detail/10004.html iphoneX 10004 100
2020年3月3日 www.baizhiedu.com /product/detail/10003.html thinkpadx390 10003 200
2020年3月3日 www.baizhiedu.com /product/detail/10004.html iphoneX 10004 100
2020年3月3日 www.baizhiedu.com /product/detail/10003.html thinkpadx390 10003 100
2020年3月3日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 120
2020年3月4日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 200
2020年3月5日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 25
2020年3月3日 www.baizhiedu.com /product/detail/10004.html iphoneX 10004 100
2020年3月3日 www.baizhiedu.com /product/detail/10002.html xps15 10002 20
2020年3月6日 www.baizhiedu.com /product/detail/10001.html iphoneSE 10001 20
2020年3月3日 www.baizhiedu.com /product/detail/10004.html iphoneX 10004 100
# 期望结果
pid 访问次数
10001 7
10002 3
10003 2
10004 4
思路分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tt73sKFO-1631064667588)(MapReduce笔记.assets/image-20210228224209911.png)]
ReduceTask并行度
# 1. 概念
Reduce阶段ReduceTask程序可以设置多个。(默认是1个)
# 2. 特点
1:提高Reduce端的程序个数,并行执行,提高执行速度。
2:每个ReduceTask程序,对应一个输出结果文件。
# 3. 编码
job.setNumberReduceTasks(reduce数量); //设置ReduceTask个数,实际设置分区的个数。
导致Reduce阶段的数据进行了分区。
ReduceTask提高并行度,会导致Mapper输出的key-value进行分区操作。(上图)
目的:
- 提升效率:提前MapTask阶段分区,并行分流,效率高。且,避免到Reduce端merge时候,多余的比较操作,提升效率。
- 防止数据倾斜:利用多个ReduceTask计算节点,平分汇总阶段的数据计算压力。(数据计算压力的负载均衡)
10.2.2 分区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7W75PSCq-1631064667588)(MapReduce笔记.assets/image-20210228225316914.png)]
# 默认规则
对mapper输出的key,进行分区号的计算。
partitioner.getPartition(k,v)--看上图
# HashParitioner
# 默认分区规则好不好:
1. 启动多个ReduceTask并行度,提升效率。--- 优点
2. 防止ReduceTask阶段的数据倾斜,数据计算压力均衡。--- 优点
默认分区代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhvfLd8p-1631064667589)(MapReduce笔记.assets/image-20210228225500438.png)]
自定义分区
案例:学生成绩统计分析
将学生成绩,按照各个成绩降序排序,各个科目成绩单独输出。
数据:
# 自定义partition
将下面数据分区处理:
姓名 科目 成绩
张三 语文 10
李四 数学 30
王五 语文 20
赵6 英语 40
张三 数学 50
李四 语文 10
张三 英语 70
李四 英语 80
王五 英语 45
王五 数学 10
赵6 数学 10
赵6 语文 100
思路:
# 自定义分区
1. 编写自定义分区类,继承Partitioner覆盖getPartition方法
注意:分区号从0开始算。(示例代码在下面)
2. 给job注册分区类
job.setPartitionerClass(自定义Partitioner.class);
3. 设置ReduceTask个数(开启分区)
job.setNumReduceTasks(数字);//reduceTask数量要和分区数量一样。
public class SubjectPartitioner extends Partitioner<DescDoubleWritable,SubjectStudent> {
/**
* 根据 subject 计算对应的分区好
* 结论:
* 语文 -- 0
* 数学 -- 1
* 英语 -- 2
* 其他 -- 3
* @param descDoubleWritable
* @param subjectStudent
* @param i
* @return
*/
@Override
public int getPartition(DescDoubleWritable descDoubleWritable, SubjectStudent subjectStudent, int i) {
String subject = subjectStudent.getSubject();
int partitionNum=0;
switch (subject){
case "语文":
partitionNum = 0;
break;
case "数学":
partitionNum = 1;
break;
case "英语":
partitionNum = 2;
break;
default:
partitionNum = 3;
break;
}
return partitionNum;
}
}
11 MapReduce工作原理讲解
11.1 环形缓冲区和Spill溢写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryvs1Kln-1631064667590)(MapReduce笔记.assets/image-20210307112749667.png)]
流程分析:
- mapper输出的k-v结果会先临时存放到一个大小为100MB的圆形缓冲区(mapreduce.task.io.sort.mb)
- 一旦缓冲区写满80%(mapreduce.map.sort.spill.percent),触发溢写线程(第2个线程)工作。将80%缓冲区内的内容根据分区排序后,写出到临时文件中。(如果发生多次溢写,会有多个临时文件)
- MapTask工作结束后,将不同的分区临时文件的内容进行归并排序,产生分区后的有序文件
环形缓冲区:
临时存放mapper输出的k-v结果
运行缓冲区大小默认100MB(可以配置修改)
Mapper输出k-v,经过分区,得到分区号,不断写入环形缓冲区
一旦写满80%,触发第2个线程(溢写线程)
读取80%范围内的数据,按照分区进行排序,写出到本地临时文件。
每个分区1个临时文件,文件中key是有序的
溢写:
概念:单独一个线程SpillThread,负责溢写过程
- 从圆形缓冲区中读取k-v(带分区号)
- 根据分区号,对k-v做分区内排序
- 将排序后的k-v写出到本地磁盘文件中,文件按照分区号保存
溢写触发的时机:
- 圆形缓冲区写满80%,触发一次溢写
- MapTask的mapper处理完数据,即使没有达到80%,也会触发最后一次溢写
11.2 MapReduce排序次数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6fKYm6q-1631064667590)(MapReduce笔记.assets/image-20210307120124899.png)]
第一次: MapTask阶段环形缓冲区开始spill溢写,缓冲区每次溢写,发生一轮排序。
第二次: Maptask多次溢写产生的多个溢写文件(单个文件每部k有序),要做归并排序,maptask每个分区内,只保留1个文件(key有序),分区内溢写文件的归并排序。
第三次: ReduceTask 汇总多个MapTask的(对应分区)结果文件,归并排序(合并排序)
11.3 Shuffle
Shuffle: MapReduce整体中的一个部分过程,K-V数据从MapTask的Mapper.map方法中离开,一直到ReduceTask的Reducer.reduce方法接收kv这个中间过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2q8liT5m-1631064667591)(MapReduce笔记.assets/image-20210307121444693.png)]
Shuffle
MapTask阶段
① mapper.map输出kv
② 分区: getPartion(k,v)计算分区号
③ 写入环形缓冲区。
④ 一旦达到溢写条件,溢写k-v-分区号 分区排序 溢写文件产生
⑤ 多个溢写文件归并排序,合并一个文件
ReduceTask阶段
① 下载:每个ReduceTask按照分区号,从所有MapTask本地下载对应分区号的文件。
② 归并排序
③ 按照key分组
④ 将k相同的v合并在一个集合中。
⑤ 将k-vs传入Reducer.reduce(k,vs)处理。
12 MapReduce工作流程汇总
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mczjCYEd-1631064667591)(MapReduce笔记.assets/image-20210307193429209.png)]
- 对输入的文件分片(split)
- 每一个分片交由一个MapTask来处理
- Mapper将结果保存到内存缓冲区中(默认100MB),当缓冲区的内容到达阈值(默认80%),将缓冲区中的内容溢写到磁盘
- 溢写时会先进行分区排序,如果定义了Combiner的话,中间也会执行Combine操作,最终生成分区有序的文件
- 在Reduce流程中,首先相同分区的数据进入到同一个reduce,在这个过程中伴随着排序、合并
- 一个分区对应一个Reducer,有多少分区就有多少ReduceTask,最终通过reduce产生分区结果
13 源码分析
源码的价值:
# 简历
阅读过MyBatis部分源码
阅读过Spring的部分源码
阅读过Mapreducede部分源码
# 技术学习
通过阅读源码,验证我对技术流程的理解
通过阅读源码,加强我对于技术流程的理解。
13.1 Split拆分
Split拆分源码–任务提交 TextInputFormat--FileInputForamt.getSplits(job)
/**
* 对文件生成逻辑上的切片Split,对应InputSplit,多个split对应List集合。
* @param job the job context
* @throws IOException
*/
public List<InputSplit> getSplits(JobContext job) throws IOException{
// 最小值 1,本质上就是【mapreduce.input.fileinputformat.split.minsize】
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
// 最大值 LongMax, 本质上对应:【mapreduce.input.fileinputformat.split.maxsize】
long maxSize = getMaxSplitSize(job);
...
// 创建空的InputSplit的List,一会切一个,放里面放一个Split信息。
List<InputSplit> splits = new ArrayList<InputSplit>();
...
// 获得blockSize=128MB
long blockSize = file.getBlockSize();//128MB
// 获得splitSize=128MB【计算方式:minSize blockSize maxSize 在三者取其中,可以通过调节参数,修改split的大小】
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
// 循环条件:如果剩余的字节大小 > splitSize的1.1倍。
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
// block的序号
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
// 构造一个split(文件路径 start length host 内存host)
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
// 切一刀,减去当前split的字节数。
bytesRemaining -= splitSize;
}
}
13.2 MapTask局部计算
回顾自定义Mapper
// 一个split切片处理,创建一个Mapper对象
class 自定义Mapper extends Mapper{
public void run(Context context) throws IOException, InterruptedException{
//1: 调用setup 一次。:一般用来覆盖后,天加初始化资源操作。---调用1次。
setup(context);
// 循环读取 行数据 k(偏移量)-v(行)
while (context.nextKeyValue()) {
//2: 每读1行,调用1次map方法。
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
//3: 调用cleanup一次:一般覆盖,重写一些释放资源的代码----调用1次。
cleanup(context);
}
// 数据处理map方法:被子类覆盖。
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
//自定义实现
}
}
MapTask调用Mapper的run方法作为调用map的入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vq8C1ymQ-1631064667592)(MapReduce笔记.assets/image-20210805224510875.png)]
1.3.3 ReduceTask汇总计算
自定义Reducer回顾
class 自定义Reducer extends Reducer {
public void run(Context context) throws IOException, InterruptedException {
//1. 调用setup方法。 1次。
setup(context);
try {
while (context.nextKey()) {
// 2:循环读取一组k-vs,调用reduce方法处理:循环调用。
reduce(context.getCurrentKey(), context.getValues(), context);
// If a back up store is used, reset it
Iterator<VALUEIN> iter = context.getValues().iterator();
if(iter instanceof ReduceContext.ValueIterator) {
((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();
}
}
} finally {
// 3:调用cleanup方法。 reduce方法之后调用一次。
cleanup(context);
}
}
// 数据处理reduce方法:汇总操作。
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
//自定义实现
}
}
ReduceTask调用Reducer的run方法作为调用reduce的入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kn3PV1iH-1631064667592)(MapReduce笔记.assets/image-20210805225514661.png)]
14 经典案例:TopN
需求:获得主播观众人数前3名的信息。
数据:
# 原始数据
主播id 观众人数 时长
团团 2345 1000
小黑 67123 2000
哦吼 3456 7000
卢本伟 912345 6000
八戒 1234 5000
悟空 456 4000
唐僧 123345 3000
# 期望结果
卢本伟 912345
唐僧 123345
小黑 67123
常规思路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0FtD9Ep-1631064667593)(MapReduce笔记.assets/image-20210307213549711.png)]
优化思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBafBqkt-1631064667593)(MapReduce笔记.assets/image-20210307213928931.png)]
15 Yarn分布式安装
集群规划:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiP7Vb12-1631064667594)(MapReduce笔记.assets/image-20210805205835409.png)]
安装步骤:
-
环境准备
# 保证HDFS分布式环境已经正确搭建(在之前的HDFS集群基础上搭建Yarn集群) 要求:查看hadoop11:50070. 在datanode标签页看到3个正常的datanode节点信息 # 关闭HDFS集群 stop-dfs.sh
-
初始化yarn的配置
-
配置mapred-site.xml
<!-- mapreduce使用的资源调度器 --> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property> <!-- 设置日志服务器的远程传输日志信息的端口和地址 --> <property> <name>mapreduce.jobhistory.address</name> <value>hadoop10:10020</value> </property> <!-- 设置日志服务器的web访问的地址和端口 --> <property> <name>mapreduce.jobhistory.webapp.address</name> <value>hadoop10:19888</value> </property>
-
配置yarn-site.xml
<property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <!--配置resourcemanager的主机ip--> <property> <name>yarn.resourcemanager.hostname</name> <value>hadoop11</value> </property> <!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 --> <property> <name>yarn.log-aggregation-enable</name> <value>true</value> </property> <!-- 设置日志保存时间 --> <property> <name>yarn.log-aggregation.retain-seconds</name> <value>106800</value> </property>
-
slaves
# 配置NodeManager的ip,同时也是HDFS的DataNode hadoop11 hadoop12 hadoop13
-
远程复制配置文件到其它节点
[root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp slaves root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp slaves root@hadoop13:/opt/installs/hadoop/etc/hadoop/
-
-
启动yarn集群
# 先启动hdfs集群:在NameNode所在机器上执行 start-dfs.sh # 再启动yarn集群:在ResourceManager所在机器上执行 start-yarn.sh # 最后启动历史日志服务器 mr-jobhistory-daemon.sh start historyserver 说明如果要关闭,就执行之前讲过的关闭hdfs和yarn的命令
-
验证
# 访问yarn的资源调度器web网页。 http://主节点ResourceManager节点的ip:8088
程传输日志信息的端口和地址 -->
mapreduce.jobhistory.address
hadoop10:10020
<!-- 设置日志服务器的web访问的地址和端口 -->
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>hadoop10:19888</value>
</property>
```
-
配置yarn-site.xml
<property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <!--配置resourcemanager的主机ip--> <property> <name>yarn.resourcemanager.hostname</name> <value>hadoop11</value> </property> <!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 --> <property> <name>yarn.log-aggregation-enable</name> <value>true</value> </property> <!-- 设置日志保存时间 --> <property> <name>yarn.log-aggregation.retain-seconds</name> <value>106800</value> </property>
-
slaves
# 配置NodeManager的ip,同时也是HDFS的DataNode hadoop11 hadoop12 hadoop13
-
远程复制配置文件到其它节点
[root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp slaves root@hadoop12:/opt/installs/hadoop/etc/hadoop/ [root@hadoop11 hadoop]# scp slaves root@hadoop13:/opt/installs/hadoop/etc/hadoop/
-
启动yarn集群
# 先启动hdfs集群:在NameNode所在机器上执行 start-dfs.sh # 再启动yarn集群:在ResourceManager所在机器上执行 start-yarn.sh # 最后启动历史日志服务器 mr-jobhistory-daemon.sh start historyserver 说明如果要关闭,就执行之前讲过的关闭hdfs和yarn的命令
-
验证
# 访问yarn的资源调度器web网页。 http://主节点ResourceManager节点的ip:8088