Hadoop学习
https://www.bilibili.com/video/BV1Qp4y1n7EN?p=21
概述
Hapdoop是用于分布式存储海量数据的架构,它可以将一个很大的文件的数据分散到多个服务器中进行存储。
Hdfs:负责数据存储
DataNode:用于实际存放数据
NameNode:记录一个文件的数据存放在哪个DataNode的哪个位置上,是一个文件在集群查找的目录。
NameNode2:作为NameNode的从节点,进行定期对主节点的数据进行备份,防止主节点故障后数据丢失,同时可以在主节点出问题后代替主节点提供服务。
Yarn:负责资源调度和数据计算
ResourceManager:资源的总管理者,负责管理一部分结点的所有资源分配,负责不同结点间的资源调度
NodeManager:单个结点的资源管理者,负责根据ResourceManager的调度分配所在结点的资源
ApplicationMaster:任务执行者,负责实际任务的执行
Container:任务运行的环境,包含CPU,内存,磁盘,网络等任务运行所需的资源,由NodeManager来分配
MapReduce
Map:将任务分解成若干子任务,并行执行
Reduce:统计子任务的执行结果,得到问题的答案
如何Map和如何Reduce由ApplicationMaster来负责管理,Map和Reduce同样是运行在容器中
Hapdoop解决方案
环境配置
配置虚拟机IP地址
防止IP动态变化
设置子网IP,虚拟机设置在同一个子网当中:
设置网关:
设置宿主机WIN10中对应虚拟机的IP:
下面这个设置的网段要包含我们使用的ip地址
修改虚拟机的IP地址,将动态分配改成静态,同时设置DNS
vim /etc/sysconfig/network-scripts/ifcfg-ens33
修改位置:
sudo passwd 设置root的密码
centos镜像
http://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/
设置域名:
vim /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.10.100 hadoop100
192.168.10.101 hadoop101
192.168.10.102 hadoop102
192.168.10.103 hadoop103
192.168.10.104 hadoop104
192.168.10.105 hadoop105
192.168.10.106 hadoop106
192.168.10.107 hadoop107
192.168.10.108 hadoop108
然后reboot重启
查看ip地址:ifconfig
ping 192.168.10.100成功
安装xshell:
点击新建,然后输入虚拟机登录的账号和密码即可
安装软件环境
yum install -y epel-release 下载额外的软件包
关闭防火墙:
systemctl stop firewalld
systemctl disable firewall.service
vim /etc/sudoers
设置lth的权限:
注意设置免密登录的顺序不能反,要放到%wheel的下面
创建两个文件夹并设置所有者:
卸载原有的JDK,以便安装我们自己的版本:
克隆三台虚拟机:hadoop102,hadoop103,hadoop104
按照上面的方式配置ip和hostname
然后xshell也配置相应的窗口
安装jdk:
tar -zxvf xxxx.tar.gz -C /opt/module
然后进入/etc/profile.d中,添加一个.sh文件,这个目录下的.sh文件都会被开机执行,这个逻辑保存在/etc/profile中
设置环境变量:
一定要安装jdk8
[root@hadoop102 profile.d]# cat my_env.sh
#JAVA_HOME
export JAVA_HOME=/opt/module/jdk8
export PATH=$PATH:$JAVA_HOME/bin
source /etc/profile 让配置生效
安装hadoop
[root@hadoop102 profile.d]# cat my_env.sh
#JAVA_HOME
export JAVA_HOME=/opt/module/jdk-13
export PATH=$PATH:$JAVA_HOME/bin
#HADOOP_HOME
export HADOOP_HOME=/opt/module/hadoop
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
配置文件:
bin:配置文件和hadoop命令
sbin:集群启动和关闭的命令
lib:动态链接库
share:示例程序:
执行wordcount实例程序:
hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.2.1.jar wordcount wcinput/ ./wcoutput
wcinput是输入路径的文件夹 ./wcoutput是输出路径
输出路径指定的文件不能存在
进入wcoutput,查看执行结果
![(https://s2.loli.net/2022/04/05/Zr3CcQYk5xoftFs.png)
搭建集群
将jdk,hadoop都复制到其他虚拟机上
这里我们在hosts中对域名进行了映射,所以我们直接写域名即可,-r表示递归
这个是发送方式,我们也可以在hadoop103上把文件拉过来:
scp -r root@hadoop102:/opt/module/* ./
我们也可以通过这种方式将hadoop102的文件复制到hadoop104上
scp -r root@hadoop102:/opt/module/* root@hadoop104:/opt/module/
同步修改过程:
rsync -av root@hadoop102:/opt/module/hadoop/ hadoop/
rsync -av 数据源 被同步的数据
scp是复制全部文件,而rsync是同步修改的数据
创建我们自己的脚本命令简化同步过程
在PATH下的命令我们都可以直接使用而不用加上路径参数,其中包含home/lth/bin,我们在这个路径下创建我们的新命令即可
shell命令补充:
$# :获取参数个数
$@:所有参数的集合
pwd:获取当前文件夹的路径名
[-e $file] 判断文件是否存在
dirname file :获得file在哪个目录
ln -s aaa bbb :为aaa创建一个快捷方式,名字叫bbb(软链接)
cd -P bbb 通过bbb进到实际所在的目录
basename ./a.txt 通过路径获取文件的原名
mkdir -p xxx 创建文件夹,如果已经存在该文件夹就忽略
脚本:
#!/bin/bash
#1. 判断参数个数
if [ $# -lt 1 ]
then
echo Not Enough Arguement!
exit;
fi
#遍历所有机器
for host in hadoop102 hadoop103 hadoop104
do
echo ========== $host ================
#3. 遍历所有目录
for file in $@
do
if [ -e $file ]
then
pdir=$(cd -P $(dirname $file); pwd)
fname=$(basename $file)
ssh $host "mkdir -p $pdir"
rsync -av $pdir/$fname $host:$pdir
else
echo $file does not exits!
fi
done
done
通过这个脚本就可以帮助我们同步相关的配置,例如环境变量
配置免密登录
ssh-keygen -t rsa 设置密码对
ssh-copy-id hadoop103 配置免密登录
集群搭建
集群搭建注意事项:
也就是NameNode,SecondNameNode,ResourceManager都比较消耗内存,所以这三个应当放在三个服务器上,然后每个服务器还能运行一个DataNode和NodeManager
默认配置文件:
我们可以自定义的配置文件:
可以修改这些配置文件,满足项目的需要
根据需求,这个结点上配置NameNode和DataNode
配置core-site
<configuration>
<property>
<!-- NameNode的地址 -->
<name>fs.defaultFS</name>
<value>hdfs://hadoop102:8020</value>
</property>
<property>
<!-- 数据的存储目录 -->
<name>hadoop.tmp.dir</name>
<value>/opt/module/hadoop/data</value>
</property>
</configuration>
(暂时先配置这两个,我的hadoop文件夹就叫hadoop)
配置hdfs-site
这个主要是配置图形界面
<configuration>
<property>
<name>dfs.namenode.http-address</name>
<value>hadoop102:9870</value>
</property>
<property>
<name>dfs.namenode.secondary.http-address</name>
<value>hadoop104:9868</value>
</property>
</configuration>
配置yarn-site
hadoop3.2以上可能就不需要进行环境变量的继承
aux-services是指定算法,默认为空,推荐是mapreduce_shuffle
<configuration>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>hadoop103</value>
</property>
<property>
<name>yarn.nodemanager.env-whitelist</name>
<value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
</property>
</configuration>
配置mapred-site
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
写好后用我们的xsync脚本分发配置文件到其他的服务器(因为我们的配置文件是通用的)
xsync hadoop/
配置主机名称
/etc/hadoop/workers
hadoop102
hadoop103
hadoop104
注意不能有多余的空格和回车
格式化
注意不能多次格式化,多次格式化要删除data和log
hdfs namenode -format 格式化namenode
格式化后会多出data和log,用来保存数据和日志
data里面有一个VERSION用户保存当前集群的版本号:
#Tue Apr 05 22:45:50 CST 2022
namespaceID=674222065
blockpoolID=BP-1168166571-192.168.10.102-1649169950849
storageType=NAME_NODE
cTime=1649169950849
clusterID=CID-be03e330-0a09-4133-a513-2b0326018ba2
layoutVersion=-65
启动集群
然后进入sbin目录下启动集群,因为我们配置了ssh免密登录,并且配置了哪些集群应当启动哪些服务,所以在其中一个结点启动即可。
启动集群不能用root用户,并且lth要有相应的权限,并且lth要能和其他虚拟机免密登录,所以直接更改所有者:
sudo chown -R lth:lth *
chmod -R 755 *
在hadoop102启动:sbin/start-hdfs.sh
在hadoop103启动:sbin/start-yarn.sh
图形界面:
datanode:http://hadoop102:9870/
yarn:http://hadoop103:8088/
集群测试
hadoop fs -mkdir /wcinput 创建文件夹
hadoop fs -put word.txt /wcinput 上传文件
数据的存储位置:
一个结点的数据会在其他结点存储三份,作为备份,只要有一份存活就能下载下来
执行一个命令:
输入输出都是集群的路径
集群崩溃处理
关掉所有进程
删除所有的log和data文件夹
然后按照规则重新启动
每个结点的datanode都会被namenode记录一个版本号,如果没把数据删干净,会残留历史版本号,而格式化后会出现新集群版本号,两者不对应会启动失败
配置运行历史记录
在mapred-site里配置:
在后台启动进程:
bin/mapred --daemon start historyserver
日志记录
在yarn-site加入一下内容:
修改配置后需要重启yarn:
历史服务器:
mapred --daemon stop historyserver
yarn相关:
sbin/stop-yarn.sh
单独启动一个结点:
hdfs --daemon start xxx 用于启动datanode,namenode,secondnamenode
yarn --daemon start xxx 用于启动yarn相关的结点:nodemanager,resourcemanager
编写启动和关闭脚本,进行自动化运维:
#!/bin/bash
if [ $# -lt 1 ]
then
echo "No Args Input..."
exit ;
fi
case $1 in
"start")
echo " =================== 启动 hadoop 集群 ==================="
echo " --------------- 启动 hdfs ---------------"
ssh hadoop102 "/opt/module/hadoop/sbin/start-dfs.sh"
echo " --------------- 启动 yarn ---------------"
ssh hadoop103 "/opt/module/hadoop/sbin/start-yarn.sh"
echo " --------------- 启动 historyserver ---------------"
ssh hadoop102 "/opt/module/hadoop/bin/mapred --daemon start historyserver"
;;
"stop")
echo " =================== 关闭 hadoop 集群 ==================="
echo " --------------- 关闭 historyserver ---------------"
ssh hadoop102 "/opt/module/hadoop/bin/mapred --daemon stop historyserver"
echo " --------------- 关闭 yarn ---------------"
ssh hadoop103 "/opt/module/hadoop/sbin/stop-yarn.sh"
echo " --------------- 关闭 hdfs ---------------"
ssh hadoop102 "/opt/module/hadoop/sbin/stop-dfs.sh"
;;
*)
echo "Input Args Error..."
;;
esac
myhadoop stop 关闭
myhadoop start 启动
查看启动状态:
#!/bin/bash
for host in hadoop102 hadoop103 hadoop104
do
echo =============== $host ===============
ssh $host jps
done
jspall 即可查看端口号
1、常用端口号
hadoop3.x
HDFS NameNode 内部通常端口:8020/9000/9820
HDFS NameNode 对用户的查询端口:9870
Yarn查看任务运行情况的:8088
历史服务器:19888
hadoop2.x
HDFS NameNode 内部通常端口:8020/9000
HDFS NameNode 对用户的查询端口:50070
Yarn查看任务运行情况的:8088
历史服务器:19888
2、常用的配置文件
3.x core-site.xml hdfs-site.xml yarn-site.xml mapred-site.xml workers
2.x core-site.xml hdfs-site.xml yarn-site.xml mapred-site.xml slaves
HDFS
概述
将一个大文件存储在不同的datanode,由namenode来记录存储位置。HDFS适合一次写入,多次读取的场景,保存到HDFS后,只能在文件后追加数据,不能修改数据。
HDFS适合存储和处理大数据,而不适合存储大量小数据,因为大量小数据会快速占满namenode的存储空间
块大小
一般传输速率在100MB/s的机械硬盘用128M,而速率在200~300MB/S的硬盘可以选择256M,因为尽量要将传输速率控制在1s左右
块太小:寻址时间长
块太大:传输时间长,处理困难
块大小注意取决于磁盘传输速率
HDFS shell
hadoop fs 和hdfs dfs等价
hadoop fs -mkdir xxx 创建文件夹
hadoop fs -mv xxx /dir 剪切到dir文件夹
hadoop fs -copyFromLocal xxx /dir 复制到dir ,等价于:
hadoop fs -put xxx /dir 复制到dir
hadoop fs -appendToFile xxx /dir/yyy 将xxx追加到dir下yyy的末尾
hadoop fs -copyToLocal xxx
hadoop fs -get xxx yyy 复制文件
(其实用法和Linux差不多……)
27:单个大小,81总大小(一共有三份数据)
hadoop fs -setrep 10 xxx 设置副本数量,每个服务器最多有一个副本,因而即使设置为10个副本,当前只有3台服务器时,仍然只会有三份数据,增加服务器数量后才会增加副本数量,最多到10
Maven工程实践
下载windowns版的hadoop,并设置环境变量,然后重启电脑
pom依赖:
<dependencies>
<dependency>
<!-- hadoop版本务必和集群一致-->
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
hadoop版本要和集群版本号一致
使用junit测试:
package com.lth.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* @author 李天航
* 客户端操作
* 1.获取一个客户端对象
* 2.执行相关操作命令
* 3.关闭资源
*/
public class HdfsClient {
static FileSystem fileSystem;
@Before
public void init() throws URISyntaxException, IOException, InterruptedException {
URI uri = new URI("hdfs://hadoop102:8020");
Configuration configuration = new Configuration();
String user="lth";
fileSystem = FileSystem.get(uri, configuration,user);
}
@After
public void close() throws IOException {
fileSystem.close();
}
@Test
public void makeDir() throws URISyntaxException, IOException, InterruptedException {
fileSystem.mkdirs(new Path("/huaguoshan"));
}
}
FS API:
fileSystem.mkdirs(new Path("/huaguoshan")); 创建文件夹
上传文件:
第一个参数表示是否删除源文件,第二个参数表示是否覆盖已有文件,第三个参数表示源文件路径,第四个参数表示目标文件夹和需要改成的名字,如果不写名字就是原名
public void uploadFile() throws IOException {
fileSystem.copyFromLocalFile(false,false,new
Path("src/main/java/com/lth/hdfs/HdfsClient.java"),
new Path("/"));
}
文件副本默认数量为3,定义在hdfs-default.xml中
我们可以在resource目录下再定义一个xml文件,参数优先级高于默认文件:
参数优先级 参数优先级排序:(1)客户端代码中设置的值 >(2)ClassPath 下的用户自定义配置文 件 >(3)然后是服务器的自定义配置(xxx-site.xml)>(4)服务器的默认配置(xxx-default.xml)
设置副本个数:
configuration.set("dfs.replication","2");
下载文件:
第一个参数表示是否删除源文件,第二个参数是hadoop上文件的位置,第三个参数是下载到本地的路径,第四个参数是是否需要CRC校验
public void downloadFile() throws IOException {
fileSystem.copyToLocalFile(false,new Path("/2.java"),new Path("/"),true);
}
删除:
第一个参数表示删除的路径,既可以是文件,也可以是文件夹,第二个参数表示是否递归删除,如果要删除非空文件夹需要设置为true
public void deleteFile() throws IOException {
System.out.println(fileSystem.delete(new Path("/2.java"), true));
}
移动文件(夹)
第一个参数是源文件,第二个参数是目的文件,也能修改文件夹,和Linux一样有修改名称功能
@Test
public void moveFile() throws IOException {
System.out.println(fileSystem.rename(new Path("/2.java"), new Path("/3.java")));
}
获取文件详情:
第一个参数的要查看的路径,第二个参数的是否递归查询
public void getFileDetail() throws IOException {
RemoteIterator<LocatedFileStatus> listFiles = fileSystem.listFiles(new Path("/"),
true);
while (listFiles.hasNext()) {
LocatedFileStatus fileStatus = listFiles.next();
System.out.println("========" + fileStatus.getPath() + "=========");
System.out.println(fileStatus.getPermission());
System.out.println(fileStatus.getOwner());
System.out.println(fileStatus.getGroup());
System.out.println(fileStatus.getLen());
System.out.println(fileStatus.getModificationTime());
System.out.println(fileStatus.getReplication());
System.out.println(fileStatus.getBlockSize());
System.out.println(fileStatus.getPath().getName());
// 获取块信息
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
System.out.println(Arrays.toString(blockLocations));
}
}
获取文件状态:
@Test
public void judgeDirOrFile() throws IOException {
FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
for(FileStatus status:fileStatuses){
if(status.isDirectory()){
System.out.println("dir:"+status.getPath().getName());
}else {
System.out.println("file:"+status.getPath().getName());
}
}
}
写数据流程
选择结点时,采用负载均衡的策略
传输数据时,是给其中一个结点传输,然后再由该节点向其他结点传输数据。
传送,会将数据暂存起来,等到其攒成一个大的packet后再发送,发送完会等待回复ack包后才将内存中的包删除,类似TCP的滑动窗口
结点具体=结点最近公共祖先的距离之和
结点选择原则:
1.选择Client所在本地机器
2.选择另一个机架的机器
3.选择和2号机器同机架的机器
NameNode备份机制
namenode数据只保存在内存中,断电后数据就会丢失,不可靠
而保存在磁盘中计算效率又太低,所以我们采用一个折衷的方法
采用镜像文件+日志文件的形式备份数据,镜像文件保存某一时刻的数据状态,日志文件记录对文件的操作记录,这两个文件合在一起可以还原出当前的数据。集群启动的时候会自动将这两个文件加载进来,还原之前备份的数据。
但是这样的话,过一段时间后日志文件就变得很大,加载数据的效率就会比较低,所以在Edits中的数据满的时候,或者到了一定的时间后,2NN会合并日志文件(Edits)和(fsimage),合并成新的fsimage,合并过程中的新的修改操作会记录在新的Edits中,等合并完成后,NN将合并好的镜像文件拿过来作为新的镜像文件,然后和新的Edits一起记录当前NameNode中的数据。
在进行修改时,要先写日志文件再进行修改。
fsimage会在初始化的时候生成
保存在/opt/module/hadoop/data/dfs/name/current
查看一个fsimage文件:hdfs oiv -p XML -i fsimage_0000000000000000281 see.txt
点击sz下载文件
查看edits文件:hdfs oev -p XML -i edits_0000000000000000040-0000000000000000041 -o ~/see2.txt
edits和fsimage文件会记录编号,开启启动时只会合并大于fsiamge编号的edits文件
默认3600s进行一次合并或者一百万次操作进行合并
datanode默认每6个小时想namenode发送所有的数据块的状态
每3秒发送一次心跳,连续十次没有心态视为datanode挂掉:
dfs.heartbeat.interval
MapReduce
概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-06IK5GUS-1653974219169)(https://s2.loli.net/2022/05/31/FfVlihapJg5RuHA.png)]
Mapduce适合大规模数据运算,但不擅长小数据实时计算和有一定执行顺序的任务的计算。这些缺陷可以被spark弥补
MapReduce数据类型:
Map结点负责将数据转换为hadoop能处理的形式,而Reduce负责处理数据
Mapreduce编程规范
Maven项目实践
导入mapreduce依赖
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
Mapper
我们观察mapper的源码
public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKeyValue()) {
this.map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
this.cleanup(context);
}
}
发现执行核心是这个run方法,setup用于初始化,clearup用于释放资源,真正的业务逻辑在map中
/**
* @author 李天航
* KEYIN, 输入Mapper的key是什么类型(这里是偏移量,也就是行数)
* VALUEIN, 输入Mapper的value是什么类型(这里是字符串,一般这两个参数都是这也)
* KEYOUT,输出是什么类型(输出的key是什么类型)
* VALUEOUT,输出要转换为什么类型(输出的value是什么类型,代表需要计算的数据)
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text,Text,LongWritable> {
Text tKey=new Text();
LongWritable tValue=new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, LongWritable>.Context context) throws IOException, InterruptedException {
//key 这里是偏移量,value这里是偏移量对应的文本,context是我们mapper结束后的输出的容器
String[] words = value.toString().split(" ");
for (String word : words) {
tKey.set(word);
context.write(tKey,tValue);
}
}
}
Reduer
Reducer代码也是类似:
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
while (context.nextKey()) {
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 {
cleanup(context);
}
}
/**
* @author 李天航
* KEYIN, 输入类型,和Mapper的输出的key的类型保持一致
* VALUEIN,输入的值类型,和Mapper的输出的value保持一致
* KEYOUT,输出的key类型
* VALUEOUT,输出的value类型
* Reducer的输入是Mapper的输出
*/
public class WordCountReducer extends Reducer<Text, LongWritable,Text,LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Reducer<Text, LongWritable, Text, LongWritable>.Context context) throws IOException, InterruptedException {
long sum=0;
for (LongWritable value : values) {
sum+=value.get();
}
context.write(key,new LongWritable(sum));
}
}
Driver
1.获取Job
2.获取当前执行类的class
3.获取Mapper的class和Reducer的class
4.设置Mapper的输出的kv类型和Reducer的输出kv类型
5.设置输入输出路径
6.提交Job
7.设置返回值
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration configuration=new Configuration();
Job job=Job.getInstance(configuration);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.setInputPaths(job,new Path("D://wordCountInput.txt"));
FileOutputFormat.setOutputPath(job,new Path("D://JavaWeb//Hadoop/WordCountOutput"));
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
这里使用main方法运行,实际上是本地模式并没有用到集群
MapReduce集群运行
TIPS:
使用本地的文件系统不能直接删除文件夹,但是hdfs可以直接删除文件夹:
public abstract boolean delete(Path f, boolean recursive) throws IOException;
如果删除的目标是个文件夹,如果第二个参数是true则会正常删除,如果是false会抛出异常
本地运行时,读取的文件和输出等待文件都在本地,这是因为本地没有hadoop环境,使用java -jar来运行的
而在虚拟机上运行时,使用的是hadoop jar 所以输入和输出文件都会在hdfs上
放入虚拟上运行前,需要将代码打成jar包,需要使用插件
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- 不需要依赖时不用打包-->
<!-- <plugin>-->
<!-- <artifactId>maven-assembly-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <descriptorRefs>-->
<!-- <descriptorRef>jar-with-dependencies</descriptorRef>-->
<!-- </descriptorRefs>-->
<!-- </configuration>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <id>make-assembly</id>-->
<!-- <phase>package</phase>-->
<!-- <goals>-->
<!-- <goal>single</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
</plugins>
</build>
如果是在有hadoop的地方运行,则不用加上被注释掉的部分(用于添加依赖)
如果是在没有hadoop的地方运行,则需要加上被注释的部分来安装MapReduce的jar包,使用java -jar来运行
可以写一个脚本来运行:
#!/bin/bash
hadoop jar MapReduce-1.0-SNAPSHOT.jar com.lth.mapreduce.wordcount2.WordCountDriver /WordCountDemo/word.txt /WordCountDemo/WordCountDemoOutput
hadoop jar 用hadoop的方式运行jar包
MapReduce-1.0-SNAPSHOT.jar 是jar包的名称
com.lth.mapreduce.wordcount2.WordCountDriver 是启动类的引用
/WordCountDemo/word.txt 是输入文件的位置
/WordCountDemo/WordCountDemoOutput 输出文件夹的位置
启动类代码:
package com.lth.mapreduce.wordcount2;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* @author 李天航
*/
public class WordCountDriver {
//本地测试时的输入输出位置,在使用集群运行时可以使用命令行来覆盖这两个参数
public static String INPUT_FILE="D:\\JavaWeb\\Hadoop\\MapReduceDemos\\src\\main\\java\\com\\lth\\mapreduce\\matrix.txt";
public static String OUTPUT_DIR="WordCountDemo/WordCountOutput";
public static FileSystem fileSystem;
public static final String HADOOP_URI="hdfs://hadoop102:8020";
public static URI hadoopURI = null;
public static final String HADOOP_USER="lth";
//使用java运行时删除文件夹,如果是hadoop方式运行则不需要
public static void deleteDir(File file){
if (!file.isFile()) {
File[] files = file.listFiles();
assert files != null;
if (files.length > 0) {
for (File listFile : files) {
deleteDir(listFile);
}
}
}
file.delete();
}
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException {
Configuration configuration=new Configuration();
Job job=Job.getInstance(configuration);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
if(args.length>1) {
INPUT_FILE = args[0];
OUTPUT_DIR = args[1];
}
hadoopURI=new URI(HADOOP_URI);
Path inputPath=new Path(INPUT_FILE);
Path outputPath=new Path(OUTPUT_DIR);
FileInputFormat.setInputPaths(job,inputPath);
fileSystem=FileSystem.get(hadoopURI,new Configuration(),HADOOP_USER);
//如果存在同名的文件夹则删除
if(fileSystem.exists(outputPath)) {
fileSystem.delete(outputPath,true);
}
FileOutputFormat.setOutputPath(job,outputPath);
//获取运行结果(0表示正常)
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
}
MapReduce序列化
序列化概述
Object转为对象集合:
List<FixedEvent> fixedEvents=new ObjectMapper().convertValue(alreadyIn, new TypeReference<List<FixedEvent>>(){});
其余的直接强制类型转换即可
序列化:实现不同服务器内存中对象的传输
java序列化比较严谨,加上了许多校验信息,使得序列化的过程比较笨重,可能不太适合大数据的对象传输
hadoop序列化因为考虑到是在一个系统内部,所以使用少量的校验信息即可完成序列化传输。因而占用空间更少,传输更快。
hadoop的基本数据类型都实现了WritableComparable接口
public interface WritableComparable<T> extends Writable, Comparable<T> {
}
Writable用于序列化和反序列化
Comparable用于重写比较函数,如果我们相应自定义bean也需要重写这个接口(因为shuffle阶段会进行排序)
序列化需求分析
Mapper输出k是我们的手机号,v就是我们要输出的bean
Reduce的输入就是Mapper的输出,Reduce的输出k是手机号,v是统计好数据的bean
想实现对象在不同机器上的传输(比如在一台机器上Mapper完,要将数据转移到另一台机器上Reduce)就必须要实现序列化
序列化写入的顺序需要和反序列化读出的顺序相同
所有的k和v都需要能序列化,而k还需要能排序(重写比较函数)
代码实现
FlowBean
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
void setSumFlow(){
this.sumFlow=this.upFlow+this.downFlow;
}
@Override
public String toString() {
return upFlow +
"\t" + downFlow +
"\t" + sumFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
upFlow=in.readLong();
downFlow=in.readLong();
sumFlow=in.readLong();
}
}
DataInput和DataOutput就相当于是封装好的输入输出流,往in里面写入数据时会自动用统一 的分隔符进行分割,读入数据时也会根据分隔符进行切分来生成对象
mapper
每一次任务都会创建一次FlowMapper对象来处理,每次任务处理多少数据,取决于分配任务的多少
public class FlowMapper extends Mapper<LongWritable, Text,Text,FlowBean> {
/**
* 这里的两个对象会经过序列化后写入输出流,不同机器是通过输入输出流(相当于一个文件)来交互的
* 而不是类似map那样通过引用来实现,因而这里只需要一个对象即可
*/
private final FlowBean flowBean=new FlowBean();
private final Text text=new Text();
FlowBean getFlowBean(String[] words){
flowBean.setUpFlow(Long.parseLong(words[words.length-2]));
flowBean.setDownFlow(Long.parseLong(words[words.length-1]));
flowBean.setSumFlow();
return flowBean;
}
Text getTextPhone(String[] words){
text.set(words[1]);
return text;
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
String[] words = value.toString().split("[\t]");
context.write(getTextPhone(words),getFlowBean(words));
}
}
5 15271374548 192.168.10.2 www.atguigu.com 2244 3355
每一行都是由\t分割的,中间可能有字段(比如ip)没有数据,所以读取流量时,我们从后往前数
context.write方法会直接将kv序列化后写入输出流,也就相当于持久化了,所以我们在这里只需要创建一个对象即可,reduce层是通过输出流拿到对象,而不是通过我们对象的引用拿到对象,所以这里只需要设置一个对象,可以节省内存。
reduce
reduce层,所有key都会在这里执行,并且会按照key进行排序后才会处理
public class FlowReduce extends Reducer<Text, FlowBean, Text, FlowBean> {
private final FlowBean outV=new FlowBean();
private final Text outK=new Text();
FlowBean getOutV(Iterable<FlowBean> values){
long upSum=0;
long downSum=0;
for (FlowBean value : values) {
upSum+=value.getUpFlow();
downSum+=value.getDownFlow();
}
outV.setUpFlow(upSum);
outV.setDownFlow(downSum);
outV.setSumFlow();
return outV;
}
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
context.write(key,getOutV(values));
}
}
driver
写法和之前的一样
package com.lth.mapreduce.writable;
import com.lth.mapreduce.wordcount2.WordCountMapper;
import com.lth.mapreduce.wordcount2.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import static com.lth.mapreduce.filesystem.HdfsFileSystem.deleteHdfsDirIfExist;
/**
* @author 李天航
*/
public class FlowDriver {
public static String INPUT_FILE="D:\\JavaWeb\\Hadoop\\MapReduceDemos\\src\\main\\java\\com\\lth\\mapreduce\\writable\\phones.txt";
public static String OUTPUT_DIR="FlowAddDemo/FlowAddOutput";
private static Path inputPath;
private static Path outputPath;
static void setPath(String[] args){
if(args.length>1) {
INPUT_FILE = args[0];
OUTPUT_DIR = args[1];
}
inputPath=new Path(INPUT_FILE);
outputPath=new Path(OUTPUT_DIR);
}
public static void main(String[] args) throws Exception {
Configuration configuration=new Configuration();
Job job=Job.getInstance(configuration);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
/*
设置输入输出路径
*/
setPath(args);
/*
如果输出路径上存在文件夹则将其删除
*/
deleteHdfsDirIfExist(outputPath);
FileInputFormat.setInputPaths(job,inputPath);
FileOutputFormat.setOutputPath(job,outputPath);
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
}
MapReduce框架原理
inputFormat 设置入数据的格式,例如切片
Mapper 数据预处理
Shuffle 切片,分区,压缩,排序
Reduce 处理数据
OutputFormat 输出的格式,数据的保存位置
切片
切片的大小决定决定了MapTask的并行度,切片大小不能太大也不能太小。太大的切片会让并行度降低,而太小的切片对效率的提升不显著而且浪费了许多资源(MapTask的启动也要时间)
数据块:HDFS存储数据的基本单位(一般为128M),将一个大文件分成若干小文件进行存储
数据切片:只是在逻辑上进行切片,并不会按照这个切片进行存储,每一个数据切片使用一个MapTask进行计算
虽然数据切片和数据块的大小没有直接联系,但是为了避免进行MapTask前进行大量的数据传输,一般默认将切片大小设置为数据块大小。这样需要计算的数据都在本地,从而避免跨结点的数据传输。基于这个思想,我们也可以设置数据切片的大小(例如128M),切片时不是对整个文件的进行切片,而是对每个数据块进行切片(也是为了防止跨结点数据传输,因为数据一般都很大)
假如两个数据结点DataNode,一个有300M数据,一个有100M数据,切片时会将300M切分为128M,128M,44M,将另一个结点的100M单独切分为一个数据切片。每个切片都会开启一个MapTask,在本机器上运行。
Job提交源码解析
我们提交job时使用的方法是waitForCompletion,参数表示要不要输出详细的日志信息
public boolean waitForCompletion(boolean verbose
) throws IOException, InterruptedException,
ClassNotFoundException {
if (state == JobState.DEFINE) {
//提交任务
submit();
}
if (verbose) {
//监视并打印日志信息
monitorAndPrintJob();
} else {
// get the completion poll interval from the client.
int completionPollIntervalMillis =
Job.getCompletionPollInterval(cluster.getConf());
while (!isComplete()) {
try {
Thread.sleep(completionPollIntervalMillis);
} catch (InterruptedException ie) {
}
}
}
return isSuccessful();
}
其中有submit方法用于提交任务
public void submit()
throws IOException, InterruptedException, ClassNotFoundException {
ensureState(JobState.DEFINE);
//兼容新旧mapreduce的API
setUseNewAPI();
//连接Yarn客户端和本地客户端
connect();
final JobSubmitter submitter =
getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException,
ClassNotFoundException {
//提交任务
return submitter.submitJobInternal(Job.this, cluster);
}
});
//将状态设置为RUNNING
state = JobState.RUNNING;
LOG.info("The url to track the job: " + getTrackingURL());
}
submitter.submitJobInternal(Job.this, cluster)s 用于提交任务的方法,里面完成的事情有这么几个:
1.校验输出路径,如果没有输出路径或者输出路径以及存在则报错
checkSpecs(job);
2.设置job的id值,每个提交的job都会有一个唯一的id值
JobID jobId = submitClient.getNewJobID();
3.拷贝并配置文件(提交jar包)
如果是集群模式则会提交jar包到创建的临时文件夹中,如果是本地则不用提交
copyAndConfigureFiles(job, submitJobDir);
->rUploader.uploadResources(job, jobSubmitDir);
->uploadResourcesInternal(job, submitJobDir);
->uploadJobJar(job, jobJar, submitJobDir, replication, statCache);
提交jar包到集群,如果是本地则不用提交
(上面还有获取Job的保存路径,创建文件夹,配置一些信息之类的)
4.切片
提交切片信息,这个步骤会把切片信息以文件的形式保存到之前创建的临时文件夹中
int maps = writeSplits(job, submitJobDir);
maps是切片的个数
5.设置任务个数
conf.setInt(MRJobConfig.NUM_MAPS, maps);
这个也证实了任务个数等于切片个数
6.提交mapreduce的运行参数
writeConf(conf, submitJobFile);
小记:提交Job时提交了哪些东西?
xml形式的配置信息,jar包,切片信息
7.向客户端提交任务,并获取状态
status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
至此,任务就完成了提交
然后将state由DEFINE设置为RUNNING
state = JobState.RUNNING;
然后任务提交就完成了(submit方法完成)
然后进行监视并输出日志信息
monitorAndPrintJob();
这一步完成后,本次任务就算完成了,上述创建的用于提交集群的临时文件,然后返回任务执行成功与否
return isSuccessful()
然后程序就结束了
如果是本地模式,将临时文件夹中的文件提交给集群模拟器
如果是集群模式,则会提交给YARN,由YARN来调度资源运行MR程序
FileInputFormat切片源码
默认使用的切片规则InputFormat是FileInputFormat
int maps = writeSplits(job, submitJobDir);
新旧API兼容:
private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
JobConf jConf = (JobConf)job.getConfiguration();
int maps;
if (jConf.getUseNewMapper()) {
maps = writeNewSplits(job, jobSubmitDir);
} else {
maps = writeOldSplits(jConf, jobSubmitDir);
}
return maps;
}
这里用的是hadoop3,所以用的新API:maps = writeNewSplits(job, jobSubmitDir)
maps = writeNewSplits(job, jobSubmitDir)
->List<InputSplit> splits = input.getSplits(job);
input 是InputFormat接口的一个对象,而当前的运行的接口的实现类是FileInputFormat
InputFormat由许多实现类,对应这不同的输入和切片方式
public static final String SPLIT_MINSIZE =
"mapreduce.input.fileinputformat.split.minsize";
通过调用InputFormat的getSplits方法来进行切片
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
//获取最小切片数,getFormatMinSplitSize()返回1,表示最小切片数最小为1
//getMinSplitSize(job)用于获取我们设置的最小切片数,默认是0,所以不设置的话,最小切片数就是1
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
boolean ignoreDirs = !getInputDirRecursive(job)
&& job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
for (FileStatus file: files) {
if (ignoreDirs && file.isDirectory()) {
continue;
}
//获取文件路径
Path path = file.getPath();
//获取文件长度
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
//如果文件是可切割的
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
//切片
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
//添加最后一片
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable
if (LOG.isDebugEnabled()) {
// Log only if the file is big enough to be splitted
if (length > Math.min(file.getBlockSize(), minSize)) {
LOG.debug("File is not splittable so no parallelization "
+ "is possible: " + file.getPath());
}
}
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}
1.获取最小切片数
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
需要用到配置项:
public static final String SPLIT_MINSIZE =
"mapreduce.input.fileinputformat.split.minsize";
配置的这个值和1取最大值,作为最小切片数
2.获取最大切片数
long maxSize = getMaxSplitSize(job);
需要用到配置项
public static final String SPLIT_MAXSIZE =
"mapreduce.input.fileinputformat.split.maxsize";
但是这一项默认没有配置
没有配置时,默认返回Long的最大值:
public static long getMaxSplitSize(JobContext context) {
return context.getConfiguration().getLong(SPLIT_MAXSIZE,
Long.MAX_VALUE);
}
3.遍历指定输入路径下的所有文件(如果路径是一个文件夹,则会遍历文件夹下的所有文件,如果路径是一个文件则只对这一个文件进行操作,不过处理方式都是一样的)
对每个单文件单独切片
List<FileStatus> files = listStatus(job);
for (FileStatus file: files){
}
3.1获取文件的一些基础信息
3.2判断文件能否切割
isSplitable(job, path)
如果文件不能切割(比如是压缩文件)
则将整个文件(无论多大),作为一个切片,开启一个MapTask处理
如果能切割
long blockSize = file.getBlockSize();
3.2.1获取块大小,我们在集群里面设置的是128M,不过现在是本地模式,默认资源不是很充足,所以本地模式的块大小为32M
long blockSize = file.getBlockSize();
3.2.2获取实际的块大小
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
Math.max(minSize, Math.min(maxSize, blockSize))
其实保证实际的块大小在最大之间和最小值之间,溢出的部分进行截断。默认范围是[1,LONG.MAX_VALUE],所以不设置最大最小值时,切片大小就是块大小(验证了前面所说的)
3.2.3切片
//获取文件大小
long bytesRemaining = length;
//如果文件剩余部分至少是切片大小的1.1倍(SPLIT_SLOP的值),才进行切片
//如果剩余大小不足切片大小的1.1倍则剩下的部分分为一片
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//添加切片
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
//更新剩余文件大小
bytesRemaining -= splitSize;
}
前面提到过,为了避免文件计算时在不同DataNode之间传输,切片时直接对DataNode的结点进行切片。
如果有多个文件,对每个文件单独切片,而不是合在一起切片
如果严格按照切片大小进行切面,假如最后只剩下了很少一点(比如0.1M),也得单独进行分为一片,开启maptask进行计算,对计算效率提升不大反而浪费了资源。所以如果零头比较少,小于块大小的0.1倍,则不单独进行切片,和最后一片合在一起。
最后加上不大于块大小1.1倍的切片
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
切片结束后,会将结果保存在一个List中,然后转换为一个数组进行排序,数组的长度就是切片的个数,排序后将切片结果写入用于提交的临时文件夹中
private <T extends InputSplit>
int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
Configuration conf = job.getConfiguration();
InputFormat<?, ?> input =
ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
//获得切片结果
List<InputSplit> splits = input.getSplits(job);
T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);
// sort the splits into order based on size, so that the biggest
// go first
Arrays.sort(array, new SplitComparator());
JobSplitWriter.createSplitFiles(jobSubmitDir, conf,
jobSubmitDir.getFileSystem(conf), array);
return array.length;
}
然后返回切片数量。
至此切片就完成了
FileInputFormat切面要点:
- 切片时,如果路径是文件夹,则对文件夹下的每个文件单独切片
- 块大小我们不能随意改变,但是切片大小我们可以通过调整minSize和maxSize来调整(超出这个范围进行截取,默认切片大小就是块大小),想要变小,则跳转maxSize,想要变大,跳转minSize
- 切片时,当剩余文件大小小于切片大小的1.1倍时不进行切片,大于1.1倍时才将1倍切分出来
- 这里的切片并没有涉及到数据的赋值和拷贝,只是将切片的元数据信息保存了下来(比如切片的起始位置,切片长度,文件名称等)
- 切片规划文件提交到YARN上后,YARN上的AppMrMaster就可以根据切片信息开启等同于切片数量的maptask
FileInputFormat的实现类
FileInputFormat是一个抽象类,里面帮我们编写了大量的通用逻辑(比如上面说的切片逻辑),但是我们还需要实现里面的两个方法
boolean isSplitable(FileSystem fs, Path file)
判断文件是否可切
RecordReader<LongWritable, Text> getRecordReader(InputSplit genericSplit, JobConf job,Reporter reporter)
根据读取数据的方式,返回数据读取器
TextInputFormat<LongWritable, Text>
默认的FileInputFormat
@InterfaceAudience.Public
@InterfaceStability.Stable
public class TextInputFormat extends FileInputFormat<LongWritable, Text>
implements JobConfigurable {
private CompressionCodecFactory compressionCodecs = null;
public void configure(JobConf conf) {
compressionCodecs = new CompressionCodecFactory(conf);
}
protected boolean isSplitable(FileSystem fs, Path file) {
final CompressionCodec codec = compressionCodecs.getCodec(file);
if (null == codec) {
return true;
}
return codec instanceof SplittableCompressionCodec;
}
public RecordReader<LongWritable, Text> getRecordReader(
InputSplit genericSplit, JobConf job,
Reporter reporter)
throws IOException {
reporter.setStatus(genericSplit.toString());
String delimiter = job.get("textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter) {
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
}
return new LineRecordReader(job, (FileSplit) genericSplit,
recordDelimiterBytes);
}
}
TextInputFormat中
可切割的条件是,文件不是压缩文件
数据读取方式是,按行读取文件
map阶段得到的k是每行第一个字符距离文件首字符的偏移量(从0开始),v是每行的内容
KeyValueTextInputFormat<Text, Text>
可以选择一个分割符,每行很具分隔符进行切割,分隔符前的是key,分隔符后的是value
CombineFileInputFormat<K, V>
将多个文件合成一个文件进行切片处理
先将每个文件在逻辑上切分成若干小文件,然后再将这些小文件(按照字典序排序后)组合为一个一个大于切片大小的切片
(1)虚拟存储的过程
对于一个文件,如果大小小于设置的切片大小splitsize,则分为一个块,如果大小大于一个splitsize而小于两倍的splitsize则将这个文件切分成两块,如果这个文件大于两倍的splitsize,则从中不断切分出一个splitsize大小文件块,直到剩余文件大小小于2倍的splitsize,然后如果小于一个splitsize单独分为一块,大于一个splitsize小于两个splitsize均分为两块。这样可以防止出现过小的虚拟存储块
(2)切片过程
将虚拟存储块从前往后进行合并,合并后的文件大小直到大于一个splitsize,将已经合并后的文件分为一个切片,然后继续合并
将输入模式切换为CombineFileInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class)
设置切片大小的最大值
CombineFileInputFormat.setMaxInputSplitSize(job,MAX_INPUT_SPLIT_SIZE);
使用这种输入方式,将小文件合并成大文件进行输出,避免过多的小文件开启过多的maptask,浪费资源(一个maptask需要1G内存和一个CPU)
MapTask输出结果kv值的存储
环形缓冲区,索引向右写,数据向左写,等到占用率到达80%时,从中间开始,反向写数据和索引,同时将前面写好的数据持久化到磁盘上,这样两个线程(写入线程和持久化线程)可以同步进行,如果写入线程即将覆盖还没有来得及写入磁盘的数据,则会将写入线程等待持久化线程完成后再进行写入。如果到达100%才反向写入的化,写入线程就必须等持久化线程结束后才能写入,效率不如80%高。
索引是描述数据的相关信息
MapReduce的执行流程
持久化之前会进行排序,排序算法是快速排序(排序时,交换的是索引而不是数据,数据量很大,交换也不可能),然后按照分区写入不同的maptask中
mapstask执行过程中会将结果保存到环形缓冲区(context.write),缓冲区到达80%会触发持久化,将数据进行快速排序后输出到文件中(一个mapstask会产生多个文件),这些数据在单个文件的内部是有序的,在整体上是无序的,因而使用归并排序最快。不同的分区排序后输出到不同的文件中,即为maptask的结果
等到maptask全部完成后(有些特殊情况不用等mapstask全部完成,提前开始运行)ReduceTask会拉取不同maptask的输出结果中相同分区的数据,合并到一起,排序后形成一个有序的数据集合,然后进行reduce,调用context.write方法输出结果,调用后会通过OutputFormat得到RecordedWriter,然后使用RecordedWriter按照特定的格数输入到文件中
Shuffle
shuffle过程
上述执行流程中,从Map阶段结束到Reduce阶段开始,这段排序混洗分区的阶段称为shuffle
分区是为了将不同类型的数据分开进行Reduce,最后输出到不同的文件中
maptask结束后会先进入默认100m的环形缓冲区
Combinner: 聚合操作,可以提前进行一些合并操作,让传输的数据量变小
maptask结束后,写入磁盘钱会对文件进行压缩,减少文件的大小。ReduceTask拉取MapTask的数据结果时,会拉取不同mapstask中不同分区的数据,并尝试用内存保存起来,如果内存不够则会溢写到磁盘中,然后进行归并排序(均按照字典序对索引进行排序),然后将相同key的value分为一组,传入Reduce中进行处理
shuffle 分区
源码分析
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;
}
}
分区是为了将不同类别的数据分开计算,输出到不同的文件中。
默认的分区是用哈希值和INT的最大值相与后(将Long的数映射到Int范围内)
我们可以通过
job.setNumReduceTasks(2);
设置Reduce任务的个数,每个ReduceTask都会生成一个输出文件(NumReduceTasks默认是1)
ReduceTask的个数也就是分区数,默认是按照hash来分区的,没有什么意义
写入数据时,会来到这一步:
public void write(K key, V value) throws IOException, InterruptedException {
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));
}
会先调用partitioner.getPartition(key, value, partitions)方法,得到当前数据应当分到哪个分区,然后再写入
如果没有设置NumReduceTasks,则NumReduceTasks默认是1,此时不使用HashPartitioner而是直接使用一个静态内部类返回0(可能这样避免的取模运算,效率更高)
partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
@Override
public int getPartition(K key, V value, int numPartitions) {
return partitions - 1;
}
};
自定义分区实例
编写分区类:
/**
* @author 李天航
* 按照电话前缀进行分区
*/
public class PhonePrefixPartition extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
String phone=text.toString();
String prefix=phone.substring(0,3);
switch (prefix) {
case "138":
return 0;
case "139":
return 1;
case "152":
return 2;
default:
return 3;
}
}
}
重写里面的getPartition方法来设置分区的逻辑,其中Partitioner<T,V>因为分区是map阶段的输出需要进行的步骤,所以两个泛型需要和map阶段的输出泛型保持一致
这里map的输出是<Text, FlowBean> 表示电话号码和我们的自定义输出对象
然后在驱动类设置Partion的类型和分区个数
public class FlowDriver {
public static String INPUT_FILE="D:\\JavaWeb\\Hadoop\\MapReduceDemos\\src\\main\\java\\com\\lth\\mapreduce\\writable\\phones.txt";
public static String OUTPUT_DIR="FlowAddDemo/FlowAddOutput";
private static Path inputPath;
private static Path outputPath;
private static final int MAX_INPUT_SPLIT_SIZE =20*1024*1024;
static void setPath(String[] args){
if(args.length>1) {
INPUT_FILE = args[0];
OUTPUT_DIR = args[1];
}
inputPath=new Path(INPUT_FILE);
outputPath=new Path(OUTPUT_DIR);
}
public static void main(String[] args) throws Exception {
Configuration configuration=new Configuration();
Job job=Job.getInstance(configuration);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
job.setInputFormatClass(CombineTextInputFormat.class);
job.setPartitionerClass(PhonePrefixPartition.class);
job.setNumReduceTasks(5);
CombineFileInputFormat.setMaxInputSplitSize(job,MAX_INPUT_SPLIT_SIZE);
/*
设置输入输出路径
*/
setPath(args);
/*
如果输出路径上存在文件夹则将其删除
*/
// deleteHdfsDirIfExist(outputPath);
FileInputFormat.setInputPaths(job,inputPath);
FileOutputFormat.setOutputPath(job,outputPath);
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
}
和之前相比多了两个核心代码:
//设置分区策略的类型
job.setPartitionerClass(PhonePrefixPartition.class);
//设置分区个数
job.setNumReduceTasks(5);
必须要设置分区个数,并且个数必须大于1
job.setNumReduceTasks(5);
否则他不会走我们自己的写的Partitioner,而是会走他自己自己定义的匿名内部类来返回0
getPartition返回的分区编号需要在[0,NumReduceTasks)这个范围中,可以多(但是多了会开启不必要reducetask,浪费资源)不能少,否则会报错
最后输出文件的编号和分区编号相同
shuffle 排序
源码分析
排序是hadoop MapReduce的默认行为(必须进行的行为),如果key不能排序则会报错
因为最后进行reduce的时候,key相同的value会进入一个方法中一起处理,所以排序是必要的行为
默认按照字典序排序,排序算法是快速排序
所以这里key和value可能并不一定要按照主键:值的关系来设置,可以将需要排序的部分放到key,不需要排序的部分放到value,key和value在reduce阶段共同还原出原有的信息
操作实例
我们利用直接统计好的流量作为输入进行排序
我们将FlowBean作为key,电话号码作为value,这样就可以根据FlowBean中的sumFlow字段进行排序
FlowBean实现WritableComparable,使其具有序列化和排序的能力
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
void setSumFlow(){
this.sumFlow=this.upFlow+this.downFlow;
}
@Override
public String toString() {
return upFlow +
"\t" + downFlow +
"\t" + sumFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
upFlow=in.readLong();
downFlow=in.readLong();
sumFlow=in.readLong();
}
//返回1表示o应当放在前面,返回-1表示o应当放在后面,返回0就随机
@Override
public int compareTo(FlowBean o) {
if(o.sumFlow>this.sumFlow){
return 1;
}else if(o.sumFlow<this.sumFlow){
return -1;
}
return 0;
}
}
Mapper层:
将拿到的FlowBean作为key,电话号码作为value,写入容器(缓存)
public class FlowMapper extends Mapper<LongWritable, Text,FlowBean, Text> {
final FlowBean flowBean=new FlowBean();
final Text phoneOutput=new Text();
Text getPhone(String[] splits){
phoneOutput.set(splits[0]);
return phoneOutput;
}
FlowBean getFlowBean(String[] splits){
flowBean.setUpFlow(Long.parseLong(splits[1]));
flowBean.setDownFlow(Long.parseLong(splits[2]));
flowBean.setSumFlow();
return flowBean;
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] splits = line.split("[\t ]");
context.write(getFlowBean(splits),getPhone(splits));
}
}
Reduce:
public class FlowReduce extends Reducer<FlowBean,Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value,key);
}
}
}
因为我们的目的只是排序,而排序工作在shuffle阶段就已经完成了,所以这里我们只需要控制一下输入格式,然后写入容器中即可,返回后因为是全排序,只有一个reduce过程,所以只会输出一个文件
二次排序:
说得很高大上,其实也也就排序的比较函数更复杂了一点,我们需要更复杂的排序时,都在这里编写优先级的逻辑即可,返回1表示o应当放在前面,返回-1表示o应当放在后面,返回0就随机(快速排序不稳定)
@Override
public int compareTo(FlowBean o) {
if(o.sumFlow>this.sumFlow){
return 1;
}else if(o.sumFlow<this.sumFlow){
return -1;
}else{
if(o.upFlow<this.upFlow){
return 1;
}else if(o.downFlow>this.downFlow){
return -1;
}else{
return 0;
}
}
}
分区加排序
将分区和排序组合在一起,对已经统计的好的流量数据进行统计
mapreduce会在map写出时,对不同分区进行内部排序,然后reduce前会拉取对应分区的文件数据,进行合并(归并排序),然后再进入reduce阶段。所以我们只要设置分区逻辑即可,框架会再shuffle时对每个分区的数据进行排序,并在reduce后写入不同的文件
编写分区类:
因为需要排序,map阶段的输出是<流量对象,电话>,根据电话分区时,应当获取后面的值
public class SortPartition extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
String phone=text.toString();
String prefix=phone.substring(0,3);
switch(prefix){
case "152":
return 0;
case "138":
return 1;
case "139":
return 2;
default:
return 3;
}
}
}
驱动类,driver
public class FlowDriver {
public static String INPUT_FILE="FlowAddDemo/FlowAddOutput";
public static String OUTPUT_DIR="SortedFlowAddDemo/FlowAddOutput";
private static Path inputPath;
private static Path outputPath;
private static final int MAX_INPUT_SPLIT_SIZE =20*1024*1024;
static void setPath(String[] args){
if(args.length>1) {
INPUT_FILE = args[0];
OUTPUT_DIR = args[1];
}
inputPath=new Path(INPUT_FILE);
outputPath=new Path(OUTPUT_DIR);
}
public static void main(String[] args) throws Exception {
Configuration configuration=new Configuration();
Job job=Job.getInstance(configuration);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
job.setInputFormatClass(CombineTextInputFormat.class);
job.setPartitionerClass(SortPartition.class);
job.setNumReduceTasks(5);
CombineFileInputFormat.setMaxInputSplitSize(job,MAX_INPUT_SPLIT_SIZE);
/*
设置输入输出路径
*/
setPath(args);
FileInputFormat.setInputPaths(job,inputPath);
FileOutputFormat.setOutputPath(job,outputPath);
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
Integer.parseInt("1",10);
Integer.valueOf("1",10);
}
}
和分区一致
map:
和排序一致
public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
final FlowBean flowBean=new FlowBean();
final Text phoneOutput=new Text();
Text getPhone(String[] splits){
phoneOutput.set(splits[0]);
return phoneOutput;
}
FlowBean getFlowBean(String[] splits){
flowBean.setUpFlow(Long.parseLong(splits[1]));
flowBean.setDownFlow(Long.parseLong(splits[2]));
flowBean.setSumFlow();
return flowBean;
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] splits = line.split("[\t ]");
context.write(getFlowBean(splits),getPhone(splits));
}
}
reduce:
直接写入容器
public class FlowReduce extends Reducer<FlowBean,Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value,key);
}
}
}
Combiner
如果将reduce的任务数量设置为0,则就没有reduce阶段,此时会把map阶段的结果输出到输出文件夹中,文件名中带有m,表示是map阶段的输出结果
job.setNumReduceTask(0)
如果没有reduce阶段,也就没有shuffle阶段,Combiner是shuffle的一部分
Combiner是预聚合,用法和reducer一样,继承reducer接口重写里面的reduce方法,然后再驱动类里面设置combiner
job.setCombinerClass(FlowCombiner.class);
即可再reduce前,对每个map结果的每个分区进行一次预处理
OutPutFormat
我们可以通过设置OutPutFormat控制输出的格式和结果
我们想要完成将包含atguigu的url输出到atguigu.log中,将其余的输出到other.log
虽然这里用分区也可以实现,我们这里使用自定义OutPutFormat来实现
因为每行只有一个url,所以map和reduce实际上什么都不用做,直接把结果写入容器即可
mapper
public class OutputMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
context.write(value,NullWritable.get());
}
}
reducer
public class OutputReducer extends Reducer<Text, NullWritable,Text,NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
values.forEach(o->{
try {
context.write(key,NullWritable.get());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
}
}
RecordeWriter
自定义RecordeWriter
mapreduce最后输出结果时,对调用这个对象的write方法来将数据写入文件,最后调用close方法来执行一些关闭连接的操作
public class SplitRecorderWriter extends RecordWriter<Text, NullWritable> {
FileSystem fs;
private static final Path ATGUIGU_OUPUT_PATH =new Path("OutputFormat/data/atguigu.log");
private static final Path OTHER_PATH =new Path("OutputFormat/data/other.log");
FSDataOutputStream atguiguOutputStream;
FSDataOutputStream otherOutputStream;
SplitRecorderWriter(TaskAttemptContext job){
try {
fs=FileSystem.get(job.getConfiguration());
atguiguOutputStream = fs.create(ATGUIGU_OUPUT_PATH);
otherOutputStream = fs.create(OTHER_PATH);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
String url=key.toString();
if(url.contains("atguigu")){
atguiguOutputStream.writeBytes(url+"\n");
}else{
otherOutputStream.writeBytes(url+"\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
atguiguOutputStream.close();
otherOutputStream.close();
}
}
FileOutputFormat
自定义FileOutputFormat
public class SplitOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
return new SplitRecorderWriter(job);
}
}
其实这里只需要返回需要调用的RecordWriter即可
Driver
最后编写驱动类,和常规写法一样,只是要单独设置SplitOutputFormat
public class OutputDriver {
private static final Path INPUT_PATH=new Path("src/main/java/com/lth/mapreduce/customoutput/Urls.txt");
private static final Path OUTPUT_SUCCESS_PATH=new Path("OutputFormat");
public static void main(String[] args) throws Exception {
Configuration configuration=new Configuration();
Job job = Job.getInstance(configuration);
job.setJarByClass(OutputDriver.class);
job.setMapperClass(OutputMapper.class);
job.setReducerClass(OutputReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(SplitOutputFormat.class);
FileInputFormat.setInputPaths(job,INPUT_PATH);
FileOutputFormat.setOutputPath(job, OUTPUT_SUCCESS_PATH);
boolean result=job.waitForCompletion(false);
System.exit(result?1:0);
}
}
这个对象是为了设置数据的输出格式,包括数据文件和校验码
job.setOutputFormatClass(SplitOutputFormat.class);
这个是为了输出成功标志_SUCCESS
FileOutputFormat.setOutputPath(job, OUTPUT_SUCCESS_PATH);
MapTask和ReduceTask的工作机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvloFz6g-1653974219193)(https://s2.loli.net/2022/05/31/n51uBSZ3eTxG8pj.png)]
MapTask源码解析
map阶段调用context.write方法后,会进入collect阶段,将数据写入环形缓冲区
1.keySerializer.serialize(key); 序列化key
2.valSerializer.serialize(value); 序列化value
3.写入数据和元数据元数据
当缓冲区的容量到达80%以上,或者maptask已经完成会将环形缓冲区的数据写入磁盘
我们回顾map的源码:
public void run(Context context) throws IOException, InterruptedException {
setup(context);
try {
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);
}
}
}
对每一行数据都会调用map方法,进入我们所写的逻辑后,进入write方法写入环形缓冲区,等它到达80%后会写入磁盘,写入前会进行排序,排序算法是快速排序。写入磁盘的过程在整个maptask结束时也会有一次,发生output.close(mapperContext)中(调用map的外层)
output.close(mapperContext)
->collector.flush() //将缓存刷到磁盘中
->sortAndSpill();//排序和溢写
->sorter.sort(MapOutputBuffer.this, mstart, mend, reporter)//排序,里面是优化后的快速排序算法
->mergeParts();//合并溢写文件
分区的时候,并不是一个分区有一个文件,一个maptask的所有分区的数据都在一个文件中,并且有索引标识分区在文件中的位置
排序和分区时,操作的都是索引文件,而不是操作原数据,排序和分区完成后,会生成一个索引文件,代表分区和排序后的结果
ReduceTask源码解析
public void run(JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, InterruptedException, ClassNotFoundException {
job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
if (isMapOrReduce()) {
copyPhase = getProgress().addPhase("copy");
sortPhase = getProgress().addPhase("sort");
reducePhase = getProgress().addPhase("reduce");
}
// start thread that will handle communication with parent
TaskReporter reporter = startReporter(umbilical);
boolean useNewApi = job.getUseNewReducer();
initialize(job, getJobID(), reporter, useNewApi);
// check if it is a cleanupJobTask
if (jobCleanup) {
runJobCleanupTask(umbilical, reporter);
return;
}
if (jobSetup) {
runJobSetupTask(umbilical, reporter);
return;
}
if (taskCleanup) {
runTaskCleanupTask(umbilical, reporter);
return;
}
// Initialize the codec
codec = initCodec();
RawKeyValueIterator rIter = null;
ShuffleConsumerPlugin shuffleConsumerPlugin = null;
Class combinerClass = conf.getCombinerClass();
CombineOutputCollector combineCollector =
(null != combinerClass) ?
new CombineOutputCollector(reduceCombineOutputCounter, reporter, conf) : null;
Class<? extends ShuffleConsumerPlugin> clazz =
job.getClass(MRConfig.SHUFFLE_CONSUMER_PLUGIN, Shuffle.class, ShuffleConsumerPlugin.class);
shuffleConsumerPlugin = ReflectionUtils.newInstance(clazz, job);
LOG.info("Using ShuffleConsumerPlugin: " + shuffleConsumerPlugin);
ShuffleConsumerPlugin.Context shuffleContext =
new ShuffleConsumerPlugin.Context(getTaskID(), job, FileSystem.getLocal(job), umbilical,
super.lDirAlloc, reporter, codec,
combinerClass, combineCollector,
spilledRecordsCounter, reduceCombineInputCounter,
shuffledMapsCounter,
reduceShuffleBytes, failedShuffleCounter,
mergedMapOutputsCounter,
taskStatus, copyPhase, sortPhase, this,
mapOutputFile, localMapFiles);
shuffleConsumerPlugin.init(shuffleContext);
rIter = shuffleConsumerPlugin.run();
// free up the data structures
mapOutputFilesOnDisk.clear();
sortPhase.complete(); // sort is complete
setPhase(TaskStatus.Phase.REDUCE);
statusUpdate(umbilical);
Class keyClass = job.getMapOutputKeyClass();
Class valueClass = job.getMapOutputValueClass();
RawComparator comparator = job.getOutputValueGroupingComparator();
if (useNewApi) {
runNewReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
} else {
runOldReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
}
shuffleConsumerPlugin.close();
done(umbilical, reporter);
}
1.reducetask的三个阶段:copy,sort,reduce
copyPhase = getProgress().addPhase("copy");
sortPhase = getProgress().addPhase("sort");
reducePhase = getProgress().addPhase("reduce");
2.初始化一些参数,里面会根据我们传入的OutputFormat的class文件创建OutputFormat的对象
initialize(job, getJobID(), reporter, useNewApi)
通过分析源码,我们也验证了reducetask的执行流程
Join 连接(Reduce)
连接时,我们只需要将连接字段作为key,将表字段的并集的Bean作为value。这样pid相同的tablebean就会进入同一个reduce阶段,就可以进行替换
Tips:
java的输入输出处理可以使用一些流的辅助类,比如scanner。这里再介绍两种辅助类:DataOutput,DataInput
DataOutput
作为流的辅助类,我们先创建一个输出流,然后作为参数传入DataOutputStream的构造函数
FileOutputStream fileOutputStream=new FileOutputStream("src/main/java/com/lth/mapreduce/tablejoin/UTF.txt");
DataOutput dataOutput=new DataOutputStream(fileOutputStream);
里面对各种数据类型都提供了写入的方法
DataInput
这个是输入流的辅助类,和DataOutput配套使用
FileInputStream fileInputStream=new FileInputStream("src/main/java/com/lth/mapreduce/tablejoin/UTF.txt");
DataInput dataInput=new DataInputStream(fileInputStream);
需要传入输入流作为参数
writeUTF
writeUTF是以UTF的格式写入字符串,前面几个字节标识字符串的长度,后面是字符串
得到的结果是:
@SneakyThrows
@Test
public void outputFile(){
FileOutputStream fileOutputStream=new FileOutputStream("src/main/java/com/lth/mapreduce/tablejoin/UTF.txt");
DataOutput dataOutput=new DataOutputStream(fileOutputStream);
dataOutput.writeUTF("hello world");
dataOutput.writeUTF("hello lth");
}
这样就可以根据长度进行反序列化,用readUTF拿到原来的数据
@SneakyThrows
@Test
public void inputFile(){
FileInputStream fileInputStream=new FileInputStream("src/main/java/com/lth/mapreduce/tablejoin/UTF.txt");
DataInput dataInput=new DataInputStream(fileInputStream);
System.out.println(dataInput.readUTF());
System.out.println(dataInput.readUTF());
}
其他方法也是:
@SneakyThrows
@Test
public void test2(){
dataOutput.writeLong(123);
dataOutput.writeLong(234);
System.out.println(dataInput.readLong());
System.out.println(dataInput.readLong());
}
writexxx和readxxx要配套使用,里面输入的是乱码
不想出现乱码可以用
@SneakyThrows
@Test
public void test3(){
dataOutput.writeBytes("123");
dataOutput.write("234".getBytes());
}
但是这样数据直接就没有分割符,也没有指定长度,无法序列化,我们只能指定长度来读取
TableBean
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TableBean implements Writable {
String id;
String pid;
int account;
String name;
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(pid);
out.writeInt(account);
out.writeUTF(name);
}
@Override
public void readFields(DataInput in) throws IOException {
id=in.readUTF();
pid=in.readUTF();
account=in.readInt();
name=in.readUTF();
}
}
TableBean是要作为value,所以需要实现write接口,来实现序列化和反序列化即可(为了便于在不同服务器上传输)
写入字符串使用writeUTF,写入其他类型使用writexxx,反过来读取字符串使用readUTF,读入其他类型使用readxxx
Mapper层
将两张表一起放到Mapper层处理,生成TableBean对象输出,不同的表的一行数据的分布不同,生成TableBean的方法不同,所以这里可以使用一个工厂模式来根据filename生成不同的table,来设置outK和outV
public class TableMapper extends Mapper<Writable, Text,Text,TableBean> {
String fileName;
Text outK=new Text();
TableBean outV=new TableBean();
final static String ORDER="order";
final static String PD="pd";
private final TableFactory tableFactory=new TableFactory();
interface Table{
/**
* 设置输出的value
* @param splits 分割后的字段
*/
void setOutValue(String[] splits);
/**
* 设置输出的key
* @param splits 分割后的字段
*/
void setOutKey(String[] splits);
}
class PdTable implements Table{
@Override
public void setOutKey(String[] splits) {
outK.set(splits[0]);
}
@Override
public void setOutValue(String[] splits) {
outV.setId("");
outV.setName(splits[1]);
outV.setTableName(fileName);
outV.setAccount(0);
outV.setPid(splits[0]);
}
}
class OrderTable implements Table{
@Override
public void setOutValue(String[] splits) {
outV.setId(splits[0]);
outV.setPid(splits[1]);
outV.setAccount(Integer.parseInt(splits[2]));
outV.setTableName(fileName);
outV.setName("");
}
@Override
public void setOutKey(String[] splits) {
outK.set(splits[1]);
}
}
class TableFactory{
Table getTableByName(String fileName){
switch (fileName){
case ORDER:
return new OrderTable();
case PD:
return new PdTable();
default:
return null;
}
}
}
@Override
protected void setup(Mapper<Writable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
FileSplit fileSplit = (FileSplit) context.getInputSplit();
fileName=fileSplit.getPath().getName();
}
@Override
protected void map(Writable key, Text value, Mapper<Writable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] split = line.split("[\t ]");
Table table = tableFactory.getTableByName(fileName);
table.setOutKey(split);
table.setOutValue(split);
context.write(outK,outV);
}
}
设置key value的时候记得将没有设置的字段清空,一方面防止序列化时出现空指针,一方面防止序列化时序列化入无用字段,浪费资源
获取切片信息可以将context转换为FileSplit即可
Reducer
public class TableReducer extends Reducer<Text,TableBean,TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Reducer<Text, TableBean, TableBean, NullWritable>.Context context) throws IOException, InterruptedException {
List<TableBean> pd = new ArrayList<>();
List<TableBean> orders = new ArrayList<>();
for (TableBean value : values) {
TableBean tmpTableBean =new TableBean();
try {
BeanUtils.copyProperties(tmpTableBean,value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
if(ORDER.equals(value.getTableName())){
orders.add(tmpTableBean);
}else{
pd.add(tmpTableBean);
}
}
String name=pd.get(0).getName();
for (TableBean order : orders) {
order.setName(name);
context.write(order,NullWritable.get());
}
}
}
这里将连接字段作为key,因而reduce时会将两张表中pid相同的数据都放在这里处理(可以根据pid进行分区,提高并行度),这里将order和pd两张表的数据分开,放在不同的集合中,pd表中对于同一个key只会有一条数据(实体完整性),所以将pd中的name赋值到order的name字段,然后写出即可。reduce结束后不会进行排序(只有shuffle才会进行排序),所以这里的key不要求实现Comparator接口。
注意:Iterable和java中的集合不同,hadoop改写了它的底层原理,因为考虑到将所有的数据都读进来会占用大量的内存,所以hadoop这里将所有的Iterable集合中的数据使用的都是同一块内存区域,每读进来一个对象都会覆盖前面的对象。所以直接add的话,最后添加到集合中的只有一个对象,所以我们要再创建一个对象后再添加到集合中。
我们调试也可以发现,遍历values的时候使用的bean的地址都是同一个
Driver层
public class TableDriver {
private static final String INPUT_FILE="src/main/java/com/lth/mapreduce/tablejoin/input";
private static final String OUTPUT_DIR="src/main/java/com/lth/mapreduce/tablejoin/output";
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Job job=Job.getInstance(new Configuration());
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
//pid
job.setMapOutputKeyClass(Text.class);
//数据
job.setMapOutputValueClass(TableBean.class);
//数据,reduce的key不需要排序
job.setOutputKeyClass(TableBean.class);
//无
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job,new Path(INPUT_FILE));
FileOutputFormat.setOutputPath(job,new Path(OUTPUT_DIR));
boolean result = job.waitForCompletion(false);
System.exit(result?1:0);
}
}
Join连接(Map)
再reduce阶段来join是通用做法,适合连接的两张表都比较大的情况。而如果其中一张表比较小,另一张表比较大,我们可以把比较小的那张表放到内存中缓存起来,这样在map阶段就可以完成连接操作,这样就不需要reduce了
驱动类 Driver
public class TableDriver {
private static final String INPUT_FILE="src/main/java/com/lth/mapreduce/mapjoin/input/order.txt";
private static final String CACHED_FILE="src/main/java/com/lth/mapreduce/mapjoin/input/pd.txt";
private static final String OUTPUT_DIR="src/main/java/com/lth/mapreduce/mapjoin/output";
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Job job=Job.getInstance(new Configuration());
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job,new Path(INPUT_FILE));
FileOutputFormat.setOutputPath(job,new Path(OUTPUT_DIR));
job.setCacheFiles(new URI[]{URI.create(CACHED_FILE)});
job.setNumReduceTasks(0);
boolean result = job.waitForCompletion(false);
System.exit(result?1:0);
}
}
添加缓存文件:
job.setCacheFiles(new URI[]{URI.create(CACHED_FILE)});
此次任务不需要reduce,所以可以将其设置为0
job.setNumReduceTasks(0);
将NumReduceTasks设置为0后,就没了reduce和shuffle阶段,因而key也不要求排序
TableBean
TableBean和上一节一样
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TableBean implements Writable {
private String id;
private String pid;
private int account;
private String name;
private String tableName;
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(pid);
out.writeInt(account);
out.writeUTF(name);
out.writeUTF(tableName);
}
@Override
public void readFields(DataInput in) throws IOException {
id=in.readUTF();
pid=in.readUTF();
account=in.readInt();
name=in.readUTF();
tableName=in.readUTF();
}
@Override
public String toString(){
return id+" "+name+" "+account;
}
}
Mapper
package com.lth.mapreduce.mapjoin;
/**
* @author 李天航
*/
public class TableMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
HashMap<String, String> pdHashMap=new HashMap<>();
Text outK=new Text();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
URI[] cacheFiles = context.getCacheFiles();
FileSystem fileSystem = FileSystem.get(context.getConfiguration());
FSDataInputStream pdFileInputStream = fileSystem.open(new Path(cacheFiles[0]));
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(pdFileInputStream, StandardCharsets.UTF_8));
String line;
while(StringUtils.isNotEmpty(line=bufferedReader.readLine())){
String[] fields = line.split("[\t ]");
pdHashMap.put(fields[0],fields[1]);
}
bufferedReader.close();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
String line=value.toString();
String[] fields = line.split("[\t ]");
outK.set(fields[0]+" "+pdHashMap.getOrDefault(fields[1],"")+" "+fields[2]);
context.write(outK,NullWritable.get());
}
}
在setup中和缓存文件建立输入流,并存入hashmap中
//获取缓存的文件
URI[] cacheFiles = context.getCacheFiles();
//拿到文件系统
FileSystem fileSystem = FileSystem.get(context.getConfiguration());
//获取输入流
FSDataInputStream pdFileInputStream = fileSystem.open(new Path(cacheFiles[0]));
//创建输入流辅助类BufferedReader
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(pdFileInputStream, StandardCharsets.UTF_8));
然后map的时候直接根据pid填入name即可
StringUtils是org.apache.commons.lang3.StringUtils
数据清洗(ETL)
将数据中不满足要求的数据过滤掉,留下我们想要的数据
mapreduce也可以作为数据清洗的工具
驱动类:Driver
public class EtlDriver {
private static final String INPUT_FILE="src/main/java/com/lth/mapreduce/etl/input";
private static final String OUTPUT_DIR="src/main/java/com/lth/mapreduce/etl/output";
@SneakyThrows
public static void main(String[] args) {
if(args.length==0){
args=new String[]{INPUT_FILE,OUTPUT_DIR};
}
Job job=Job.getInstance(new Configuration());
job.setJarByClass(EtlDriver.class);
job.setMapperClass(EtlMapper.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
}
因为不需要reduce所以将NumReduceTasks设置为0
job.setNumReduceTasks(0);
Mapper层
public class EtlMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
boolean parseLog(String line){
return line.length() >= 30;
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
String line=value.toString();
if(!parseLog(line)){
return;
}
context.write(value,NullWritable.get());
}
}
只需要通过mapper层即可达到过滤数据的目的,我们可以在parseLog中对数据进行解析,然后进行一些复杂的判断来到达数据清洗的目的。
总结
1)输入数据接口:InputFormat
(1)默认使用的实现类是:TextInputFormat
(2)TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为 key,行内容作为 value 返回。
(3)CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。
2)逻辑处理接口:Mapper 用户根据业务需求实现其中三个方法:map() setup() cleanup ()
3)Partitioner 分区
(1)有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个 分区号;key.hashCode()&Integer.MAXVALUE % numReduces
(2)如果业务上有特别的需求,可以自定义分区。
4)Comparable 排序 尚硅谷大数据技术之 Hadoop(MapReduce)
(1)当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接 口,重写其中的 compareTo()方法。
(2)部分排序:对最终输出的每一个文件进行内部排序。
(3)全排序:对所有数据进行排序,通常只有一个 Reduce。
(4)二次排序:排序的条件有两个。
5)Combiner 合并 Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的 业务处理结果。
6)逻辑处理接口:Reducer 用户根据业务需求实现其中三个方法:reduce() setup() cleanup ()
7)输出数据接口:OutputFormat
(1)默认实现类是 TextOutputFormat,功能逻辑是:将每一个 KV 对,向目标文本文件 输出一行。
(2)用户还可以自定义 OutputFormat。
压缩
概述
压缩算法的比较
压缩性能
压缩算法的选择
map阶段前
数据量小,不要切片,考虑Snappy(速度快)
数据量大,需要切片,考虑LZO(支持切片)
Shuffle阶段
无需切片,考虑体积小,速度快的即可,可以考虑Snappy
Reduce阶段后
如果要保存,考虑体积小的gzip
如果还要进行下一次运算,再按照map阶段考虑
压缩实际操作
可以在控制台输入hadoop checknative来查看集群支持的压缩格式,如果要修改压缩格式,需要配置core-site.xml,需要配置的参数是io.compression.codecs
Snnapy在集群运行时支持,在本地模式运行不支持
设置以上参数可以在代码设置configuration
开启map阶段输出的压缩格式
// 开启 map 端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
开启这个后,输出的仍然是文本格式,因为map阶段压缩的文件,在进入reduce前被解压
开启reduce输出后的压缩
// 设置 reduce 端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
这样输出后得到的就是压缩文件
开启压缩需要修改驱动类即可
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException {
Configuration configuration=new Configuration();
// 开启 map 端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job=Job.getInstance(configuration);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
job.setInputFormatClass(CombineTextInputFormat.class);
if(args.length>1) {
INPUT_FILE = args[0];
OUTPUT_DIR = args[1];
}
hadoopURI=new URI(HADOOP_URI);
Path inputPath=new Path(INPUT_FILE);
Path outputPath=new Path(OUTPUT_DIR);
FileInputFormat.setInputPaths(job,inputPath);
FileOutputFormat.setOutputPath(job,outputPath);
// 设置 reduce 端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
boolean result = job.waitForCompletion(false);
System.exit(result?0:1);
}
Yarn
Yarn的组成
Yarn工作机制
- 客户端上的YarnRunner向Yarn提交任务执行的请求 job.waitCompletion()
- Yarn中由ResourceManager来处理客户端的请求,收到请求后给客户端一个提交任务的路径
- 客户端收到路径后会计算切片大小并按照选择的切片逻辑进行切片,将切片信息保存在Job.split中(逻辑上切片),然后将Job.xml(配置信息),wc.jar(应用代码)提交到指定的路径当中
- 资源提交完毕,客户端向ResourceManager申请运行MapReduceMaster
- ResourceManager收到申请后会初始化一个Task,并向队列中提交任务(采用我们设置的调度算法,默认是FIFO),这个Task用于管理MapReduce程序的整个执行流程
- 当有NodeManager有空闲资源时,会从ResourceManager的队列中领走一个Task
- 当领走的是MapReduceMaster这个Task时,会创建一个容器,分配内存,磁盘,CPU,网络等资源给这个应用进程
- MapReduceMaster将Job的指定路径下客户端提交的的job.split,job.xml,wc.jar下载到本地
- MapReduceMaster根据切片信息,创建多个MapTask并将MapTask放入ResourceManager的等待队列中,等待NodeManager来领取任务执行
- NodeManager领取到MapTask时,创建容器准备执行MapTask
- 领取到MapTask的NodeManager向MapReduceMaster发送请求,标识自己领取到了任务,然后MapReduceMaster向这个NodeManager发送运行脚本运行MapTask,得到Map阶段的输出结果
- MapReduceMaster向ResourceManager发送运行ReduceTask的请求,将ReduceTask放到等待队列中,由NodeManager来领取任务
- 领取到任务的ReduceTask从MapTask的执行结果中拉取自己负责的分区的数据,进行合并作为ReduceTask的输入,然后拉取jar包运行ReduceTask,将结果写入磁盘
- 所有的任务都完成后,MapReduceMaster会注销自己,并释放占有的资源。每个MapTask和ReduceTask的结果都是从磁盘中读取,结果也是写入到磁盘上,运行完成后就会释放自己占有的容器,便于运行其他的Task。当这个Job完成后,MapReduceMaster会释放自己锁占有的容器。
三者的关系
MapReduce程序读取的数据来源于HDFS,最后得到的结果也可以来源于HDFS,而中间结果MapTask是写在运行MapTask的服务器上(减少一次网络IO)
Yarn调度器
公平调度器FIFO(很少使用)
容量调度器(Yarn默认使用)
一个队列中可以给多个任务分配资源,直至资源不够
领取任务时,也不一定是按照FIFO,还有以上这些因素:
优先选择占用资源最小队列的任务
同一个队列中,优先选择优先级高的作业,或者最先来的作业
同一个作业中,优先给优先级高的作业分配资源,优先级相同则按照本地性原则来尽可能执行运行时间短的任务
公平调度器
它会为队列中的每个job分配等量的资源,每个job会有最低资源限制和最大资源限制,利用分配的资源来完成各种maptask和reducetask,每个job利用自己分配的资源先开启部分的task,等这些task结束后释放资源,在开启其他的task,直到所有任务结束,将自己移出队列中
分配队列:
一开始是绝对均分,然后再将多的部分给少的
job也是
如果权重,则空闲资源加在一起,按照权重分配给缺资源的job
RDF调度器
同时考虑CPU和内存的调度器
Yarn 命令行操作
应用操作
yarn application -list
查看所有的任务
可以根据状态进行过滤
job的执行有以下状态:
ALL,NEW,NEW_SAVING,SUBMITTED,ACCEPTED,RUNNING,FINISHED,FAILED,KILLED
yarn application -list -appStates finished
杀死进程
yarn application -kill <id>
查看应用日志
yarn logs -applicationId <id>
查看应用中某个具体的任务的日志(容器)
yarn logs -applicationId <ApplicationId> -containerId <containerId>
正在尝试运行的任务
yarn applicationattempt -list <applicationId>
正在尝试运行的任务的状态
yarn applicationattempt -status <applicationId>
容器操作
查看所有正在运行的容器
yarn container -list
查看容器的状态
yarn container -status <containerId>
结点查看
查看所有结点的状态
yarn node -list -all
重新加载队列配置文件
yarn rmadmin -refreshQueues
查看队列
每个yarn都默认有一个default队列
yarn queue -status default
Yarn核心参数配置
如果想要高并发可以使用公平调度器,如果对并发量没有那么高,可以使用容量调度器
可以在 yarn-site.xml配置
<!-- 选择调度器,默认容量 -->
<property>
<description>The class to use as the resource scheduler.</description>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capaci
ty.CapacityScheduler</value>
</property>
<!-- ResourceManager 处理调度器请求的线程数量,默认 50;如果提交的任务数大于 50,可以
增加该值,但是不能超过 3 台 * 4 线程 = 12 线程(去除其他应用程序实际不能超过 8) -->
<property>
<description>Number of threads to handle scheduler
interface.</description>
<name>yarn.resourcemanager.scheduler.client.thread-count</name>
<value>8</value>
</property>
<!-- 是否让 yarn 自动检测硬件进行配置,默认是 false,如果该节点有很多其他应用程序,建议
手动配置。如果该节点没有其他应用程序,可以采用自动 -->
<property>
<description>Enable auto-detection of node capabilities such as
memory and CPU.
</description>
<name>yarn.nodemanager.resource.detect-hardware-capabilities</name>
<value>false</value>
</property>
<!-- 是否将虚拟核数当作 CPU 核数,默认是 false,采用物理 CPU 核数 -->
<property>
<description>Flag to determine if logical processors(such as
hyperthreads) should be counted as cores. Only applicable on Linux
when yarn.nodemanager.resource.cpu-vcores is set to -1 and
yarn.nodemanager.resource.detect-hardware-capabilities is true.
</description>
<name>yarn.nodemanager.resource.count-logical-processors-ascores</name>
<value>false</value>
</property>
<!-- 虚拟核数和物理核数乘数,默认是 1.0 -->
<property>
<description>Multiplier to determine how to convert phyiscal cores to
vcores. This value is used if yarn.nodemanager.resource.cpu-vcores
is set to -1(which implies auto-calculate vcores) and
yarn.nodemanager.resource.detect-hardware-capabilities is set to true.
The number of vcores will be calculated as number of CPUs * multiplier.
</description>
<name>yarn.nodemanager.resource.pcores-vcores-multiplier</name>
<value>1.0</value>
</property>
<!-- NodeManager 使用内存数,默认 8G,修改为 4G 内存 -->
<property>
<description>Amount of physical memory, in MB, that can be allocated
for containers. If set to -1 and
yarn.nodemanager.resource.detect-hardware-capabilities is true, it is
automatically calculated(in case of Windows and Linux).
In other cases, the default is 8192MB.
</description>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>4096</value>
</property>
<!-- nodemanager 的 CPU 核数,不按照硬件环境自动设定时默认是 8 个,修改为 4 个 -->
<property>
<description>Number of vcores that can be allocated
for containers. This is used by the RM scheduler when allocating
resources for containers. This is not used to limit the number of
CPUs used by YARN containers. If it is set to -1 and
yarn.nodemanager.resource.detect-hardware-capabilities is true, it is
automatically determined from the hardware in case of Windows and Linux.
In other cases, number of vcores is 8 by default.</description>
<name>yarn.nodemanager.resource.cpu-vcores</name>
<value>4</value>
</property>
<!-- 容器最小内存,默认 1G -->
<property>
<description>The minimum allocation for every container request at theRM in MBs. Memory requests lower than this will be set to the value of
this property. Additionally, a node manager that is configured to have
less memory than this value will be shut down by the resource manager.
</description>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>1024</value>
</property>
<!-- 容器最大内存,默认 8G,修改为 2G -->
<property>
<description>The maximum allocation for every container request at the
RM in MBs. Memory requests higher than this will throw an
InvalidResourceRequestException.
</description>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>2048</value>
</property>
<!-- 容器最小 CPU 核数,默认 1 个 -->
<property>
<description>The minimum allocation for every container request at the
RM in terms of virtual CPU cores. Requests lower than this will be set to
the value of this property. Additionally, a node manager that is configured
to have fewer virtual cores than this value will be shut down by the
resource manager.
</description>
<name>yarn.scheduler.minimum-allocation-vcores</name>
<value>1</value>
</property>
<!-- 容器最大 CPU 核数,默认 4 个,修改为 2 个 -->
<property>
<description>The maximum allocation for every container request at the
RM in terms of virtual CPU cores. Requests higher than this will throw an
InvalidResourceRequestException.</description>
<name>yarn.scheduler.maximum-allocation-vcores</name>
<value>2</value>
</property>
<!-- 虚拟内存检查,默认打开,修改为关闭 -->
<property>
<description>Whether virtual memory limits will be enforced for
containers.</description>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
<!-- 虚拟内存和物理内存设置比例,默认 2.1 -->
<property>
<description>Ratio between virtual memory to physical memory when
setting memory limits for containers. Container allocations are
expressed in terms of physical memory, and virtual memory usage is
allowed to exceed this allocation by this ratio.
</description>
<name>yarn.nodemanager.vmem-pmem-ratio</name>
<value>2.1</value>
</property>
如何创建队列
修改 capacity-scheduler.xml
添加新对了hive,修改原队列的资源
<!-- 指定多队列,增加 hive 队列 -->
<property>
<name>yarn.scheduler.capacity.root.queues</name>
<value>default,hive</value>
<description>
The queues at the this level (root is the root queue).
</description>
</property>
<!-- 降低 default 队列资源额定容量为 40%,默认 100% -->
<property>
<name>yarn.scheduler.capacity.root.default.capacity</name>
<value>40</value>
</property>
<!-- 降低 default 队列资源最大容量为 60%,默认 100% -->
<property>
<name>yarn.scheduler.capacity.root.default.maximum-capacity</name>
<value>60</value>
</property>
设置新队列的资源容量
<!-- 指定 hive 队列的资源额定容量 -->
<property>
<name>yarn.scheduler.capacity.root.hive.capacity</name>
<value>60</value>
</property>
<!-- 用户最多可以使用队列多少资源,1 表示 -->
<property>
<name>yarn.scheduler.capacity.root.hive.user-limit-factor</name>
<value>1</value>
</property>
<!-- 指定 hive 队列的资源最大容量 -->
<property>
<name>yarn.scheduler.capacity.root.hive.maximum-capacity</name>
<value>80</value>
</property>
<!-- 启动 hive 队列 -->
<property>
<name>yarn.scheduler.capacity.root.hive.state</name>
<value>RUNNING</value>
</property>
设置权限和运行时间
<!-- 哪些用户有权向队列提交作业 -->
<property>
<name>yarn.scheduler.capacity.root.hive.acl_submit_applications</name>
<value>*</value>
</property>
<!-- 哪些用户有权操作队列,管理员权限(查看/杀死) -->
<property>
<name>yarn.scheduler.capacity.root.hive.acl_administer_queue</name>
<value>*</value>
</property>
<!-- 哪些用户有权配置提交任务优先级 -->
<property>
<name>yarn.scheduler.capacity.root.hive.acl_application_max_priority</nam
e>
<value>*</value>
</property>
<!-- 任务的超时时间设置:yarn application -appId appId -updateLifetime Timeout
参考资料: https://blog.cloudera.com/enforcing-application-lifetime-slasyarn/ -->
<!-- 如果 application 指定了超时时间,则提交到该队列的 application 能够指定的最大超时
时间不能超过该值。
-->
<property>
<name>yarn.scheduler.capacity.root.hive.maximum-applicationlifetime</name>
<value>-1</value>
</property>
<!-- 如果 application 没指定超时时间,则用 default-application-lifetime 作为默认
值 -->
<property>
<name>yarn.scheduler.capacity.root.hive.default-applicationlifetime</name>
<value>-1</value>
</property>
配置完成后使用命令
yarn rmadmin -refreshQueues
这些配置参数也可以在代码中,用conf.set来设置
可以在8088看到队列的运行情况
向某个队列提交任务,可以在命令行设置参数,也可以在代码中指定
hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount -D mapreduce.job.queuename=hive /input /output
队列中设置任务的优先级
<property>
<name>yarn.cluster.max-application-priority</name>
<value>5</value>
</property>
表示设置了5个优先级
提交任务时可以设置优先级,默认是0
hadoop jar /opt/module/hadoop3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar pi -D
mapreduce.job.priority=5 5 2000000
-Dmapreduce.job.priority=5
这个值越大优先级最高
修改已经提交的任务的优先级
yarn application -appID application_1611133087930_0009 -updatePriority 5
公平调度器案例
创建两个队列,分别是 test 和 atguigu(以用户所属组命名)。期望实现以下效果:若用 户提交任务时指定队列,则任务提交到指定队列运行;若未指定队列,test 用户提交的任务 到 root.group.test 队列运行,atguigu 提交的任务到 root.group.atguigu 队列运行(注:group 为
用户所属组)。 公平调度器的配置涉及到两个文件,一个是 yarn-site.xml,另一个是公平调度器队列分 配文件 fair-scheduler.xml(文件名可自定义)。
(1)配置文件参考资料: https://hadoop.apache.org/docs/r3.1.3/hadoop-yarn/hadoop-yarn-site/FairScheduler.html
(2)任务队列放置规则参考资料: https://blog.cloudera.com/untangling-apache-hadoop-yarn-part-4-fair-scheduler-queuebasics/
配置yarn-site.xml
切换调度器,指定配置文件
<property>
<name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairS
cheduler</value>
<description>配置使用公平调度器</description>
</property>
<property>
<name>yarn.scheduler.fair.allocation.file</name>
<value>/opt/module/hadoop-3.1.3/etc/hadoop/fair-scheduler.xml</value>
<description>指明公平调度器队列分配配置文件</description>
</property>
<property>
<name>yarn.scheduler.fair.preemption</name>
<value>false</value>
<description>禁止队列间资源抢占,也可以不禁止</description>
</property>
配置 fair-scheduler.xml
配置资源和策略
<?xml version="1.0"?>
<allocations>
<!-- 单个队列中 Application Master 占用资源的最大比例,取值 0-1 ,企业一般配置 0.1
-->
<queueMaxAMShareDefault>0.5</queueMaxAMShareDefault>
<!-- 单个队列最大资源的默认值 test atguigu default -->
<queueMaxResourcesDefault>4096mb,4vcores</queueMaxResourcesDefault>
<!-- 增加一个队列 test -->
<queue name="test">
<!-- 队列最小资源 -->
<minResources>2048mb,2vcores</minResources>
<!-- 队列最大资源 -->
<maxResources>4096mb,4vcores</maxResources>
<!-- 队列中最多同时运行的应用数,默认 50,根据线程数配置 -->
<maxRunningApps>4</maxRunningApps>
<!-- 队列中 Application Master 占用资源的最大比例 -->
<maxAMShare>0.5</maxAMShare>
<!-- 该队列资源权重,默认值为 1.0 -->
<weight>1.0</weight>
<!-- 队列内部的资源分配策略 -->
<schedulingPolicy>fair</schedulingPolicy>
</queue>
<!-- 增加一个队列 atguigu -->
<queue name="atguigu" type="parent">
<!-- 队列最小资源 -->
<minResources>2048mb,2vcores</minResources>
<!-- 队列最大资源 -->
<maxResources>4096mb,4vcores</maxResources>
<!-- 队列中最多同时运行的应用数,默认 50,根据线程数配置 -->
<maxRunningApps>4</maxRunningApps>
<!-- 队列中 Application Master 占用资源的最大比例 -->
<maxAMShare>0.5</maxAMShare>
<!-- 该队列资源权重,默认值为 1.0 -->
<weight>1.0</weight>
<!-- 队列内部的资源分配策略 -->
<schedulingPolicy>fair</schedulingPolicy>
</queue>
<!-- 任务队列分配策略,可配置多层规则,从第一个规则开始匹配,直到匹配成功 -->
<queuePlacementPolicy>
<!-- 提交任务时指定队列,如未指定提交队列,则继续匹配下一个规则; false 表示:如果指
定队列不存在,不允许自动创建-->
<rule name="specified" create="false"/>
<!-- 提交到 root.group.username 队列,若 root.group 不存在,不允许自动创建;若
root.group.user 不存在,允许自动创建
如果没有显示得设置要提交到哪个队列,则提交到和用户名相同的那个队列,如果用户名没有匹配的则提交到和组名匹配的那个对象,如果都没有reject拒绝,或者default提交到default队列
-->
<rule name="nestedUserQueue" create="true">
<rule name="primaryGroup" create="false"/>
</rule>
<!-- 最后一个规则必须为 reject 或者 default。Reject 表示拒绝创建提交失败,
default 表示把任务提交到 default 队列 -->
<rule name="reject" />
</queuePlacementPolicy>
</allocations>
重启yarn:
[atguigu@hadoop102 hadoop]$ xsync yarn-site.xml
[atguigu@hadoop102 hadoop]$ xsync fair-scheduler.xml
[atguigu@hadoop103 hadoop-3.1.3]$ sbin/stop-yarn.sh
[atguigu@hadoop103 hadoop-3.1.3]$ sbin/start-yarn.sh
集群的调度器就切换完成了
输入参数处理
我们在运行jar包时,可能会传入一些参数-D,来设置参数,比如队列的类型等等(前面提到的在xml中配置的参数也可以在这里设置),但是我们在集群运行的时候还需要动态设置输入输出,所以需要用到args,而我们输入的-D的参数也会被输出到args中,导致input路径和output路径在args数组中的位置不确定,导致程序出错。我们可以让我们所写的程序继承Tool接口,我们将参数传入Tool接口后,Tool会将带有-D的命令行参数过滤掉,并作为配置参数传入conf中,这样我们的mapreduce程序拿到的命令行参数就是没有带-D的命令行参数,并且下标从0开始,这样输入输出参数就是准确的了
WordCount业务类,相当于将MapReduce程度进行了模块化处理
conf由外部传进来,由ToolRunner帮我们进行过滤和参数的设置后传入set方法,这样我们就能拿到conf,然后使用conf创建job,设置一些属性然后执行即可。mapper类和reducer类可以写在这个类里面,也可以写在这个类外面
public class WordCount implements Tool {
Configuration conf;
@Override
public int run(String[] args) throws Exception {
if(args.length<2){
args=new String[]{"src/main/java/com/mapreduce/input","src/main/java/com/mapreduce/output"};
}
Job job=Job.getInstance(conf);
job.setJarByClass(TotalDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
return job.waitForCompletion(true)?0:1;
}
@Override
public void setConf(Configuration conf) {
this.conf=conf;
}
@Override
public Configuration getConf() {
return conf;
}
public static class WordCountMapper extends Mapper<LongWritable, Text,Text,LongWritable>{
Text outK=new Text();
LongWritable outV=new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, LongWritable>.Context context) throws IOException, InterruptedException {
String[] words = value.toString().split("[\t ,.]");
for (String word : words) {
if("".equals(word)){
continue;
}
outK.set(word);
context.write(outK,outV);
}
}
}
public static class WordCountReducer extends Reducer<Text,LongWritable,Text,LongWritable>{
LongWritable outV=new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Reducer<Text, LongWritable, Text, LongWritable>.Context context) throws IOException, InterruptedException {
int sum=0;
for (LongWritable value : values) {
sum+=value.get();
}
outV.set(sum);
context.write(key,outV);
}
}
}
这样输入可以正常执行
hadoop jar MapReduceDemos.jar com.mapreduce.TotalDriver WordCount -Dyarn.scheduler.capacity.root.default.capacity=40 /WordCountDemo/word.txt /WordCountDemo/WordCountDemoOutput
输入下面这个(没有设置参数)也能正常执行
hadoop jar MapReduceDemos.jar com.mapreduce.TotalDriver WordCount /WordCountDemo/word.txt /WordCountDemo/WordCountDemoOutput
启动类:
我们可以根据第一个参数来决定程序使用哪个程序,然后调用ToolRunner.run运行
Arrays.copyOfRange(args, 1, args.length)将第一个参数剔除掉,这样过滤参数后,input和output就分布是args[0]和args[1]
public class TotalDriver {
public static void main(String[] args) throws Exception {
Configuration conf=new Configuration();
Tool tool;
if(args.length<1){
args=new String[]{"WordCount"};
}
switch (args[0]){
case "WordCount":
tool=new WordCount();
break;
default:
throw new RuntimeException("没有这个任务");
}
int result = ToolRunner.run(conf, tool, Arrays.copyOfRange(args, 1, args.length));
System.exit(result);
}
}
Hadoop调优手册
HDFS参数设置
每记录一个block的信息,需要占用NameNode大概150B的存储空间,如果NameNode内存大小有128M,则集群最多可以存放9.1亿个文件块。除非有大量零散的小文件,否则一般情况下是够用的。(一个文件块BLOCK只能存放一个文件的数据,方便在后面添加)
NameNode和DataNode大小
NameNode最小值设置为1G,每增加1e6个文件块增加1G内存
DataNode在保存的副本数少于4e6个时,设置为4G,大于4e6时,每增加1e6添加1G
设置方式:
编辑hadoop-env.sh
这里设置的是最大值
export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS -Xmx1024m"
export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS -Xmx1024m"
配置文件前面本身可能还有一些配置,我们用空格隔开,在后面加上我们需要加上的参数即可
然后启动集群
用jmap -heap 来查集各个结点占有的内存
NameNode并发线程数
NameNode需要并发处理各个客户端的请求,以及处理各个DataNode的心跳来获知各个DataNode的状态
可以配置hdfs-site.xml
The number of Namenode RPC server threads that listen to requests from clients. If dfs.namenode.servicerpc-address is not configured then Namenode RPC server threads listen to requests from all nodes. NameNode 有一个工作线程池,用来处理不同 DataNode 的并发心跳以及客户端并发 的元数据操作。 对于大集群或者有大量客户端的集群来说,通常需要增大该参数。默认值是 10。
<property>
<name>dfs.namenode.handler.count</name>
<value>21</value>
</property>
当前集群是3台,e大小大概是2.7, 20ln(3)大概是21
表示并发处理的线程数
开启回收站
回收站和我们普通操作系统的回收站功能一样,删除文件后会先放入回收站中,防止误删除
回收站存活时间,回收站中超过这个时间的数据会被清除,单位是分钟
fs.trash.interval
回收站检查时间,每到这个时间会检查一次回收站,清空里面的超时时间,单位是分钟
fs.trash.checkpoint.interval
- fs.trash.interval=0 表示禁用回收站功能(默认是禁用)
- fs.trash.checkpoint.interval=0 表示检查时间间隔和回收站文件存活时间一致
- 要求fs.trash.checkpoint.interval<=fs.trash.interval,避免需要清理的文件存活太长时间
上面两个参数都在core-site.xml上配置,单位是分钟
<property>
<name>fs.trash.interval</name>
<value>1</value>
</property>
在9870端口的那个网页删除的文件不会进入回收站
用命令行删除的文件会进入回收站(user/xxx/.Trash/……),想要恢复文件的话需要我们手动复制到其他地方
HDFS集群压测
在搭建完集群后,要考虑整个集群的性能问题,比如10T数据上传需要多久,读取10T数据需要多久
设置上传和下载的带宽,单位是Mbps和MB不一样,1B=8b
我们可以通过
python -m SimpleHTTPServer
来启动一个服务,将当前所在的文件夹暴露出去,使得外部可以通过ip+端口+文件路径,来访问(下载)本地资源
资源的根目录就是这个命令的启动目录