YARN-HA配置
YARN-HA工作机制
官网文档 : http://hadoop.apache.org/docs/r3.3.0/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html
配置YARN-HA集群
准备: 配置主机名,静态IP,ssh免密登录,JDK&环境变量配置
规划 :
hadoop111 | hadoop112 | hadoop113 |
---|---|---|
NameNode | NameNode | — |
JournalNode | JournalNode | JournalNode |
DataNode | DataNode | DataNode |
ZK | ZK | ZK |
— | ResourceManager | ResourceManager |
NodeManager | NodeManager | NodeManager |
yarn 配置:
① 配置yarn-site.xml
<!--配置hadoop classpath-->
<property>
<name>yarn.application.classpath</name>
<value>/opt/ha/hadoop-3.3.0/etc/hadoop:/opt/ha/hadoop-3.3.0/share/hadoop/common/lib/*:/opt/ha/hadoop-3.3.0/share/hadoop/common/*:/opt/ha/hadoop-3.3.0/share/hadoop/hdfs:/opt/ha/hadoop-3.3.0/share/hadoop/hdfs/lib/*:/opt/ha/hadoop-3.3.0/share/hadoop/hdfs/*:/opt/ha/hadoop-3.3.0/share/hadoop/mapreduce/*:/opt/ha/hadoop-3.3.0/share/hadoop/yarn:/opt/ha/hadoop-3.3.0/share/hadoop/yarn/lib/*:/opt/ha/hadoop-3.3.0/share/hadoop/yarn/*</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<!--启用resourcemanager ha-->
<property>
<name>yarn.resourcemanager.ha.enabled</name>
<value>true</value>
</property>
<!--声明两台resourcemanager的地址-->
<property>
<name>yarn.resourcemanager.cluster-id</name>
<value>cluster-yarn1</value>
</property>
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>hadoop112</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>hadoop113</value>
</property>
<!--指定zookeeper集群的地址-->
<property>
<name>yarn.resourcemanager.zk-address</name>
<value>hadoop111:2181,hadoop112:2181,hadoop113:2181</value>
</property>
<!--启用自动恢复-->
<property>
<name>yarn.resourcemanager.recovery.enabled</name>
<value>true</value>
</property>
<!--指定resourcemanager的状态信息存储在zookeeper集群-->
<property>
<name>yarn.resourcemanager.store.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
</property>
② 分发给其他机器
③ 启动hdfs
# 分别启动 各个JournalNode节点
sbin/hadoop-daemon.sh start journalnode
# nn1上 hdfs 格式化,并启动
hdfs namenode -format
hdfs --daemon start namenode
# nn2上同步nn1的元数据
hdfs namenode -bootstrapStandby
# 启动nn2
hdfs --daemon start namenode
# 浏览器查看,都是 standby 状态
# http://hadoop111:9870
# http://hadoop112:9870
# nn1 上启动所有datanode (daemons)
hadoop-daemons.sh start datanode
# 将nn1切换为 Active
hdfs haadmin -transitionToActive nn1
# 查看是否Active
hdfs haadmin -getServicecState nn1
④ 启动yarn
# 在 hadoop112 上群启
start-yarn.sh
# 在hadoop113上启动resourcemanager
yarn --daemon start resourcemanager
# 查看服务状态
yarn rmadmin -getServerState rm1
浏览器查看yarn地址 : http://hadoop112:8088/
HDFS Federation架构设计(联邦架构设计)
(1) NameNode架构的局限性
① Namespace( 命名空间) 的限制
由于NameNode在内存中存储所有的元数据( metadata) , 因此单个NameNode所能存储的对象( 文件+块) 数目受到NameNode所在JVM的heap size的限制. 50G的heap能够存储20亿( 200million) 个对象, 这20亿个对象支持4000个DataNode, 12PB的存储( 假设文件平均大小为40MB) . 随着数据的飞速增长, 存储的需求也随之增长. 单个DataNode从4T增长到36T, 集群的尺寸增长到8000个DataNode. 存储的需求从12PB增长到大于100PB.
② 隔离问题
由于HDFS仅有一个NameNode, 无法隔离各个程序, 因此HDFS上的一个实验程序就很有可能影响整个HDFS上运行的程序.
③ 性能的瓶颈
由于是单个NameNode的HDFS架构, 因此整个HDFS文件系统的吞吐量受限于单个NameNode的吞吐量.
(2) HDFS Federation架构设计
能不能有多个NameNode
(3) HDFS Federation应用思考
不同应用可以使用不同NameNode进行数据管理
图片业务,爬虫业务,日志审计业务
Hadoop生态系统中, 不同的框架使用不同的NameNode进行管理NameSpace. ( 隔离性)
NameNode | NameNode | NameNode |
---|---|---|
元数据 | 元数据 | 元数据 |
Log | machine | 电商数据/话单数据 |
MapReduce-深入探索
MapReduce概述
MapReduce是一个并行计算与运行软件框架.MapReduce核心功能是将用户编写的业务代码和自带的默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上.
优点:
-
易于编程,面向接口 简单实现一些接口就可以完成一个分布式程序.
-
良好扩展性 可以动态增加节点来提供更高的计算能力
-
高容错 集群中一个机器挂了,可以把到另外一个节点上运行,不至于这个任务运行失败.
-
适合PB级以上海量数据的离线处理 可以千台服务器集群
缺点:
- 不支持实时计算 不能在毫秒或秒级以内返回运算结果.
- 不支持流式计算 MapReduce输入数据是静态的,不能动态变化
- 不擅长DAG(有向图)计算 前一个程序的输出作为后一个程序的输入,每个MapReduce任务的输出结果都将写入磁盘,会造成大量磁盘I/O导致性能低下.
MapReduce核心思想
-
分布式的运算程序往往需要需要分成2个阶段
-
第一个阶段MapTask并发实例,完全并行,互不相干
-
第二个阶段ReduceTask并发实例互不相干,但是数据来源依赖MapTask的结果
-
MapReduce编程模型中只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那只能多个MapReduce程序,串行运行.
分析WordCount数据流走向深入理解MapReduce核心思想
MapReduce进程
一个完整的MapReduce程序在分布式运行时有三个实例进程:
- Mr AppMaster : 负责整个程序的过程调度和状态协调.
- MapTask : 负责Map阶段的整个数据处理流程
- ReduceTask:负责Reduce阶段的整个数据处理流程
官方WordCount源码分析
Map类,Reduce类,驱动类,数据类型封装
Java类型 | Hadoop Writable类型 |
---|---|
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
String | Text |
map | MapWritable |
array | ArrayWritable |
MapReduce编程规范
编写MapReduce分为3个部分 : Mapper,Reducer,Driver
① Map阶段
# 1. 用户自定义的Mapper继承 org.apache.hadoop.mapreduce.Mapper
# 2. Mapper的输入数据是K,V形式(K,V类型自定义)
# 3. Mapper中的业务逻辑写在map()方法中
# 4. Mapper的输出数据是K,V形式(自定义类型)
# 5. map()方法(MapTask进程)对每一个<K,V>调用一次
② Reduce阶段
# 1. 用户自定义的 Reducer 继承 父类
# 2. Reducer 的输入数据类型对应Mapper的输出数据类型也是K,V形式
# 3. Reducer 中的业务逻辑写在reduce()方法中
# 4. Reducer进程对每一组相同K的<K,V>调用一次reduce()方法
③ Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象.
编写WordCount案例(debug才能够弄明白流程)
ps : I debugged n times and finally understood how Hadoop counts words…
mapTask走完后到reduceTask阶段时,reduce的数据来源已经是按字典顺序排序的了,相同的<K,V>,只会执行一次
- 准 wordcount.txt (自定义)
idea dev
dev dev
count count this
word word
this hadoop hadoop
hadoop
1) WordCountMapper
package com.example.wc;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* <p>
* WordCountMapper
* </p>
*
* @author f
* @description WordCountMapper
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 输出
for (String word : words) {
k.set(word);
context.write(k, v);
System.out.printf("%s %s\n",k,v);
}
}
}
2) WordCountReducer
package com.example.wc;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* <p>
* WordCountReducer
* </p>
*
* @author f
* @description WordCountReducer
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable value : values) {
System.out.printf("%s : %s\n",key.toString(),value);
sum += value.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
3) WordCountDriver
package com.example.wc;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* <p>
* WordCountDriver
* </p>
*
* @author f
* @description WordCountDriver
*/
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
// 1 获取配置信息以及封装任务
Job job = Job.getInstance(conf);
// 2 设置jar加载路径
job.setJarByClass(WordCountDriver.class);
// 3 设置map和reduce类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4 设置map输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path("./hadoop-demo/wordcount.txt"));
FileOutputFormat.setOutputPath(job, new Path("./hadoop-demo/output"));
// 7 提交
boolean result = job.waitForCompletion(true);
System.out.printf("result : %s%n", result);
System.exit(result ? 0 : 1);
}
}
Hadoop序列化
对象====>>>>序列化====>>>> <<<<====反序列化<<<<====字节序列
序列化 : 把内存中的对象转换成字节序列,便于网络传输和存储.
反序列化 : 将收到的字节序列(或者硬盘持久化数据),转换成内存中的对象.
为什么要序列化?
保存对象的状态(持久化)和用于网络传输
Hadoop序列化没有用Java Serializable,为什么?
一个对象被序列化之后,会附带很多额外信息(校验信息+Header+继承体系…),网络中不能达到高效传输,所以开发一套属于自己的序列化机制(Writable).
Hadoop序列化特点 :
-
紧凑 : 高效实用存储空间
-
快速 : 读写数据开销小
-
可扩展 : 通信协议可升级
-
多语言交互 : 支持多种语言交互
自定义bean对象实现序列化接口(Writable)
// 1. 实现 Writable 接口
// 2. 反序列化时,会调用无参构造,所以自定义对象必须有无参构造
// 3. 重写序列化 write(DataOutput out) 方法
// 4. 重写反序列化 readFields(DataInput in) 方法
// 5. 反序列化的顺序和序列化的顺序需要完全一致
// 6. 显示结果,可以重写toString()
// 7. 如果自定义对象的key要进行MR运算,需要对象实现 Comparable 接口,因为MaperReduce中的Shuffle过程要求对key必须能排序
####### 案例 ########
user_log需求说明 :
序号 用户手机号 IP 网站域名 上行流量 下行流量 网络状态
1 13736230513 192.196.100.1 www.baidu.com 2481 24681 200
案例中每一列不一定都有值,没有的为null
需求 : 统计每一个手机号耗费的总上行流量,下行流量,总流量
user_log.txt
1 13736230513 192.196.100.1 www.baidu.com 2481 24681 200
2 13846544121 192.196.100.2 264 0 200
3 13956435636 192.196.100.3 132 1512 200
4 13966251146 192.168.100.1 240 0 404
5 18271575951 192.168.100.2 www.alibaba.com 1527 2106 200
6 84188413 192.168.100.3 www.weixin.com 4116 1432 200
7 13590439668 192.168.100.4 1116 954 200
8 15910133277 192.168.100.5 www.hao123.com 3156 2936 200
9 13729199489 192.168.100.6 240 0 200
10 13630577991 192.168.100.7 www.shouhu.com 6960 690 200
11 15043685818 192.168.100.8 www.baidu.com 3659 3538 200
12 15959002129 192.168.100.9 www.baidu.com 1938 180 500
13 13560439638 192.168.100.10 918 4938 200
14 13470253144 192.168.100.11 180 180 200
15 13682846555 192.168.100.12 www.qq.com 1938 2910 200
16 13992314666 192.168.100.13 www.weixin.com 3008 3720 200
17 13509468723 192.168.100.14 www.bilibili.com 7335 110349 404
18 18390173782 192.168.100.15 www.bilibili.com 9531 2412 200
19 13975057813 192.168.100.16 www.baidu.com 11058 48243 200
20 13768778790 192.168.100.17 120 120 200
21 13568436656 192.168.100.18 www.alibaba.com 2481 24681 200
22 13568436656 192.168.100.19 1116 954 200
UserLogMapper :
package com.example.customwc;
import com.example.customwc.entity.UserLog;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class UserLogMapper extends Mapper<LongWritable, Text,Text, UserLog> {
Text k = new Text();
UserLog v = new UserLog();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
System.out.println("key : "+key.toString());
String line = value.toString();
String[] split = line.split("\t"); //maxIndex=6
//手机号
String phone = split[1];
//上行流量
String up = split[split.length-3];
//下行流量
String down = split[split.length-2];
k.set(phone);
v.setUpFlow(Long.parseLong(up));
v.setDownFlow(Long.parseLong(down));
v.setSumFlow(Long.parseLong(up)+Long.parseLong(down));
context.write(k,v);
}
}
UserLogReducer :
package com.example.customwc;
import com.example.customwc.entity.UserLog;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class UserLogReducer extends Reducer<Text, UserLog,Text, UserLog> {
@Override
protected void reduce(Text key, Iterable<UserLog> values, Context context) throws IOException, InterruptedException {
long sumUp = 0;
long sumDown = 0;
for (UserLog value : values) {
sumUp+=value.getUpFlow();
sumDown+=value.getDownFlow();
}
UserLog userLog = new UserLog(sumUp,sumDown);
context.write(key,userLog);
}
}
UserDriver :
package com.example.customwc;
import com.example.customwc.entity.UserLog;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class UserLogDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//获取job实例
Job job = Job.getInstance(new Configuration());
//设置jar路径
job.setJarByClass(UserLogDriver.class);
//设置map/reduce实例
job.setMapperClass(UserLogMapper.class);
job.setReducerClass(UserLogReducer.class);
//设置map/reduce输出<k,v>类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(UserLog.class);
//设置最终<k,v>输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(UserLog.class);
//设置源输入最终输出文件路径
FileInputFormat.setInputPaths(job, new Path("./hadoop-demo/user_log.txt"));
FileOutputFormat.setOutputPath(job, new Path("./hadoop-demo/customwc/output"));
//提交给yarn执行
boolean res = job.waitForCompletion(true);
System.out.println(res);
}
}
MapReduce框架原理
切片与MapTask并行度决定机制
1.x 版本集群默认块大小 : 64M
2.X以上集群默认块大小 : 128M
本地运行默认切片大小 : 32M
MapTask并行度决定Map阶段任务处理并发度.
数据块 : Block是HDFS物理上把数据分成一个块Block.
数据切分 : 数据切片只是在逻辑上对输入进行分片,并不会在磁盘上分片存储.
# 1. 一个Job的Map阶段并行度由客户端在提交Job时切片数决定
# 2. 每一个Split切片分配一个MapTask并行实例处理
# 3. 默认情况下,切片大小=BlockSize
# 4. 切片时(多个文件)是针对每一个文件单独切片
eg :
切片大小设置为 : 100M
默认物理块大小 : 128M
假设一个230M的文件,那么逻辑上会分3片,而物理上只有2块
Job提交流程和切片源码
// 1. job提交job.waitForCompletion(true);
job.waitForCompletion(true);
// 2. 提交任务submit();
->submit();
// 3. 创建连接 connect
-->connect();
// 4. 创建job代理对象cluster, new Cluster(getConfiguration());
--->new Cluster(getConfiguration());
// 5. 判断是本地yarn还是远程集群
---->initialize(jobTrackAddr, conf);
// 6. 提交job任务 submitter.submitJobInternal(Job.this, cluster);
-->submitter.submitJobInternal(Job.this, cluster);
// 7. 检验输出路径是否已存在;已存在直接抛异常
--->checkSpecs(job);
// 8. 获取给集群提交配置路径
--->Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 9. 获取jobId job_local1394216225_0001
--->JobID jobId = submitClient.getNewJobID();
// 10. 创建提交job路径对象
--->Path submitJobDir = new Path(jobStagingArea, jobId.toString());
// 11. 拷贝配置文件
--->copyAndConfigureFiles(job, submitJobDir);
// 12. 上传job文件
---->rUploader.uploadResources(job, jobSubmitDir);
----->uploadResourcesInternal(job, submitJobDir);
// 13. 这个时候才创建提交job文件夹 job_local1394216225_0001
------>mkdirs(jtFs, submitJobDir, mapredSysPerms);
// 14. 获取提交job文件
--->Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
// 15. 把切片信息写到job提交任务文件夹 2个crc校验文件和一个split切片文件和一个切片元数据文件
// ( .job.split.crc .job.splitmetainfo.crc job.split job.splitmetainfo )
--->int maps = writeSplits(job, submitJobDir);
// 16. 这里分新旧版本,新版本分片 旧版本走else是这个: maps = writeOldSplits(jConf, jobSubmitDir);
---->maps = writeNewSplits(job, jobSubmitDir);
// 17. 获取所有分片
----->List<InputSplit> splits = input.getSplits(job);
// 18. 最小值默认为: 1
------>long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
// 19. 最大值默认为: Long.MAX_VALUE
------>long maxSize = getMaxSplitSize(job);
// 20. 获取最大值
------->return context.getConfiguration().getLong(SPLIT_MAXSIZE, Long.MAX_VALUE);
// 21. 列出job文件
------>List<FileStatus> files = listStatus(job);
------>for (FileStatus file: files) {
// 22. 是否可切分,不可切分走else if 或者else 就一块,要么有数据,要么没数据
------>if (isSplitable(job, path)) {
// 23. 默认LOCAL:32M 远程集群:128M
------>long blockSize = file.getBlockSize();
// 24. 计算出切片大小
------>long splitSize = computeSplitSize(blockSize, minSize, maxSize);
// 25. 切片公式
------>Math.max(minSize, Math.min(maxSize, blockSize));
// 26. 当文件超过切片大小的1.1倍才切分,这个很重要
------>while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//切片...
}
// 27. 配置文件写到job任务文件夹 ( .job.xml.crc && job.xml )
---->writeConf(conf, submitJobFile);
----->conf.writeXml(out);
// 28. 本地提交或yarn提交(LocalRunner或YarnRunner)
---->status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
-->jobId, submitJobDir.toString(), job.getCredentials());
// 29. 修改job任务状态,删除本地job提交任务文件,返回成功 jtFs.delete(submitJobDir, true);
->monitorAndPrintJob();
// 30. 结束返回
boolean result = job.waitForCompletion(true);
提交任务文件夹在/temp/hadoop/… 下
提交文件(crc为校验文件):
- .job.split.crc
- .job.splitmetainfo.crc
- job.split (切片位置记录信息)
- job.splitmetainfo (job的切片元数据)
- .job.xml.crc
- job.xml (hadoop 的配置信息)
FileInputFormat切片源码分析
-
先找到数据存储目录
-
开始遍历处理(规划切片)目录下的每一个文件
-
遍历第一个文件
-
获取文件大小fs.sizeOf(test.txt)
-
计算切片大小
computeSlitSize(Math.max(minSize,Math.min(maxSize,blockSize)))=blockSize=128M
-
默认情况下,切片大小=blockSize
-
开始切,形成第1个切片: test.txt – 0: 128M 第二个切片 test.txt-- 128:256M 第3个切片 test.txt —256M–280M
每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分为一块
-
将切片信息写到提交切片规划文件中
-
整个切片核心在getSplit()方法中
-
InputSplit只记录了切片的元数据(起始位置,长度,所在节点列表等)
-
-
提交切片规划文件到YARN,YARN上的MrMaster就可以根据切片规划文件计算MapTask个数
FileInputFormat切片机制:
简单的按照文件的内容长度进行切片
切片大小,默认等于block大小
切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
eg:
file1.txt 300M 经过FileInputFormat切片机制运算后,切片数量为 3片
file2.txt 1M 经过FileInputFormat切片机制运算后,切片数量为 1片
源码中计算切片大小公式
Math.max(minSize,Math.min(maxSize,blockSize))
mapreduce.input.fileinputformat.split.minsize=1
mapreduce.input.fileinputformat.split.maxsize=Long.MaxValue
默认情况下,切片大小=blockSize
切片大小设置
maxSize : 值比BlockSize小,切片大小变小,值为maxSize.
minSize : 值比BlockSize大,切片大小比blockSize大,值为minSize.
获取切片信息API
//获取切片文件名称
String pathName = inputSplit.getPath().getName();
//根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit)context.getInputSplit();
CombineTextInputFormat切片机制,跟2.x版本不一样
Hadoop默认TextInputFoemat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样会产生大量的MapTask,效率低下.
CombineTextInputFormat应用场景:
用于小文件过多的场景,可以将过多的小文件从逻辑上规划到一个切片中,交给一个MapTask处理.
虚拟存储切片最大值设置:
//根据实际情况来设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4 * 1024 *1024); //4M
2.x版本CombineTextInputFormat切片机制
将输入目录下的所有文件大小,依次和设置的 setMaxInputSplitSize 的值比较,如果不大于设置的最大值,逻辑上划分为一块.如果输入文件大于设置最大值的2倍,按照最大值切块,最后余数数据大小超过设置的最大值且不大于最大值的2倍,此时将文件均分成2个虚拟存储块(防止出现太小的切片).
eg:
setMaxInputSplitSize 值为: 4M
输入文件大小为: 8.02M
第一次判断比4M的2倍大,切分4M,还剩4.02M;
第二次判断比4M大但是比4M的2倍小,此时均分成2个 2.01M的逻辑文件
所以最终切片为 : 4M , 2.01M , 2.01M
3.x版本CombineTextInputFormat切片机制
默认使用 TextInputFormat 切片机制, 不做任何处理,运行上面的WordCount案例程序( 编写WordCount案例 )
TextInputFormat默认切片数为4
准备4个小文件
file1 1k
file2 1k
file3 1k
file4 1k
PS : 如果你看到这里,想要作者源码调试 : 可以去我GitHub/码云上拉取,如有问题,csdn可以在下面评论区留言,公众号,可以点击公众号中的 [关于我] 那里面有作者的微信,欢迎各位读者交流学习相关问题,其他作者不回
GitHub : https://github.com/al00000/rep 码云 : https://gitee.com/al00000/rep
本文代码目录位置 rep/hadoop-demo , 文末也有GitHub/码云地址
上面是FileInputFormat使用的默认TextInputFormat切片机制,切片为4 ;
CombineTextInputFormat 切片机制默认切片.不设置切片最大值情况
CombineTextInputFormat默认切片数为1
准备4个小文件
file1 1k
file2 1k
file3 1k
file4 1k
此案例只是源码分析,开发不建议,使用了 CombineTextInputFormat 肯定要配合 最大值 处理小文件
WordCountDriver 类仅添加CombineTextInputFormat切片配置:
// 测试1
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
// 设置切片机制CombineTextInputFormat
job.setInputFormatClass(CombineTextInputFormat.class);
debug 源码跟进:
// 这一行不要点太快跳过了,进去才知道怎么切分的
List<InputSplit> splits = input.getSplits(job);
// CombineTextInputFormat切片核心源码
@Override
public List<InputSplit> getSplits(JobContext job)
throws IOException {
long minSizeNode = 0;
long minSizeRack = 0;
long maxSize = 0;
Configuration conf = job.getConfiguration();
// ...
// all the files in input set
List<FileStatus> stats = listStatus(job); //案例中此时文件数为4
List<InputSplit> splits = new ArrayList<InputSplit>();//用来装所有切片信息
// ...
//这里面才是切片逻辑.案例中会走这里 pools.Size()=0
// create splits for all files that are not in any pool.
getMoreSplits(job, stats, maxSize, minSizeNode, minSizeRack, splits);
--->createSplits(nodeToBlocks, blockToNodes, rackToBlocks, totLength,
maxSize, minSizeNode, minSizeRack, splits);
--->addCreatedSplit(splits, getHosts(racks), validBlocks);
// 由此步可以看出 不设置 setMaxInputSplitSize 最大值
// 永远都只有一个CombineFileSplit对象,也就是只有一个切片
// 下面这2步很关键
--->CombineFileSplit thissplit = new CombineFileSplit(fl, offset,
length, locations.toArray(new String[0]));
--->splitList.add(thissplit);
//...
return splits;
}
设置 setMaxInputSplitSize 最大值 后切片情况 :
CombineTextInputFormat设置切片最大值后,切片数为1
准备4个大小不均文件 , 不要管文件内容(或者你弄个几M或几十M的文本),这里测试,主要分析源码切片流程
apache-ant-1.10.8-bin.tar.gz 6.65M
googletest-1.10.x.tar.gz 883K
apache-maven-3.6.3-bin.tar.gz 9.06M
hadoop-3.1.3-src.tar.gz 28.4M
核心思想 : 循环所有文件累计判断超过最大值没有,超过时就包含前面所有不超过的文件划分为一个切片 ; 不超过就循环下一个文件判断
WordCountDriver 类添加CombineTextInputFormat切片配置 && 设置切片最大值:
// 测试2
// 设置切片机制CombineTextInputFormat
job.setInputFormatClass(CombineTextInputFormat.class);
// 虚拟存储切片最大值设置4M
CombineTextInputFormat.setMaxInputSplitSize(job, 4 * 1024 * 1024);//4M
// 这一行不要点太快跳过了,进去才知道怎么切分的
List<InputSplit> splits = input.getSplits(job);
// CombineTextInputFormat切片核心源码
@Override
public List<InputSplit> getSplits(JobContext job)
throws IOException {
long minSizeNode = 0;
long minSizeRack = 0;
long maxSize = 0;
Configuration conf = job.getConfiguration();
// ...
// all the files in input set
List<FileStatus> stats = listStatus(job); //案例中此时文件数为4
List<InputSplit> splits = new ArrayList<InputSplit>();//用来装所有切片信息
// ...
//这里面才是切片逻辑.案例中会走这里 pools.Size()=0
// create splits for all files that are not in any pool.
getMoreSplits(job, stats, maxSize, minSizeNode, minSizeRack, splits);
--->createSplits(nodeToBlocks, blockToNodes, rackToBlocks, totLength,
maxSize, minSizeNode, minSizeRack, splits);
--->addCreatedSplit(splits, getHosts(racks), validBlocks);
// 此处设置的切片最大值,上面那个没有设置最大值为0,就直接把所有文件切1片
// 如果累计拆分大小超过最大值, 则创建此拆分====这个if很关键
// 这里跟2.x版本不一样,这里相当于是按照设置的最大值切分的
--->if (maxSize != 0 && curSplitSize >= maxSize) {
// 创建切片添加到splits集合中,继续进去,看下
addCreatedSplit(splits, Collections.singleton(node), validBlocks);
--->CombineFileSplit thissplit = new CombineFileSplit(fl, offset,
length, locations.toArray(new String[0]));
--->splitList.add(thissplit);
// ...
break;
}
//...
return splits;
}
eg:
下面这个案例是测试各种情况文件,详情看截图,就明白
FileInputFormat实现类
FileInputFormat是一个抽象类继承InputFormat抽象类
运行MapReduce程序时,输入文件类型很多(.sql .txt .log …),MapReduce如何处理???
FileInputFormat常见实现类 :
CombineFileInputFormat
CombineTextInputFormat
CombineTextInputFormat
FixedLengthInputFormat
KeyValueTextInputFormat
NLineInputFormat
TextInputFormat
TextInputFormat实现类
TextInputFormat是FileInputFormat的实现类.按行读取每条记录.键是存储改行在整个文件中的起始字节偏移量,LongWritable类型. 值时这行的内容,Text类型.
eg:
wordcount.txt
idea hadoop dev dev
count count this
hello word
读取每行记录的键值对:
<0,idea hadoop dev dev>
<21,count count this>
<39,hello word>
KeyValueTextInputFormat实现类
每一行均为一条记录,被分隔符分隔为key,value. 可以在驱动类中设置 conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPARATOR,"\t"); 分隔符.默认分隔符就是 \t ,key则是读取的每行 “\t” 左边的Text内容,右边的则是value.
eg:
word_count_test_tab.txt 这里测试按照空格切分
aaa adoopdounhis
idea doop
oun tcuthis
vvv tcuthis
aaa tcuthis
读取每行记录的键值对:
<aaa,adoopdounhis>
<idea,doop>
<oun,tcuthis>
<vvv,tcuthis>
<aaa,tcuthis>
编写注意:
// 1. extends Mapper时 前面的两个输入类型都为 Text,
// 2. 设置切割符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
// 3. 设置 KeyValueTextInputFormat 输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);
代码案例:
KeyValueDriver 类
package com.example.mapreduce.kv;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.KeyValueLineRecordReader;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.File;
import java.io.IOException;
/**
* <p>
* KeyValueDriver
* </p>
*
* @author f
* @description KeyValueDriver
*/
public class KeyValueDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
String outputPath ="./hadoop-demo/output/kv";
String inputFilePath = "./hadoop-demo/mapreduce_test_input_file/kv/word_count_test_kv.txt";
System.out.printf("运行前删除output目录文件夹 : %s%n",FileUtil.fullyDelete(new File(outputPath)));
Configuration conf = new Configuration();
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPARATOR," ");
// 1 获取配置信息以及封装任务
Job job = Job.getInstance(conf);
// 2 设置jar加载路径
job.setJarByClass(KeyValueDriver.class);
// 3 设置map和reduce类
job.setMapperClass(KeyValueMapper.class);
job.setReducerClass(KeyValueReducer.class);
// 4 设置map输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(KeyValueTextInputFormat.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(inputFilePath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
// 7 提交
boolean result = job.waitForCompletion(true);
System.out.printf("%nresult : %s%n", result);
System.exit(result ? 0 : 1);
}
}
KeyValueMapper类
package com.example.mapreduce.kv;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* <p>
* KeyValueMapper
* </p>
*
* @author f
* @description KeyValueMapper
*/
public class KeyValueMapper extends Mapper<Text, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
System.out.printf("<%s,%s>\n",key,value);
k.set(key);
context.write(k, v);
}
}
KeyValueReducer类
package com.example.mapreduce.kv;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* <p>
* KeyValueReducer
* </p>
*
* @author f
* @description KeyValueReducer
*/
public class KeyValueReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
NLineInputFormat实现类
NLineInputFormat 表示map进程处理的 InputSplit 不再按Block块去划分,而是按NLineInputFormat指定的行数N来划分. 键值对key-value 同TextInputFormat.
切片数 = 总行数 / N 或者 切片数 = (总行数 / N) + 1 (不整除情况)
eg:
nline.txt
test aaa aaa
bbb ccc vvv ddd www qqq aaa ttt
test aaa aaa
bbb ccc vvv ddd www qqq aaa ttt
test aaa aaa
bbb ccc vvv ddd www qqq aaa ttt
test aaa aaa
bbb ccc vvv ddd www qqq aaa ttttest aaa aaa
bbb ccc vvv ddd www qqq aaa ttt
代码编写注意:
// 1. 设置切片方式
job.setInputFormatClass(NLineInputFormat.class);
// 2. 设置每个切片InputSplit中划分5条记录
NLineInputFormat.setNumLinesPerSplit(job, 5);
此案例分片数 : 2
代码案例:
NLineInputDriver类
package com.example.mapreduce.nl;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.KeyValueLineRecordReader;
import org.apache.hadoop.mapreduce.lib.input.NLineInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.File;
import java.io.IOException;
/**
* <p>
* NLineInputDriver
* </p>
*
* @author f
* @description NLineInputDriver
*/
public class NLineInputDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
String outputPath ="./hadoop-demo/output/nline";
String inputFilePath = "./hadoop-demo/mapreduce_test_input_file/nline/nline.txt";
System.out.printf("运行前删除output目录文件夹 : %s%n",FileUtil.fullyDelete(new File(outputPath)));
Configuration conf = new Configuration();
// 1 获取配置信息以及封装任务
Job job = Job.getInstance(conf);
// 2 设置jar加载路径
job.setJarByClass(NLineInputDriver.class);
// 3 设置map和reduce类
job.setMapperClass(NLineInputMapper.class);
job.setReducerClass(NLineInputReducer.class);
// 4 设置map输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(NLineInputFormat.class);
// 7设置每个切片InputSplit中划分三条记录
NLineInputFormat.setNumLinesPerSplit(job, 5);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(inputFilePath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
// 7 提交
boolean result = job.waitForCompletion(true);
System.out.printf("%nresult : %s%n", result);
System.exit(result ? 0 : 1);
}
}
NLineInputMapper类
package com.example.mapreduce.nl;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* <p>
* NLineInputMapper
* </p>
*
* @author f
* @description NLineInputMapper
*/
public class NLineInputMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1. 获取一行
String line = value.toString();
// 2. 切割
String[] words = line.split(" ");
// 3. 读取的这一行,按空格切分的都写出去 <xxx,n>的形式
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
NLineInputReducer类
package com.example.mapreduce.nl;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* <p>
* NLineInputReducer
* </p>
*
* @author f
* @description NLineInputReducer
*/
public class NLineInputReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
NLineInputReducer源码分析:
这源码都比较简单,简单提一下,断点进去就明白了
// 获取切片数
public List<InputSplit> getSplits(JobContext job) throws IOException {
List<InputSplit> splits = new ArrayList<InputSplit>();
int numLinesPerSplit = getNumLinesPerSplit(job);
for (FileStatus status : listStatus(job)) {
// getSplitsForFile 这个方法下面就是切片和添加切片信息逻辑--核心
splits.addAll(getSplitsForFile(status,
job.getConfiguration(), numLinesPerSplit));
//-->进去
//...
// 第一行开始读,每次读一行
while ((num = lr.readLine(line)) > 0) {
numLines++;
length += num;
//读到行数和设置的行数相等时,切片并添加
if (numLines == numLinesPerSplit) {
splits.add(createFileSplit(fileName, begin, length));
begin += length;
length = 0;
numLines = 0;
}
}
//不是整除的,最后的几行,分1片并添加
if (numLines != 0) {
splits.add(createFileSplit(fileName, begin, length));
}
//...
}
return splits;
}
自定义FileInputFormat
SequenceFile文件是Hadoop用来存储二进制的<K,V>文件格式,SequenceFile里面存储着多个文件,
K = 文件路径 + 名称
V = 文件内容
案例 : 实现自定义FileInputFormat合并多个小文件为一个SequenceFile文件.
eg:
// 1. 自定义类继承 FileInputFormat ,自定义类继承Reader去处理读文件逻辑
// 2. 重写 isSplitable() 返回false不可切割,true可切割,这个案例要返回false
// 3. 重写 createRecordReader() 创建自定义 RecordReader 对象
// 4. I/O一次读取一个文件输出给value,把所有文件封装value中
// 5. 获取 文件路径信息和名称,设置key
// 6. Driver中设置inputFormat类为 自定义的FinleInputFormat
//job.setInputFormatClass(CustomInputFormat.class);
// 7. Driver中设置输出 outputFormat 类为 SequenceFileOutputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
代码案例:
CustomInputFormat 类
package com.example.mapreduce.custom_input_format;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
/**
* <p>
* CustomInputFormat
* </p>
*
* @author f
* @description CustomInputFormat
*/
public class CustomInputFormat extends FileInputFormat<Text, BytesWritable> {
@Override
protected boolean isSplitable(JobContext context, Path filename) {
//返回false,表示不可切割
return false;
}
@Override
public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
//自定义一个RecordReader去处理读取文件逻辑
CustomRecordReader customRecordReader = new CustomRecordReader();
customRecordReader.initialize(split,context);
return customRecordReader;
}
}
CustomInputFormatDriver 类
package com.example.mapreduce.custom_input_format;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.NLineInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.File;
import java.io.IOException;
/**
* <p>
* CustomInputFormatDriver
* </p>
*
* @author f
* @description CustomInputFormatDriver
*/
public class CustomInputFormatDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
String outputPath ="./hadoop-demo/output/custom_input_format";
System.out.printf("运行前删除output目录文件夹 : %s%n",FileUtil.fullyDelete(new File(outputPath)));
Configuration conf = new Configuration();
// 获取配置信息以及封装任务
Job job = Job.getInstance(conf);
// 设置jar加载路径 map和reduce类
job.setJarByClass(CustomInputFormatDriver.class);
job.setMapperClass(CustomInputMapper.class);
job.setReducerClass(CustomInputReducer.class);
// 设置map输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
// 设置最终输出KV类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
// 设置自定义 CustomInputFormat
job.setInputFormatClass(CustomInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
// 设置输入和输出路径
FileInputFormat.setInputPaths(job,
new Path("./hadoop-demo/mapreduce_test_input_file/custom_input_format/file1"),
new Path("./hadoop-demo/mapreduce_test_input_file/custom_input_format/file2"),
new Path("./hadoop-demo/mapreduce_test_input_file/custom_input_format/file3"),
new Path("./hadoop-demo/mapreduce_test_input_file/custom_input_format/file4")
);
FileOutputFormat.setOutputPath(job, new Path(outputPath));
// 提交
boolean result = job.waitForCompletion(true);
System.out.printf("%nresult : %s%n", result);
System.exit(result ? 0 : 1);
}
}
CustomInputMapper 类
package com.example.mapreduce.custom_input_format;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* <p>
* CustomInputMapper
* </p>
*
* @author f
* @description CustomInputMapper
*/
public class CustomInputMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> {
Text k = new Text();
BytesWritable v = new BytesWritable();
@Override
protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
context.write(k, v);
}
}
CustomInputReducer 类
package com.example.mapreduce.custom_input_format;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* <p>
* CustomInputReducer
* </p>
*
* @author f
* @description CustomInputReducer
*/
public class CustomInputReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
context.write(key,values.iterator().next());
}
}
CustomRecordReader 类
package com.example.mapreduce.custom_input_format;
import java.io.IOException;
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 org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
/**
* @author f
* @description 自定义Reader读取文件
*/
public class CustomRecordReader extends RecordReader<Text, BytesWritable> {
private Configuration config;
private FileSplit split;
//读取标志, 为什么要这个标志而不是直接返回true/false呢?
//要看下 Mapper 中的 run 方法;因为run方法一次一次读,
// 直到没有数据,判断依据来自 nextKeyValue() 的返回值
// 如果直接返回true则只读完,永远都是告诉 Mapper 还以读,
// 这样一直死循环读下去了;直接返回false,则读完一次之后就告诉Mapper不能读了,后面还有文件内容呢,不读了?
// 所以要设置一个标志来告诉Mapper什么时候读完返回false,没有读完就返回true,继续读
// 这个读取标志设置很关键
private boolean hasNext = true;
private final BytesWritable v = new BytesWritable();
private final Text k = new Text();
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.split = (FileSplit) split;
config = context.getConfiguration();
}
@Override
public boolean nextKeyValue() {
if (hasNext) { //读取标志
// 1. 定义缓存区,一次读完整个文件,(小文件)
byte[] buf = new byte[(int) split.getLength()];
FileSystem fs;
FSDataInputStream fis = null;
try {
// 2. 获取文件路径
Path path = split.getPath();
fs = path.getFileSystem(config);
// 3. 读取数据
fis = fs.open(path);
// 4. 读完全部内容到buf中
IOUtils.readFully(fis, buf, 0, buf.length);
// 5. 将buf中的内容设置到 value中
v.set(buf, 0, buf.length);
// 6. 获取文件路径和名称
String name = path.toString();
// 7. 设置输出的key值
k.set(name);
} catch (Exception ignored) {
} finally {
IOUtils.closeStream(fis);
}
hasNext = false;
return true;
}
return false;
}
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return k;
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return v;
}
//进度条 : 获取当前进度
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
@Override
public void close() throws IOException {
}
}
Mapper run源码 :
/**
* Expert users can override this method for more complete control over the
* execution of the Mapper.
* @param context
* @throws IOException
*/
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
//nextKeyValue() 返回true表示还要继续读,false就是读完了
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
MapReduce工作流程
流程详解 :
上面的流程是整个MapReduce最全工作流程, 但是Shuffle过程只是从第7步开始到第16步结束, 具体Shuffle过程详解, 如下:
① MapTask收集我们的map()方法输出的kv对, 放到内存缓冲区中
② 从内存缓冲区不断溢出本地磁盘文件, 可能会溢出多个文件
③ 多个溢出文件会被合并成大的溢出文件
④ 在溢出过程及合并的过程中, 都要调用Partitioner进行分区和针对key进行排序
⑤ ReduceTask根据自己的分区号, 去各个MapTask机器上取相应的结果分区数据
⑥ ReduceTask会取到同一个分区的来自不同MapTask的结果文件, ReduceTask会将这些文件再进行合并( 归并排序)
⑦ 合并成大文件后, Shuffle的过程也就结束了, 后面进入ReduceTask的逻辑运算过程( 从文件中取出一个一个的键值对Group, 调用用户自定义的reduce()方法)
注意 : Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率, 原则上说, 缓冲区越大, 磁盘io的次数越少, 执行速度就越快.
缓冲区的大小可以通过参数调整, 参数: io.sort.mb默认100M.
个人理解ps :
1. 待处理文本 aaa.txt 230M
2. 客户端获取待处理文件,获取配置,规划分片信息,最后submit()给Yarn集群/本地Yarn
提交文件:{job.split,wc.jar,job.xml,job.splitmetainfo 以及每个文件对应的crc校验文件}
3. MrAppMaster & NodeManager 计算MapTask数量,默认TextFileInputFormat会切2片,所以有2个MapTask
4. MapTask执行 map(K,V) & reduce(K2,V2) 方法
TextInputFormat#map(K,V)-->RecorderReader#reader(aaa.txt)--->context.write(K2,V2)--->OutputCollector--->环形缓冲区(默认100M,超过80%后落盘,反向写,后面详细介绍)--->分区排序(字典排序,快排)--->reduce(K2,V2)--->MrAppMaster启动对应分区个数的ReduceTask--->ReduceTask把每个分区(有序文件)进行归并排序--->ReduceTask合并自己任务下所有分区成一个大文件(有序) & 计算--->context.write(K3,V3)--->OutPutFormat--->RecoderWriter#write(K3,V3)--->part-r-00000
Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle.
Partition分区
默认分区是根据hashCode对ReduceTask个数取模得到的,用户无法控制哪个key存储到哪个分区.
源码 :
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
自定义Partitioner
- 继承Partitioner,重写getPartition()方法
- job中设置自定义的Partitioner
- job中设置ReduceTask数量
分区特点 :
ReduceTask数量 > getPartition()结果数 ,会多产生几个空的输出文件 part-r-00000;
1 < ReduceTask数量 < getPartition()结果数 ,部分数据无法存储,抛异常 IOException: Illegal partition for xxx;
ReduceTask数量 = 1 ,不管MapTask输出有多少个分区文件,最终结果都交给这一个ReduceTask,最终只会产生一个结果文件 part-r-00000;
分区号必须从0开始累加.
eg:
// 自定义分区数 : 3
job.setNumReduceTasks(1); // 运行正常,只产生一个输出文件 part-r-00000
job.setNumReduceTasks(5); // 正常运行,多2个空文件
job.setNumReduceTasks(1); // 抛异常
代码核心设置:
//设置自定义分区类和ReduceTask数量
job.setPartitionerClass(PhonePartition.class);
job.setNumReduceTasks(5);
目录 hadoop-demo/src/main/java/com/example/mapreduce/partition/
项目地址:文末
WritableComparable排序
排序是MapReduce中最重要的操作之一.
MapTask和Reduce均会对数据按照key进行排序.该操作属于Hadoop的默认行为.任何应用程序中的数据均会被排序,不管逻辑上是否需要.
默认排序是按照字典顺序排序,且实现该排序的方法是快排.
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序.
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中.如果磁盘上文件数目达到一定阈值,进行一次归并排序以生成一个更大文件;若果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上.当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序.
Haoop中排序分类
-
部分排序
MapReduce根据输入记录的<K,V>键值对,对数据集排序,保证输出的每个文件内部有序.
-
全排序
最终输出结果只有一个文件,且内部排序. 实现方式只设置一个ReduceTask.但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全违背了MapReduce提供的并行架构.
-
辅助排序(GroupingComparator分组)
在Reduce端对key进行分组.应用于 : 在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不同)的key进入到同一个reduce方法,可以采用分组排序.
-
二次排序
自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序.
自定义WriteableComparable
原理 : key为bean对象,需要实现WritableComparable接口重写comparTo方法就可以实现排序.
WritableComparable 接口 继承 Writable,Commparable; 排序就重写comparTo()方法
代码核心设置:
//设置自定义分区类和ReduceTask数量
job.setPartitionerClass(PhonePartition.class);
job.setNumReduceTasks(5);
// WritableComparable UML类图关系如上图
public class UserLog implements WritableComparable<UserLog>{
//...
//按需重写排序方法
@Override
public int compareTo(UserLog userLog) {
//按照sumFlow倒序排列
return Long.compare(userLog.getSumFlow(), sumFlow);
}
}
代码目录 hadoop-demo/src/main/java/com/example/mapreduce/writable/
项目地址:文末
Combiner合并
Combiner继承Reducer是MR程序中的Mapper和Reducer之外的一种组件.
Combiner在每一个MapTask所在的节点运行;Reducer接收全局所有Mapper输出结果;
Combiner的意义在于对每一个MapTask的输出惊醒局部汇总,以减小网络传输量.
Combiner能够应用的前提是不能影响最终业务逻辑,而且,Combiner的输出<K,V>要对应Reducer<K,V>
核心思想 eg:
Combiner Reducer
1 + 3 = 4 1 + 3 + 5 + 7 = 16
4 + 5 = 9
9 + 7 = 16
自定义Combiner实现
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1 局部汇总
sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
代码案例目录 : hadoop-demo/src/main/java/com/example/mapreduce/wc_combiner/
项目地址:文末
还有一部分,后续会陆续完善整个技术栈