尚硅谷hadoop+ha入门笔记

文章目录

1 大数据特点 (4V)

  • Volume(大量)
  • Velocity(高速)
  • Variety(数据的多样)
  • Value( 低价值密度,价值密度的高低与数据总量的大小成反比)

2 大数据部门

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3xjcMLZ-1658641856512)(…/…/images/image-20220421143300194.png)]

3 Hadoop概念

  • Hadoop是apache开发的分布式系统基础架构,主要解决海量数据的存储和分析计算,广义的Hadoop通常指Haoop生态圈
  • Google三篇论文是Hadoop的思想,GFS->HDFS, Map-Reduce->MR, BigTable->HBase

3.1 Hadoop三大发行版本

  • Hadoop三大发行版本:Apache、Cloudera、Hortonworks。
  • Apache版本最原始(最基础)的版本,对于入门学习最好。2006

官网地址:http://hadoop.apache.org
下载地址:https://hadoop.apache.org/releases.html

  • Cloudera内部集成了很多大数据框架,对应产品CDH。2008

官网地址:https://www.cloudera.com/downloads/cdh
下载地址:https://docs.cloudera.com/documentation/enterprise/6/release-notes/topics/rg_cdh_6_download.html

  • Hortonworks文档较好,对应产品HDP。2011

官网地址:https://hortonworks.com/products/data-center/hdp/
下载地址:https://hortonworks.com/downloads/#data-platform

  • Hortonworks现在已经被Cloudera公司收购,推出新的品牌CDP。

3.2 Hadoop优势

  • 高可靠性:Hadoop底层维护多个数据副本,所以即使Hadoop某个计算元素或存储出现故障,也不会导致数据的丢失。
  • 高扩展性:在集群间分配任务数据,可方便的扩展数以千计的节点。
  • 高效性:在MapReduce的思想下,Hadoop是并行工作的,一个任务可以分为多个子任务交由集群同时进行,以加快任务处理速度。
  • 高容错性:能够自动将失败的任务重新分配。

3.3 Hadoop组成

  • Hadoop 1.x:Common(辅助工具)、HDFS(数据存储)、MapReduce(计算+资源调度)
  • Hadoop 2.x:Common(辅助工具)、HDFS(数据存储)、MapReduce(计算)、Yarn(资源调度)
  • Haddop3.x组成同2.x

3.3.1 HDFS

  • Hadoop Distributed File System,简称HDFS,是一个分布式文件系统,用于处理海量数据的存储
  • NameNode(nn):存储文件的元数据,如文件名,文件目录结构,文件属性(生成时间、副本数、文件权限),以及每个文件的块列表和块所在的DataNode等。(记录数据的信息跟数据存储在什么位置)
  • DataNode(dn):在本地文件系统存储文件块数据,以及块数据的校验和。(用于存储数据)
  • Secondary NameNode(2nn):每隔一段时间对NameNode元数据备份。

3.3.2 YARN

  • Yet Another Resource Negotiator简称YARN,另一种资源协调者,是Hadoop的资源管理器
  • ResourceMange(RM):管理整个集群资源(内存、CPU等)
  • NodeManager(NM): 管理单个节点服务器资源
  • ApplicationMaster(AM):管理单个任务
  • Container:容器,相当一台独立的服务器,里面封装了任务运行所需要的资源,如内存、CPU、磁盘、网络等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mSCGD2k1-1658641856513)(…/…/images/image-20220422140409575.png)]

3.3.3 MapReduce

  • MapReduce将计算过程分为两个阶段:Map和Reduce,Map阶段并行处理输入数据,Reduce阶段对Map结果进行汇总,即对多个结点分配任务,然后再对任务完成结果进行汇总。

3.3.4 HDFS、YARN、MapReduce三者的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bz7jzlgM-1658641856513)(…/…/images/image-20220422142301180.png)]

3.4 大数据技术生态体系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIDBXL6Z-1658641856514)(…/…/images/image-20220423114523451.png)]

  • Sqoop:Sqoop是一款开源的工具,主要用于在Hadoop、Hive与传统的数据库(MySQL)间进行数据的传递,可以将一个关系型数据库(例如 :MySQL,Oracle 等)中的数据导进到Hadoop的HDFS中,也可以将HDFS的数据导进到关系型数据库中。
  • Flume:Flume是一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统,Flume支持在日志系统中定制各类数据发送方,用于收集数据;
  • Kafka:Kafka是一种高吞吐量的分布式发布订阅消息系统; 更多Java –大数据 –前端 –python人工智能资料下载,可百度访问:尚硅谷官网
  • Spark:Spark是当前最流行的开源大数据内存计算框架。可以基于Hadoop上存储的大数
    据进行计算。
  • Flink:Flink是当前最流行的开源大数据内存计算框架。用于实时计算的场景较多。
  • Oozie:Oozie是一个管理Hadoop作业(job)的工作流程调度管理系统。
  • Hbase:HBase是一个分布式的、面向列的开源数据库。HBase不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库。
  • Hive:Hive是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张数据库表,并提供简单的SQL查询功能,可以将SQL语句转换为MapReduce任务进行运行。其优点是学习成本低,可以通过类SQL语句快速实现简单的MapReduce统计,不必开
    发专门的MapReduce应用,十分适合数据仓库的统计分析。
  • ZooKeeper:它是一个针对大型分布式系统的可靠协调系统,提供的功能包括:配置维护、
    名字服务、分布式同步、组服务等。

3.5 Hadoop目录结构

  • bin目录:存放对Hadoop相关服务(hdfs,yarn,mapred)进行操作的脚本
  • etc目录:Hadoop的配置文件目录,存放Hadoop的配置文件
  • lib目录:存放Hadoop的本地库(对数据进行压缩解压缩功能)
  • sbin目录:存放启动或停止Hadoop相关服务的脚本
  • share目录:存放Hadoop的依赖jar包、文档、和官方案例

4 Hadoop运行模式

  • Hadoop运行模式包括本地模式、伪分布式模式和完全分布式模式。
  • 本地模式:单机运行,用于演示官方案例
  • 伪分布式模式:单机运行,具备Hadoop集群的功能,一台服务器模拟一个分布式环境。
  • 完全分布式模式:多台服务器组成分布式环境,企业常用。

4.1 完全分布式模式(重点)

4.1.1 集群配置

  • NameNode 和 SecondaryNameNode 不要安装在同一台服务器
  • ResourceManager 也很消耗内存,不要和 NameNode、SecondaryNameNode 配置在 同一台机器上。
  • 制定集群部署规划,根据集群部署规划去配置各种配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiSWLM7y-1658641856514)(…/…/images/image-20220517153115616.png)]

  • Hadoop 配置文件分两类:默认配置文件和自定义配置文件,只有用户想修改某一默认 配置值时,才需要修改自定义配置文件,更改相应属性值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlbOwPkM-1658641856514)(…/…/images/image-20220517153645646.png)]

  • 配置核心配置文件core-site.xml,每个节点都需要配置

    <configuration>
     <!-- 指定 NameNode 的地址 -->
     		<property>
     			<name>fs.defaultFS</name>
     			<value>hdfs://hadoop102:8020</value> # 相当于定义了集群内部的通讯地址
     		</property>
     <!-- 指定 hadoop 数据的存储目录 -->
     		<property>
     			<name>hadoop.tmp.dir</name>
     			<value>/opt/module/hadoop-3.1.3/data</value> 
     		</property>
     <!-- 配置 HDFS 网页登录使用的静态用户为 F -->
     		<property>
                 <name>hadoop.http.staticuser.user</name>
                 <value>F</value>
     		</property>
    </configuration>
    
    
  • 配置HDFS配置文件hdfs–site.xml,每个节点都需要配置

    <configuration>
    <!-- nn web 端访问地址-->
    	<property>
    		 <name>dfs.namenode.http-address</name>
    		 <value>hadoop102:9870</value>
    	 </property>
    <!-- 2nn web 端访问地址-->
     	<property>
        	<name>dfs.namenode.secondary.http-address</name>
     		<value>hadoop104:9868</value>
    	 </property>
    </configuration>
    
  • 配置YRAN配置文件yarn-site.xml,每个节点都需要配置

    <configuration>
     <!-- 指定 MR 走 shuffle -->
    	 <property>
             <name>yarn.nodemanager.aux-services</name>
             <value>mapreduce_shuffle</value>
     	</property>
     <!-- 指定 ResourceManager 的地址-->
    	 <property>
             <name>yarn.resourcemanager.hostname</name>
             <value>hadoop103</value>
     	</property>
     <!-- 环境变量的继承 -->
    	 <property>
     		<name>yarn.nodemanager.env-w+hitelist</name>
    		<value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CO
    NF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAP
    RED_HOME</value>
    	 </property>
        <property>
            <name>yarn.nodemanager.vmem-check-enabled</name>
            <value>false</value>
    	</property>
    </configuration>
    
  • 配置MapReduce配置文件mapred-site.xml,每个节点都需要配置

    <configuration>
    <!-- 指定 MapReduce 程序运行在 Yarn 上, 即让Yarn来负责资源调度 -->
    	 <property>
             <name>mapreduce.framework.name</name>
             <value>yarn</value>
    	 </property>
          <property>
              <name>yarn.app.mapreduce.am.env</name>
              <value>HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3</value>
        </property>
    
        <property>
            <name>mapreduce.map.env</name>
            <value>HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3</value>
        </property>
        <property>
            <name>mapreduce.reduce.env</name>
            <value>HADOOP_MAPRED_HOME=/opt/module/hadoop-3.1.3</value>
        </property>
    </configuration>
    
  • 配置workers,有多少个节点就配置多少个主机名称,workers文件在$HADOOP_HOME/etc/hadoop下,注意每个主机名称后不能加空格,每个节点都需要配置

  • 配置历史服务器,在mapred-site.xml中配置以下代码,每个节点都需要配置,配置完需要重启yarn

    <!-- 历史服务器端地址-->
    <property>
    	<name>mapreduce.jobhistory.address</name>
        <value>hadoop102:10020</value> <!-- 内部通讯端口 -->
    </property>
    <property>
    	<name>mapreduce.jobhistory.webapp.address</name>
        <value>hadoop102:19888</value> <!-- web页面端口-->
    </property>
    

    在配置历史服务器的节点中启动历史服务器,这样在YARN的资源管理器(hadoop103:8088)可以看运行任务的历史详情。

    mapred --daemon start historyserver	
    
  • 配置日志的聚集功能,在yarn-site.xml中增加配置,每个节点都需要配置。

    <!-- 开启日志聚集功能 -->
    <property>
         <name>yarn.log-aggregation-enable</name>
         <value>true</value>
    </property>
    <!-- 设置日志聚集服务器地址 -->
    <property>
         <name>yarn.log.server.url</name>
         <value>http://hadoop102:19888/jobhistory/logs</value> <!-- 配置在历史服务器下-->
    </property>
    <!-- 设置日志保留时间为 7 天 -->
    <property>
         <name>yarn.log-aggregation.retain-seconds</name>
         <value>604800</value>
    </property>
    

    配置完后关闭历史服务器,重启yarn跟历史服务器,然后在YARN的资源管理器(hadoop103:8088)运行任务的历史详情中,可以点击logs查看日志信息

    mapred --daemon stop historyserver
    stop-yarn.sh
    start-yarn.sh
    mapred --daemon start historyserver
    

4.1.2 启动集群

  • 如果集群是第一次启动,需要在 hadoop102 节点格式化 NameNode(注意:格式 化 NameNode,会产生新的集群 id,导致 NameNode 和 DataNode 的集群 id 不一致,集群找 不到已往数据。如果集群在运行过程中报错,需要重新格式化 NameNode 的话,一定要先停 止 namenode 和 datanode 进程,并且要删除所有机器的 data 和 logs 目录,然后再进行格式 化。)

    hdfs namenode -format # namenode初始化
    
  • 在$HADOOP_HOME/sbin/下运行start-dfs.sh启动HDFS

  • web端查看HDFS的NameNode:访问hadoop102:9870,导航栏的Utiliities-Browse the file system提供了一个界面进行管理文件

  • 在$HADOOP_HOME/sbin/下运行start-yarn.sh启动YARN

  • web端查看YARN的ResourceManager:访问hadoop103:8088,可以查看任务的运行情况

4.1.3 集群基本测试

  • 创建文件夹测试

    hadoop fs -mkdir 路径
    
  • 上传文件测试

    hadoop fs -put 源文件路径 目标路径
    
  • 文件存在位置为$HADOOP_HOME/data/dfs/data/current/BP-1273524072-192.168.10.102-1652776218904/current/finalized/subdir0/subdir0,集群中随机三台服务器都存储有相同的数据,作为备份[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jfv586y-1658641856515)(…/…/images/image-20220517173209533.png)]

4.1.4 集群启动/停止方式总结

  • 整体启动/停止 HDFS

    start-dfs.sh/stop-dfs.sh
    
  • 整体启动/停止 YARN

    start-yarn.sh/stop-yarn.sh
    
  • 分别启动/停止 HDFS 组件

    hdfs --daemon start/stop namenode/datanode/secondarynamenode
    
  • 启动/停止 YARN

    yarn --daemon start/stop resourcemanager/nodemanager
    
  • 创建shell脚本一键启动集群

    #!/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-3.1.3/sbin/start-dfs.sh"
     echo " --------------- 启动 yarn ---------------"
    ssh hadoop103 "/opt/module/hadoop-3.1.3/sbin/start-yarn.sh"
     echo " --------------- 启动 historyserver ---------------"
     ssh hadoop102 "/opt/module/hadoop-3.1.3/bin/mapred --daemon start historyserver"
    ;;
    "stop")
     echo " =================== 关闭 hadoop 集群 ==================="
     echo " --------------- 关闭 historyserver ---------------"
     ssh hadoop102 "/opt/module/hadoop-3.1.3/bin/mapred --daemon stop historyserver"
     echo " --------------- 关闭 yarn ---------------"
     ssh hadoop103 "/opt/module/hadoop-3.1.3/sbin/stop-yarn.sh"
     echo " --------------- 关闭 hdfs ---------------"
     ssh hadoop102 "/opt/module/hadoop-3.1.3/sbin/stop-dfs.sh"
    ;;
    *)
     echo "Input Args Error..."
    ;;
    esac
    
  • 创建shell脚本查看每个节点java运行进程

    #!/bin/bash
    for host in hadoop102 hadoop103 hadoop104
    do
     echo =============== $host ===============
     ssh $host jps
    done
    

4.1.5 常用端口号(面试)

  • 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
    

4.1.6 常用配置文件

  • hadoop3.x

    core-site.xml, hdfs-site.xml, yarn-site.xml, mapred-site.xml, workers
    
  • hadoop2.x

    core-site.xml, hdfs-site.xml, yarn-site.xml, mapred-site.xml, slaves
    

4.1.6 集群时间同步

  • 如果服务器能连接外网则不需要时间同步,否则需要时间同步

5 HDFS

5.1 概述

  • 背景:随着数据量越来越大,在一个操作系统存不下所有的数据,那么就分配到更多的操作系 统管理的磁盘中,但是不方便管理和维护,迫切需要一种系统来管理多台机器上的文件,这 就是分布式文件管理系统。HDFS 只是分布式文件管理系统中的一种。

  • 定义:HDFS(Hadoop Distributed File System),它是一个文件系统,用于存储文件,通过目 录树来定位文件;其次,它是分布式的,由很多服务器联合起来实现其功能,集群中的服务器有各自的角色。

  • HDFS 的使用场景:适合一次写入,多次读出的场景。一个文件经过创建、写入和关闭 之后就不需要改变。

  • 优点:1)高容错性:数据自动保存多个副本。它通过增加副本的形式,提高容错性。当某一个副本丢失以后,它可以自动恢复。
    2)适合处理大数据:能够处理数据规模达到GB、TB、甚至PB级别的数据;:能够处理百万规模以上的文件数量,数量相当之大。
    3)可构建在廉价机器上,通过多副本机制,提高可靠性。

  • 缺点:1)不适合低延时数据访问,比如毫秒级的存储数据,是做不到的。
    2)无法高效的对大量小文件进行存储。存储大量小文件的话,它会占用NameNode大量的内存来存储文件目录和 块信息。这样是不可取的,因为NameNode的内存总是有限的;小文件存储的寻址时间会超过读取时间,它违反了HDFS的设计目标。
    3)不支持并发写入、文件随机修改。一个文件只能有一个写,不允许多个线程同时写;仅支持数据append(追加),不支持文件的随机修改。

  • HDFS的组成架构:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mul1ayY4-1658641856515)(…/…/images/image-20220520111832997.png)]
    1)NameNode(nn):就是Master,它 是一个主管、管理者。
    (1)管理HDFS的名称空间;
    (2)配置副本策略;
    (3)管理数据块(Block)映射信息;
    (4)处理客户端读写请求。

    2)DataNode:就是Slave。NameNode 下达命令,DataNode执行实际的操作。
    (1)存储实际的数据块;
    (2)执行数据块的读/写操作。

    3)Client:就是客户端。
    (1)文件切分。文件上传HDFS的时候,Client按照NameNode的文件块大小将文件切分成一个一个的Block,然后进行上传;
    (2)与NameNode交互,获取文件的位置信息;
    (3)与DataNode交互,读取或者写入数据;
    (4)Client提供一些命令来管理HDFS,比如NameNode格式化;
    (5)Client可以通过一些命令来访问HDFS,比如对HDFS增删查改操作;

    4)Secondary NameNode:并非NameNode的热备。当NameNode挂掉的时候,它并不 能马上替换NameNode并提供服务。
    (1)辅助NameNode,分担其工作量,比如定期合并Fsimage和Edits,并推送给NameNode ;
    (2)在紧急情况下,可辅助恢复NameNode。

  • HDFS文件块大小:HDFS中的文件在物理上是分块存储(Block),块的大小可以通过配置参数 ( dfs.blocksize)来规定,默认大小在Hadoop2.x/3.x版本中是128M,1.x版本中是64M。

    为什么默认是128M?
    1)如果寻址时间约为10ms, 即查找到目标block的时间为 10ms。
    2)寻址时间为传输时间的1% 时,则为最佳状态。 因此,传输时间 =10ms/0.01=1000ms=1s
    3)目前普通磁盘的传输速率普遍为100MB/s
    4)则块大小为1s*100MB/s=100MB,因此默认选取128MB为块大小
    5)如果用的是固态硬盘,传输速率一般为200-300MB/s,块大小一般会设置为256MB

    为什么块的大小不能设置太小,也不能设置太大?
    1)HDFS的块设置太小,会增加寻址时间,程序一直在找块的开始位置;
    2)如果块设置的太大,从磁盘传输数据的时间会明显大于定位这个块开 始位置所需的时间。导致程序在处理这块数据时,会非常慢。

5.2 HDFS的Shell相关操作(开发重点)

  • 基本语法

    hadoop fs 具体命令 或 hdfs dfs 具体命令
    
  • -help:输出命令参数

    hadoop fs -help rm
    
  • 操作命令

    # 显示目录信息
    hadoop fs -ls HDFS路径
    # 显示文件内容
    hadoop fs -cat HDFS路径
    # 创建文件夹
    hadoop fs -mkdir HDFS路径
    # 修改文件所属权限
    hadoop fs -chmod 
    hadoop fs -chown
    # 从 HDFS 的一个路径拷贝到 HDFS 的另一个路径
    hadoop fs -cp HDFS路径 HDFS路径
    # 在HDFS目录中移动文件
    hadoop fs -mv HDFS路径 HDFS路径
    # 显示一个文件的末尾1kb的数据
    hadoop fs -tail HDFS文件路径
    # 删除文件或文件夹
    hadoop fs -rm HDFS文件路径
    # 递归删除目录及目录内容
    hadoop fs -rm -r HDFS路径
    # 统计文件夹的大小信息,显示目标路径下每个文件的大小以及每个文件的副本大小
    hadoop fs -du HDFS路径
    # 设置HDFS中文件的副本数量,若设置的副本数量大于当前集群节点数量的话,则最多只有集群节点数量个副本,当增加节点时会自动拷贝生成副本
    hadoop fs -setrep 副本数量 HDFS文件路径
    
  • 上传命令

    # 从本地剪切粘贴到HDFS
    hadoop fs -moveFromLocal 本地文件路径 HDFS路径
    
  • 下载命令

    # 从本地文件中拷贝文件到HDFS路径中
    hadoop fs -copyFromLocal 本地文件路径 HDFS路径
    或
    hadoop fs -put 本地文件路径 HDFS路径
    # 追加一个文件到已经存在的文件末尾
    hadoop fs -appendToFile 本地文件路径 HDFS路径文件路径
    # 从HDFS拷贝到本地
    hadoop fs -copyToLocal HDFS文件路径 本地路径
    或
    hadoop fs -get HDFS文件路径 本地路径 # 如果本地路径具体到文件名的话可以同时将文件改名
    

5.3 HDFS的客户端API(在Windows对集群进行操作)

5.3.1 环境准备

  • 下载windows依赖文件
  • 配置HADOOP_HOME环境变量[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1qZRXt9-1658641856515)(…/…/images/image-20220521104453707.png)]
  • 配置Path环境变量[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hVNgqBgD-1658641856516)(…/…/images/image-20220521104543403.png)]
  • 验证 Hadoop 环境变量是否正常。双击 hadoop目录下的bin目录下的winutils.exe,如果报如下错误。说明缺少微软运行库(正版系统往往有这个问题)。

5.3.2 创建文件夹

  • 创建一个Maven工程 HdfsClientDemo,并导入相应的依赖坐标+日志添加

    <dependencies>
     <dependency>
     <groupId>org.apache.hadoop</groupId>
     <artifactId>hadoop-client</artifactId>
     <version>3.1.3</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>
    
  • 在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在文件 中填入

    log4j.rootLogger=INFO, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
    log4j.appender.logfile=org.apache.log4j.FileAppender
    log4j.appender.logfile.File=target/spring.log
    log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
    log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
    
  • 创建 HdfsClient 类,执行以下的单元测试创建文件夹

    package com.F.hdfs;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.FileSystem;
    import org.apache.hadoop.fs.Path;
    import org.junit.Test;
    
    import java.io.IOException;
    import java.net.URI;
    import java.net.URISyntaxException;
    
    
    public class HdfsClient {
    
        @Test
        public void testmkdir() throws URISyntaxException, IOException, InterruptedException {
            // 连接的集群nn地址,这里用的端口是内部通讯端口
            URI uri = new URI("hdfs://hadoop102:8020");
            // 创建一个配置文件
            Configuration configuration = new Configuration();
            // 用户
            String user = "F";
            // 获取客户端对象
            FileSystem fs = FileSystem.get(uri, configuration, user);
            // 创建一个文件夹
            fs.mkdirs(new Path("/xiyou/huaguoshan"));
            // 关闭资源
            fs.close();
    
        }
    }
    
    
  • 以上的代码有很多重复性操作,拆分进行分装。

    package com.F.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;
    
    
    public class HdfsClient {
    
        private FileSystem fs;
    
        @Before
        public void init() throws URISyntaxException, IOException, InterruptedException {
            // 连接的集群nn地址,这里用的端口是内部通讯端口
            URI uri = new URI("hdfs://hadoop102:8020");
            // 创建一个配置文件
            Configuration configuration = new Configuration();
            // 用户
            String user = "F";
            // 获取客户端对象
            fs = FileSystem.get(uri, configuration, user);
        }
    
        @After
        public void close() throws IOException {
            // 关闭资源
            fs.close();
        }
    
        @Test
        public void testmkdir() throws URISyntaxException, IOException, InterruptedException {
    
            // 创建一个文件夹
            fs.mkdirs(new Path("/xiyou/huaguoshan1"));
    
        }
    }
    
    

5.3.3 上传文件

package com.F.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;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        // configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }

    
    /**
     * 参数优先级
     * hdfs-default.xml < hdfs-site.xml < 在项目resources下的hdfs-site.xml < 代码里面的配置
     * 则如果要修改某些参数如修改文件副本数,在resources文件下新增hdfs-site.xml可以达到修改参数的目的
     * 或者在代码里进行配置比如利用上面的configuration对象进行设置,同样可以达到修改参数的目的
     * @throws IOException
     */
    @Test
    public void testPut() throws IOException {
        // 参数1:是否删除原数据
        // 参数2: 是否允许覆盖
        // 参数3:原数据路径
        // 参数4:目的地路径
        fs.copyFromLocalFile(true, true, new Path("F:\\java\\新建文件夹\\HDFSClient\\src\\main\\sunwukong.txt"), new Path("/xiyou/huaguoshan"));
    }
}

5.3.4 配置参数的优先级

  • hdfs-default.xml < hdfs-site.xml < 在项目resources下的hdfs-site.xml < 代码里面的配置,则如果要修改某些参数如修改文件副本数,在resources文件下新增hdfs-site.xml可以达到修改参数的目的,或者在代码里进行配置比如利用上面的configuration对象进行设置,同样可以达到修改参数的目的。

5.3.5 下载文件

package com.F.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;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }

    //文件下载
    @Test
    public void testGet() throws IOException {
        // 参数1:是否删除原数据
        // 参数2:原文件HDFS路径
        // 参数3:目标地址路径Win
        // 参数4:是否开启本地校验
        fs.copyToLocalFile(true, new Path("/xiyou/huaguoshan"), new Path("F:\\java\\新建文件夹\\HDFSClient\\src\\main"), true);
    }
}

5.3.6 删除

package com.F.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;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }

    //删除
    @Test
    public void testRm() throws IOException {
        // 参数1:要删除的路径
        // 参数2:是否递归删除
        // 删除文件
        fs.delete(new Path("/jdk-8u212-linux-x64.tar.gz"), false);

        // 删除空目录
        fs.delete(new Path("/xiyou1"), false);

        // 删除非空目录
        fs.delete(new Path("/jinguo"), true);
    }
}

5.3.7 文件更名和移动

package com.F.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;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }


    // 文件的更名和移动
    @Test
    public void testmv() throws IOException {
        // 参数1:原文件路径
        // 参数2:目标文件路径
        // 对文件更名
        fs.rename(new Path("/wcinput/word.txt"), new Path("/wcinput/word1.txt"));
        // 对文件移动和更名
        fs.rename(new Path("/wcinput/word1.txt"), new Path("/word2.txt"));
        // 目录更名
        fs.rename(new Path("/wcinput"), new Path("/wcinput1"));
    }
}

5.3.8 获取文件详情

package com.F.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }

    @Test
    public void testmkdir() throws URISyntaxException, IOException, InterruptedException {

        // 创建一个文件夹
        fs.mkdirs(new Path("/xiyou/huaguoshan1"));

    }

    // 获取文件详细信息
    @Test
    public void fileDetail() throws IOException {
        // 参数1:查看哪个路径下的文件信息
        // 参数2:是否递归
        // 获取所有文件信息
        RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/"), true);
        // 遍历文件
        while (listFiles.hasNext()) {
            LocatedFileStatus fileStatus = listFiles.next();
            fileStatus.getPath();
            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));
        }
    }
}

5.3.9 判断文件或文件夹

package com.F.hdfs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;


public class HdfsClient {

    private FileSystem fs;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        // 连接的集群nn地址
        URI uri = new URI("hdfs://hadoop102:8020");
        // 创建一个配置文件
        Configuration configuration = new Configuration();
        configuration.set("dfs.replication", "2");
        // 用户
        String user = "F";
        // 获取客户端对象
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        // 关闭资源
        fs.close();
    }

    @Test
    public void testmkdir() throws URISyntaxException, IOException, InterruptedException {

        // 创建一个文件夹
        fs.mkdirs(new Path("/xiyou/huaguoshan1"));

    }

    // 判断是文件夹还是文件
    @Test
    public void testFile() throws IOException {
        FileStatus[] listStatus = fs.listStatus(new Path("/"));
        for (FileStatus status : listStatus) {
            if (status.isFile()) {
                System.out.println("文件:" + status.getPath().getName());
            } else {
                System.out.println("目录:" + status.getPath().getName());
            }
        }
    }
}

5.4 HDFS的读写流程(面试重点)

5.4.1 写数据流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WglEuf8-1658641856516)(…/…/images/image-20220526204706937.png)]

  1. 客户端创建一个分布式文件系统,向NameNode发送上传文件请求,NameNode检查目录树是否能够创建文件
  2. NameNode响应
  3. 客户端请求上传第一个块,请求返回DataNode
  4. NameNode根据节点是否可用,负载均衡,以及节点距离返回DataNode
  5. 与DataNode建立块传输通道(比客户端同时连接效率高,因为客户端同时连接多个DataNode的话,则需要等待所有的DataNode传输成功才能接着工作,这种传输通道可以把数据交给DataNode,让数据不断往下传输)。
  6. 创建数据流(先创建缓冲队列,缓冲队列的数据单位为chunk,包括了512byte数据和4byte的校验位,多个chunk形成一个的Packet开始传输,形成的Packet同时存入另一个ack队列,数据传输应答成功则删除ack队列中的数据,否则重新传),一边往某个节点开始传输数据,一边将其他数据存到内存里后传递给下一个节点。
  7. 数据传输应答。

5.4.2 集群中节点距离的计算

在上传数据到集群中时,NameNode会选择距离待上传数据最近距离的DataNode接受数据,那么集群中节点与节点的距离如何计算?

节点距离=两个节点到达最近的共同祖先的距离总和,某一个节点到机架的距离为1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ssvob0hV-1658641856516)(…/…/images/image-20220526215524611.png)]

5.4.3 机架感知(副本存储节点选择)

  • 第一个副本在客户端所处的节点上,如果客户端在集群外,则随机选一个。(选择原因:距离最近上传速度快)
  • 第二个副本在另一个机架的随机一个节点。(选择原因:保证数据的可靠性)
  • 第三个副本在第二个副本所在机架的随机节点。(选择原因:不跨机架传输上传速度快)

5.4.4 读数据流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TM8jRlDC-1658641856517)(…/…/images/image-20220527103250930.png)]

  1. 客户端创建一个分布式文件系统,请求下载文件
  2. 判断集群中是否存在该文件,以及请求用户是否拥有读取该文件的权限,有的话则返回目标文件的元数据,包括文件信息和块信息
  3. 客户端创建一个数据流,考虑各个DataNode的距离和负载均衡能力,选择DataNode进行读数据。当读取文件包含多个块时,读取形式的串行的,也就是一个块一个块读。

5.5 NN和2NN

思考:NameNode 中的元数据是存储在哪里的?

  • 首先,我们做个假设,如果存储在 NameNode 节点的磁盘中,因为经常需要进行随机访 问,还有响应客户请求,必然是效率过低。因此,元数据需要存放在内存中。但如果只存在内存中,一旦断电,元数据丢失,整个集群就无法工作了。因此产生在磁盘中备份元数据的 FsImage。 这样又会带来新的问题,当在内存中的元数据更新时,如果同时更新 FsImage,就会导 致效率过低,但如果不更新,就会发生一致性问题,一旦 NameNode 节点断电,就会产生数 据丢失。因此,引入 Edits 文件(只进行追加操作,效率很高)。每当元数据有更新或者添 加元数据时,修改内存中的元数据并追加到 Edits 中。这样,一旦 NameNode 节点断电,可 以通过 FsImage 和 Edits 的合并,合成元数据。 但是,如果长时间添加数据到 Edits 中,会导致该文件数据过大,效率降低,而且一旦 断电,恢复元数据需要的时间过长。因此,需要定期进行 FsImage 和 Edits 的合并,如果这 个操作由NameNode节点完成,又会效率过低。因此,引入一个新的节点SecondaryNamenode, 专门用于 FsImage 和 Edits 的合并。
  • fsimage文件是NameNode中关于元数据的镜像,一般称为检查点,它是在NameNode启动时对整个文件系统的快照。fsimage文件是hadoop文件系统元数据的一个永久性的检查点,其中包含hadoop文件系统中的所有目录和文件idnode(索引节点)的序列化信息。
  • NameNode下的data/dfs/name/current跟2NN下的data/dfs/namesecondary/current下都存储有fsimage文件和edits文件

NN:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Avika8lG-1658641856517)(…/…/images/image-20220527111618664.png)]

2NN:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOGLFwEi-1658641856518)(…/…/images/image-20220527111634348.png)]

5.5.1 NameNode工作机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0KYs5vf1-1658641856518)(…/…/images/image-20220527111652373.png)]

  1. 集群一开启,加载编辑日志edits_inprogress_001文件和镜像文件fsimage到内存中。
  2. 当客户端发起对元数据的增删改请求时,会先在编辑日志文件中记录操作日志,再到内存对元数据进行操作。
  3. 当达到checkpoint的触发条件时,2NN就会请求执行checkpoint,即要对编辑日志文件和镜像文件进行合并写入内存。
  4. NN生成一个新的编辑日志文件edits_inprogress_002文件,用于记录客户端新的操作日志,同时将原来的edits_inprogress_001更名为edits_001。
  5. 将edits_001和fsimage拷贝到2NN中,由2NN将它们加载至内存并合并,生成新的fsimage。
  6. 将新的fsimage拷贝回NN,覆盖NN原来的fsimage。

5.5.2 Fsimage和Edits

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rjE2X9fY-1658641856523)(…/…/images/image-20220527114133652.png)]

使用oiv命令可以查看fsimage文件。

基本语法:

hdfs oiv -p 输出文件类型 -i 镜像文件 -o 转换后文件输出路径

示例:

hdfs oiv -p XML -i fsimage_0000000000000000025 -o /opt/module/hadoop-3.1.3/fsimage.xml

fsimage.xml中并没有记录块所对应的DataNode信息,原因是在集群启动后,会要求 DataNode 上报数据块信息,并间隔一段时间后再次上报。

使用oev命令可以查看edits文件。

基本语法:

hdfs oev -p 文件类型 -i 编辑日志 -o 转换后文件输出路径

示例:

hdfs oev -p XML -i edits_0000000000000000707-0000000000000000708 -o /opt/module/hadoop-3.1.3/edits.xml

当NameNode启动时,会合并哪些Edits文件?主要是根据Fsimage镜像文件的文件名序号,NameNode会合并序号大于当前Fsimage镜像文件的文件名序号的Edits文件。

5.5.3 Checkpoint触发条件设置

可以在hdfs-site.xml中设置,以下为默认值,在hdfs-default中是这样设置的,一般不做修改。

<!-- 设置checkpoint执行时间 -->
<property>
 <name>dfs.namenode.checkpoint.period</name>
 <value>3600s</value>
</property>
<!-- 设置多少次操作后执行checkpoint -->
 <name>dfs.namenode.checkpoint.txns</name>
	<value>1000000</value>
    <description>操作动作次数</description>
</property>
<!-- 设置多久检查一次操作次数 -->
<property>
     <name>dfs.namenode.checkpoint.check.period</name>
     <value>60s</value>
    <description> 1 分钟检查一次操作次数</description>
</property>

5.6 Datanode工作机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6MVDnD4A-1658641856523)(…/…/images/image-20220528103915112.png)]

  1. DataNode启动后向NameNode注册
  2. NameNode记录块信息
  3. DataNode定时向NameNode上报所有块信息
  4. DataNode每3秒与NameNode进行信息传输,说明自身还在线。若NameNode超过十分钟+30秒没有收到某个DataNode的信息,则认为该节点不可用

在hdfs-site.xml中可以更改以上配置:

<!-- DN 向 NN 汇报当前解读信息的时间间隔,默认 6 小时 -->
<property>
    <name>dfs.blockreport.intervalMsec</name>
    <value>21600000</value>
    <description>Determines block reporting interval in
    milliseconds.</description>
</property>
<!-- DN 扫描自己节点块信息列表的时间,默认 6 小时 -->
<property>
    <name>dfs.datanode.directoryscan.interval</name>
    <value>21600s</value>
    <description>Interval in seconds for Datanode to scan datadirectories and reconcile the difference between blocks in memory and on
    the disk.Support multiple time unit suffix(case insensitive), as described in dfs.heartbeat.interval.
    </description>
</property>
<!-- 单位为毫秒 -->
<property>
     <name>dfs.namenode.heartbeat.recheck-interval</name>
     <value>300000</value>
</property>
<!-- 单位为秒 -->
<property>
     <name>dfs.heartbeat.interval</name>
     <value>3</value>
</property>

5.7 HDFS核心参数配置

5.7.1 NameNode内存生产配置

  • Hadoop2.x 系列,配置 NameNode 内存,NameNode 内存默认 2000m,如果服务器内存 4G,NameNode 内存可以配置 3g。在 hadoop-env.sh 文件中配置如下。

    HADOOP_NAMENODE_OPTS=-Xmx3072m
    
  • Hadoop3.x 系列,配置 NameNode 内存
    (1)hadoop-env.sh 中描述 Hadoop 的内存是动态分配的

    # The maximum amount of heap to use (Java -Xmx). If no unit
    # is provided, it will be converted to MB. Daemons will
    # prefer any Xmx setting in their respective _OPT variable.
    # There is no default; the JVM will autoscale based upon machine
    # memory size.
    # export HADOOP_HEAPSIZE_MAX=
    # The minimum amount of heap to use (Java -Xms). If no unit
    # is provided, it will be converted to MB. Daemons will
    # prefer any Xms setting in their respective _OPT variable.
    # There is no default; the JVM will autoscale based upon machine
    # memory size.
    # export HADOOP_HEAPSIZE_MIN=
    HADOOP_NAMENODE_OPTS=-Xmx102400m
    

    ​ (2)查看 NameNode 占用内存

    [atguigu@hadoop102 ~]$ jps
    3088 NodeManager
    2611 NameNode
    3271 JobHistoryServer
    2744 DataNode
    [atguigu@hadoop102 ~]$ jmap -heap 2744
    Heap Configuration:
     MaxHeapSize = 1031798784 (984.0MB)
    

    查看发现 hadoop102 上的 NameNode 和 DataNode 占用内存都是自动分配的,且相等。 不是很合理。经验参考: https://docs.cloudera.com/documentation/enterprise/6/release-notes/topics/rg_hardware_requirements.html#concept_fzz_dq4_gbb

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-07QfrmY6-1658641856524)(D:\documents\notes\md\images\hadoop\image-20220712180744450.png)]

具体修改:hadoop-env.sh

# 配置namenode内存
export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS -Xmx1024m"
# 配置datanode内存
export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS-Xmx1024m"

5.7.2 NameNode心跳并发配置

  • DataNode在启动后需要向NameNode进行注册,并且需要定时向NameNode提交块信息,定时向NameNode提交信息说明自身在线,客户端跟DataNode也会有一些信息交流,因此NameNode需要设置合适的线程来处理这些信息。

  • 在hdfs-site.xml中有一个handler.count参数,用于配置线程数。企业经验:dfs.namenode.handler.count=20 × 𝑙𝑜𝑔𝑒 𝐶𝑙𝑢𝑠𝑡𝑒𝑟 𝑆𝑖𝑧𝑒,比如集群规模(DataNode 台 数)为 3 台时,此参数设置为 21。

    <!-- 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>
    

5.7.3 回收站配置

  • 回收站工作机制:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eDw32ZNe-1658641856524)(D:\documents\notes\md\images\hadoop\image-20220715180620742.png)]

  • 回收站参数
    (1)默认值 fs.trash.interval = 0,0 表示禁用回收站;其他值表示设置文件的存活时间。 (2)默认值 fs.trash.checkpoint.interval = 0,检查回收站的间隔时间。如果该值为 0,则该 值设置和 fs.trash.interval 的参数值相等。 (3)要求 fs.trash.checkpoint.interval <= fs.trash.interval。

  • 启用回收站:在core-site.xml中修改

    <!-- 配置垃圾回收的时间为1分钟 -->
    <property>
     <name>fs.trash.interval</name>
     <value>1</value>
    </property>
    
  • 在HDFS网页中直接删除的文件不会进到回收站中,通过程序删除的文件也不会经过回收站,需要调用moveToTrash才会进入回收站。只有利用hadoop fs -rm命令删除的文件才会进入回收站。

    Trash trash = New Trash(conf);
    trash.moveToTrash(path);
    
  • 回收站目录在 HDFS 集群中的路径:/user/atguigu/.Trash/….

  • 恢复回收站数据,把回收站的数据复制或移动出去

    hadoop fs -mv /user/atguigu/.Trash/Current/user/atguigu/input /user/atguigu/input
    

5.8 集群压测

5.8.1 HDFS写性能测试

  • 向HDFS集群每个节点写10个128M的文件,这里nrFiles参数为生成mapTask的数量,一般设置为CPU总核数-1,这样的目的是确保每个节点都能开启mapTask,可通过hadoop103:8088查看CPU核数。
    返回结果解析:

    Number of files:生成 mapTask 数量,一般是集群中(CPU 核数-1),我们测试虚 拟机就按照实际的物理内存-1 分配即可
    Total MBytes processed:单个 map 处理的文件大小
    Throughput mb/sec:单个 mapTak 的吞吐量 计算方式:处理的总文件大小/每一个 mapTask 写数据的时间累加 集群整体吞吐量:生成 mapTask 数量*单个 mapTak 的吞吐量
    Average IO rate mb/sec::平均 mapTak 的吞吐量 计算方式:每个 mapTask 处理文件大小/每一个 mapTask 写数据的时间全部相加除以 task 数量
    IO rate std deviation:方差、反映各个 mapTask 处理的差值,越小越均衡

    hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -write -nrFiles 10 -fileSize 128MB
    
    2021-02-09 10:43:16,853 INFO fs.TestDFSIO: ----- TestDFSIO ----- : write
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Date & time: Tue Feb 
    09 10:43:16 CST 2021
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Number of files: 10
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Total MBytes processed: 1280
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Throughput mb/sec: 1.61
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Average IO rate mb/sec: 1.9
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: IO rate std deviation: 0.76
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO: Test exec time sec: 133.05
    2021-02-09 10:43:16,854 INFO fs.TestDFSIO:
    
  • 如果测试出现异常,可能是设置了虚拟内存的原因,在 yarn-site.xml 中设置虚拟内存检测为 false

    <!--是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则
    直接将其杀掉,默认是 true -->
    <property>
     <name>yarn.nodemanager.vmem-check-enabled</name>
     <value>false</value>
    </property>
    
  • 测试结果分析,集群中每个节点设置网络带宽为100Mbps=12.5M/s,如果集群整体吞吐量大于集群整体带宽,则实测速度可以满足要求,否则需要考虑使用固态硬盘或者增加硬盘。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSBMps3D-1658641856524)(D:\documents\notes\md\images\hadoop\image-20220715192252167.png)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTowSYH8-1658641856525)(D:\documents\notes\md\images\hadoop\image-20220715192306644.png)]

5.8.2 HDFS读性能测试

  • 读取 HDFS 集群 10 个 128M 的文件

     hadoop jar /opt/module/hadoop-jobclient-3.1.3-tests.jar TestDFSIO -read -nrFiles 10 -fileSize 
    128MB
    2021-02-09 11:34:15,847 INFO fs.TestDFSIO: ----- TestDFSIO ----- : read
    2021-02-09 11:34:15,847 INFO fs.TestDFSIO: Date & time: Tue Feb 
    09 11:34:15 CST 2021
    2021-02-09 11:34:15,847 INFO fs.TestDFSIO: Number of files: 10
    2021-02-09 11:34:15,847 INFO fs.TestDFSIO: Total MBytes processed: 1280
    2021-02-09 11:34:15,848 INFO fs.TestDFSIO: Throughput mb/sec: 200.28
    2021-02-09 11:34:15,848 INFO fs.TestDFSIO: Average IO rate mb/sec: 266.74
    2021-02-09 11:34:15,848 INFO fs.TestDFSIO: IO rate std deviation: 143.12
    2021-02-09 11:34:15,848 INFO fs.TestDFSIO: Test exec time sec: 20.83
    
  • 测试结果分析:为什么读取文件速度大于网络带宽?由于目前只有三台服务器,且有三 个副本,数据读取就近原则,相当于都是读取的本地磁盘数据,没有走网络。

  • 删除测试生成数据

     hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -clean
    

5.9 HDFS多目录

5.9.1 NameNode多目录配置

  • NameNode 的本地目录可以配置成多个放在不同的文件夹承担下,且每个目录存放内容相同,增加了可靠性

  • 具体配置
    (1)在 hdfs-site.xml 文件中添加如下内容

    <property>
     	<name>dfs.namenode.name.dir</name>
    	<value>file://${hadoop.tmp.dir}/dfs/name1,file://${hadoop.tmp.dir}/dfs/name2</value>
    </property>
    

    (2)由于namenode的路径改变(原先存储在/opt/module/hadoop-3.1.3/data下),所以停止集群,删除三台节点的 data 和 logs 中所有数据。一般多目录配置在搭建集群的时候配置,生产环境下不做修改。
    (3)格式化集群并启动。

    bin/hdfs namenode -format
    sbin/start-dfs.sh
    

​ (4)配置成功后在/opt/module/hadoop-3.1.3/data/dfs会有name1和name2两个文件夹,里面的内容完全一样

  • 实际生产环境下通过HA来配置多个NameNode。

5.9.2 DataNode多目录配置

  • DataNode 可以配置成多个目录,每个目录存储的数据不一样,比如扩充新硬盘时可以这样配置

  • 具体配置
    在想要配置多目录的节点上的hdfs-site.xml 文件中添加如下内容

    <property>
     	<name>dfs.datanode.data.dir</name>
    	<value>file://${hadoop.tmp.dir}/dfs/data1,file://${hadoop.tmp.dir}/dfs/data2</value>
    </property>
    

    配置成功后在/opt/module/hadoop-3.1.3/data/dfs下会有data1和data2两个文件夹,里面的内容为空,往集群上传数据后只有data1中会有数据,说明这两个文件夹里面的内容是不一样的。

5.9.3 集群数据均衡之磁盘间数据均衡

  • 在5.9.2中配置了DataNode的多目录,相当于新增了一个空磁盘。在生产环境中,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可 以执行磁盘数据均衡命令,使得原先磁盘的数据分配一些到新磁盘中。(Hadoop3.x 新特性)

  • 具体操作

    #(1)生成均衡计划(我们只有一块磁盘,不会生成计划),如果有多个磁盘会生成一个json文件
    hdfs diskbalancer -plan hadoop103
    # (2)执行均衡计划
    hdfs diskbalancer -execute hadoop103.plan.json
    # (3)查看当前均衡任务的执行情况
    hdfs diskbalancer -query hadoop103
    # (4)取消均衡任务
    hdfs diskbalancer -cancel hadoop103.plan.json
    

5.10 HDFS—集群扩容及缩容

5.10.1 添加白名单

  • 白名单:表示在白名单的主机 IP 地址可以,用来存储数据。配置白名单,可以尽量防止黑客恶意访问攻击。

  • 配置白名单步骤:
    1)在 NameNode 节点的/opt/module/hadoop-3.1.3/etc/hadoop 目录下创建 whitelist,在其中添加主机名称,一行一个
    2)在 hdfs-site.xml 配置文件中增加 dfs.hosts 配置参数

    <!-- 白名单 -->
    <property>
         <name>dfs.hosts</name>
         <value>/opt/module/hadoop-3.1.3/etc/hadoop/whitelist</value>
    </property>
    

    3)分发配置文件 whitelist,hdfs-site.xml
    4)第一次添加白名单必须重启集群,不是第一次,只需要刷新 NameNode 节点即可

    # 刷新NameNode
    hdfs dfsadmin -refreshNodes
    
  • 如果某个集群节点不在白名单内,那么该节点只能访问集群(比如上传文件操作)但是没有办法存储数据

5.10.2 服役新服务器

  • 随着公司业务的增长,数据量越来越大,原有的数据节点的容量已经不能满足存储数据 的需求,需要在原有集群基础上动态添加新的数据节点。

  • 环境准备
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9rueB2Bw-1658641856525)(D:\documents\notes\md\images\hadoop\image-20220716153253623.png)]

  • 具体步骤:
    (1)新服务器直接启动 DataNode,即可关联到集群

    hdfs --daemon start datanode
    yarn --daemon start nodemanager
    

    (2)在白名单 whitelist 中增加新服务器,分发并重启集群

5.10.3 服务器间数据均衡

  • 在企业开发中,如果经常在 hadoop102 和 hadoop104 上提交任务,且副本数为 2,由于数据本地性原则,就会导致 hadoop102 和 hadoop104 数据过多,hadoop103 存储的数据量小。另一种情况,就是新服役的服务器数据量比较少,需要执行集群均衡命令。

  • 相关命令:

    # 开启数据均衡命令,对于参数 10,代表的是集群中各个节点的磁盘空间利用率相差不超过 10%,可根据实际情况进行调整。
    sbin/start-balancer.sh -threshold 10
    # 停止数据均衡命令,由于 HDFS 需要启动单独的 Rebalance Server 来执行 Rebalance 操作,所以尽量不要在 NameNode 上执行 start-balancer.sh,而是找一台比较空闲的机器。
    sbin/stop-balancer.sh
    

5.10.4 黑名单退役旧节点

  • 表示在黑名单的主机 IP 地址不可以,用来存储数据。配置黑名单,用来退役服务器。

  • 配置黑名单步骤:
    (1)在 NameNode 节点的/opt/module/hadoop-3.1.3/etc/hadoop 目录下创建blacklist文件,在其中添加要加入黑名单的主机名称,一行一个
    (2)在 hdfs-site.xml 配置文件中增加 dfs.hosts.exclude 配置参数

    <!-- 黑名单 -->
    <property>
         <name>dfs.hosts.exclude</name>
         <value>/opt/module/hadoop-3.1.3/etc/hadoop/blacklist</value>
    </property>
    

    (3)分发配置文件 blacklist,hdfs-site.xml
    (4)第一次添加黑名单必须重启集群,不是第一次,只需要刷新 NameNode 节点即可。一般生产环境下在搭建集群的时候会提前建立跟配置好白名单跟黑名单文件,否则新增黑名单跟白名单配置需要重启集群很麻烦。

    # 刷新NameNode
    hdfs dfsadmin -refreshNodes
    
  • 退役的服务器会将自身的数据副本复制给其他某个节点一份,来保证集群数据副本数保持不变。注意:如果副本数是 3,服役的节点小于等于 3,是不能退役成功的,需要修改 副本数后才能退役。

  • 服务器退役过程在web浏览器中会有所显示,退役节点的状态为 decommission in progress(退役中),说明数据 节点正在复制数据块到其他节点,等待退役节点状态为 decommissioned(所有块已经复制完成),需要手动停止该节点及节点资源 管理器。

    hdfs --daemon stop datanode
    yarn --daemon stop nodemanager
    
  • 如果数据不均衡,可以用命令实现集群的再平衡

    sbin/start-balancer.sh -threshold 10
    

5.11 HDFS存储优化

5.11.1 纠删码

  • HDFS 默认情况下,一个文件有 3 个副本,这样提高了数据的可靠性,但也带来了 2 倍 的冗余开销。Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约 50%左右的存储空间。原先的存储是一个文件多个副本存储在不同节点上,占用了大量存储资源,利用纠删码,将一个文件拆分成多个数据单元,并生成校验单元,当一些数据单元或校验单元或它们间的组合失效时,能够通过计算来恢复,减少了存储资源的占用,但是增加了计算资源的使用。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4LRi8nb-1658641856525)(D:\documents\notes\md\images\hadoop\image-20220716170243991.png)]

  • 纠删码操作相关命令:

    # 查看纠删码相关命令
    hdfs ec
    Usage: bin/hdfs ec [COMMAND]
     [-listPolicies] # 查看纠删码策略
     [-addPolicies -policyFile <file>] # 增加纠删码策略
     [-getPolicy -path <path>] # 获取某个路径的纠删码策略
     [-removePolicy -policy <policy>] # 删除策略
     [-setPolicy -path <path> [-policy <policy>] [-replicate]] # 对某个路径设置纠删码策略
     [-unsetPolicy -path <path>] # 
     [-listCodecs]
     [-enablePolicy -policy <policy>] # 开启某一个纠删策略
     [-disablePolicy -policy <policy>] # 关闭某一个纠删策略
     [-help <command-name>].
    
  • 查看当前支持的纠删码策略

    [atguigu@hadoop102 hadoop-3.1.3] hdfs ec -listPolicies
    
    Erasure Coding Policies:
    ErasureCodingPolicy=[Name=RS-10-4-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=10, numParityUnits=4]], CellSize=1048576, Id=5], State=DISABLED
    ErasureCodingPolicy=[Name=RS-3-2-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=3, numParityUnits=2]], CellSize=1048576, Id=2], 
    State=DISABLED
    ErasureCodingPolicy=[Name=RS-6-3-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=1], 
    State=ENABLED
    ErasureCodingPolicy=[Name=RS-LEGACY-6-3-1024k, Schema=[ECSchema=[Codec=rs-legacy, numDataUnits=6, numParityUnits=3]], 
    CellSize=1048576, Id=3], State=DISABLED
    ErasureCodingPolicy=[Name=XOR-2-1-1024k, Schema=[ECSchema=[Codec=xor, numDataUnits=2, numParityUnits=1]], CellSize=1048576, Id=4], 
    State=DISABLED
    
    • RS-3-2-1024k:使用 RS 编码,每 3 个数据单元,生成 2 个校验单元,共 5 个单元,也 就是说:这 5 个单元中,只要有任意的 3 个单元存在(不管是数据单元还是校验单元,只要总数=3),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

    • RS-10-4-1024k:使用 RS 编码,每 10 个数据单元(cell),生成 4 个校验单元,共 14 个单元,也就是说:这 14 个单元中,只要有任意的 10 个单元存在(不管是数据单元还是校 验单元,只要总数=10),就可以得到原始数据。每个单元的大小是 1024k=10241024=1048576。

    • RS-6-3-1024k:使用 RS 编码,每 6 个数据单元,生成 3 个校验单元,共 9 个单元,也 就是说:这 9 个单元中,只要有任意的 6 个单元存在(不管是数据单元还是校验单元,只要 总数=6),就可以得到原始数据。每个单元的大小是 1024k=10241024=1048576。

    • RS-LEGACY-6-3-1024k:策略和上面的 RS-6-3-1024k 一样,只是编码的算法用的是 rs-legacy。

    • XOR-2-1-1024k:使用 XOR 编码(速度比 RS 编码快),每 2 个数据单元,生成 1 个校 验单元,共 3 个单元,也就是说:这 3 个单元中,只要有任意的 2 个单元存在(不管是数据 单元还是校验单元,只要总数= 2),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576。

  • 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k 策略的支持,如要使用别的策略需要提前启用。

  • 案例实操:将/input 目录设置为 RS-3-2-1024k 策略
    (1)开启对 RS-3-2-1024k 策略的支持

    hdfs ec -enablePolicy -policy RS-3-2-1024k
    

    (2)在 HDFS 创建目录,并设置 RS-3-2-1024k 策略

    hdfs fs -mkdir /input
    hdfs ec -setPolicy -path /input -policy RS-3-2-1024k
    

    (3)往/input目录上传文件(需要大于2m才能够拆分成3个数据单元,因为数据单元是以1m为单位),可以看到文件副本只有一个,因为采用了RS-3-2-1024k 策略,上传的文件拆分成3个数据单元,并生成了2个校验单元,对任意两个单元进行破坏,都能够进行恢复

5.11.2 异构存储(冷热数据分离)

  • 异构存储主要解决,不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。热数据指经常用的数据,冷数据指不经常使用的数据。

  • 存储类型和存储策略[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vYhC49c-1658641856525)(D:\documents\notes\md\images\hadoop\image-20220716202949426.png)]

  • 查看当前有哪些存储策略可以用

    hdfs storagepolicies -listPolicies
    
  • 为指定路径(数据存储目录)设置指定的存储策略

    hdfs storagepolicies -setStoragePolicy -path xxx -policy xxx
    
  • 获取指定路径(数据存储目录或文件)的存储策略

    hdfs storagepolicies -getStoragePolicy -path xxx
    
  • 取消存储策略;执行该命令之后该目录或者文件,以其上级的目录为准,如果是根 目录,那么就是 HOT

    hdfs storagepolicies -unsetStoragePolicy -path xxx
    
  • 在节点的 hdfs-site.xml 添加如下信息,开启存储策略;在datanode存储路径前加上[SSD],系统就会认为这个路径是固态硬盘,加上[RAM-DISK]就会认定这个路径是内存

    <property>
        <name>dfs.storage.policy.enabled</name>
        <value>true</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name> 
        <value>[SSD]file:///opt/module/hadoop-3.1.3/hdfsdata/ssd,[RAM_DISK]file:///opt/module/hadoop-3.1.3/hdfsdata/ram_disk</value>
    </property>
    
  • 对一个路径设置了存储策略之后,通过查看路径下的文件分布发现文件仍在原处,需要手动让路径下的文件按照存储策略自行移动文件块

    # 查看路径下的文件分布
    hdfs fsck 路径 -files -blocks -locations
    # 手动让路径下的文件按照存储策略自己移动文件夹
    hdfs mover 路径
    
  • 注意:当我们将目录设置为 COLD 并且我们未配置 ARCHIVE 存储目录的情况下,不 可以向该目录直接上传文件,会报出异常。

  • 当把一个路径设置为LAZY_PERSIST 策略时,会发现所有的文件块都是存储在 DISK,按照理论一个副本存储在 RAM_DISK, 其他副本存储在 DISK 中,这是因为,我们还需要配置“dfs.datanode.max.locked.memory”, “dfs.block.size”参数。dfs.datanode.max.locked.memory默认值为0,表示默认不允许用内存来存储数据,如果要在内存中存储数据的话,需要设置dfs.datanode.max.locked.memory参数大于块大小。并且dfs.datanode.max.locked.memory最大只能设置为64KB,所以还需要对dfs.block.size进行调整。

5.12 HDFS故障排除

5.12.1 NameNode故障排除

  • 需求:NameNode 进程挂了并且存储的数据也丢失了,如何恢复 NameNode

  • 具体步骤:
    (1)拷贝 SecondaryNameNode 中数据到原 NameNode 存储数据目录

    [atguigu@hadoop102 dfs]$ scp -r atguigu@hadoop104:/opt/module/hadoop-3.1.3/data/dfs/namesecondary/* ./name/
    

    (2)重新启动 NameNode

    [atguigu@hadoop102 hadoop-3.1.3]$ hdfs --daemon start namenode
    
  • 一般这种方式恢复,如果namenode所在节点有一些操作在edits_progress文件中,那么可能会丢失一部分数据,否则就不会丢失数据。

5.12.2 集群安全模式&磁盘修复

  • 安全模式:文件系统只接受读数据请求,而不接受删除、修改等变更请求

  • 进入安全模式场景(1)NameNode 在加载镜像文件和编辑日志期间处于安全模式;(2)NameNode 再接收 DataNode 注册时,处于安全模式

  • 退出安全模式条件
    (1)最小可用datanode数量大于1,在dfs.namenode.safemode.min.datanodes参数设置,默认为0,表示大于0即可退出安全模式
    (2)副本数达到最小要求的 block 占系统总 block 数的 百分比,默认 0.999f(只允许丢失一个数据块),在dfs.namenode.safemode.threshold-pct配置
    (3)超过集群启动后稳定时间,默认值 30000 毫秒,即 30 秒,在dfs.namenode.safemode.extension配置

  • 相关命令

    (1)bin/hdfs dfsadmin -safemode get (功能描述:查看安全模式状态)
    (2)bin/hdfs dfsadmin -safemode enter (功能描述:进入安全模式状态)
    (3)bin/hdfs dfsadmin -safemode leave(功能描述:离开安全模式状态)
    (4)bin/hdfs dfsadmin -safemode wait (功能描述:等待安全模式状态)
    
  • 磁盘修复案例:丢失2个数据块,当集群启动或者到达节点汇报节点情况时,集群会自动进入安全模式,如何处理
    在web页面的summary中能够看到提醒,由于块数量达不到要求,安全模式已启动,利用上面的命令退出安全模式后,web页面仍然会提醒丢失块,提醒的路径中有一个是数据的元数据,可以把元数据删除就不会有提醒了。如果需要恢复数据的话只能寻求数据恢复帮助。

  • 模拟等待安全模式案例:
    利用上面的命令进入安全模式之后,写一个脚本等待安全模式关闭后立刻执行某个命令,执行下面这个脚本,会进入阻塞状态,在调用上面的命令将安全模式关闭之后,会立即执行脚本中的上传命令。

    #!/bin/bash
    hdfs dfsadmin -safemode wait
    hdfs dfs -put /opt/module/hadoop-3.1.3/README.txt /
    

5.12.3 慢磁盘监控

  • 慢磁盘”指的时写入数据非常慢的一类磁盘。其实慢性磁盘并不少见,当机器运行时 间长了,上面跑的任务多了,磁盘的读写性能自然会退化,严重时就会出现写入数据延时的问题。

  • 寻找慢磁盘的方法:
    1)通过心跳未联系时间。一般出现慢磁盘现象,会影响到 DataNode 与 NameNode 之间的心跳。正常情况心跳时 间间隔是 3s。超过 3s 说明有异常。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJvSQkHZ-1658641856526)(D:\documents\notes\md\images\hadoop\image-20220717135915111.png)]

    2)fio 命令,测试磁盘的读写性能

    # 安装fio工具
    sudo yum install -y fio
    
    • 顺序读测试,会显示读数据的速度,下同

      # 往/home/F/test.log写2g数据并读取
      sudo fio -filename=/home/F/test.log -direct=1 -iodepth 1 -thread -rw=read -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_r
      
    • 顺序写测试

      # 往/home/F/test.log写2g数据
      sudo fio -filename=/home/F/test.log -direct=1 -iodepth 1 -thread -rw=write -ioengine=psync -bs=16k -size=2G -numjobs=10 -
      runtime=60 -group_reporting -name=test_w
      
    • 随机写测试

      sudo fio -filename=/home/F/test.log -direct=1 -iodepth 1 -thread -rw=randwrite -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=test_randw
      
    • 混合随机读写

      sudo fio -filename=/home/atguigu/test.log -direct=1 -iodepth 1 -thread -rw=randrw -rwmixread=70 -ioengine=psync -bs=16k -size=2G -
      numjobs=10 -runtime=60 -group_reporting -name=test_r_w -ioscheduler=noop
      

5.12.4 小文件归档

  • 每个文件均按块存储,每个块的元数据存储在 NameNode 的内存中,因此 HDFS 存储 小文件会非常低效。因为大量的小文件会耗尽 NameNode 中的大部分内存(不管块中文件大小如何,都要耗费同样的内存去存储数据块的元数据)。但注意,存储小 文件所需要的磁盘容量和数据块的大小无关。例如,一个 1MB 的文件设置为 128MB 的块 存储,实际使用的是 1MB 的磁盘空间,而不是 128MB。

  • 解决方法:用HAR文件来存储这些小文件。
    HDFS 存档文件或 HAR 文件,是一个更高效的文件存档工具,它将文件存入 HDFS 块, 在减少 NameNode 内存使用的同时,允许对文件进行透明的访问。具体说来,HDFS 存档文 件对内还是一个一个独立文件,对 NameNode 而言却是一个整体,减少了 NameNode 的内 存。

  • 实际操作:
    (1)需要启动 YARN 进程

     start-yarn.sh
    

    (2)归档文件,把/input 目录里面的所有文件归档成一个叫 input.har 的归档文件,并把归档后文件存储 到/output 路径下。

    hadoop archive -archiveName input.har -p /input /output
    

    (3)查看归档文件,需要在hdfs路径前增加一个har协议

    hadoop fs -ls har:///output/input.har
    

    (4)将归档文件中的文件取出到根目录下

    hadoop fs -cp har:///output/input.har/* /
    

5.13 集群迁移

5.13.1 apache和apache集群间数据拷贝

  • scp 实现两个远程主机之间的文件复制

    scp -r hello.txt root@hadoop103:/user/atguigu/hello.txt // 推 push
    scp -r root@hadoop103:/user/atguigu/hello.txt hello.txt // 拉 pull
    scp -r root@hadoop103:/user/atguigu/hello.txt root@hadoop104:/user/atguigu //是通过本地主机中转实现两个远程主机的文件复制;如果在两个远程主机之间 ssh 没有配置的情况下可以使用该方式。
    
  • 采用 distcp 命令实现两个 Hadoop 集群之间的递归数据复制

    # 假设hadoop102、103、104是一个集群,105、106、107是另一个集群,下面表示从第一个集群复制数据到第二个集群,前面的hadoop102:8020是集群namenode所在地址
    bin/hadoop distcp 
    hdfs://hadoop102:8020/user/atguigu/hello.txt 
    hdfs://hadoop105:8020/user/atguigu/hello.txt
    

5.13.2 apahce和CDH集群间数据拷贝


6 MapReduce

6.1 概述

6.1.1 定义

  • MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于 Hadoop 的数据分析 应用”的核心框架。 MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的 分布式运算程序,并发运行在一个 Hadoop 集群上。
  • 优点:
    1)易于编程:它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量 廉价的 PC 机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一 样的。就是因为这个特点使得 MapReduce 编程变得非常流行。
    2)良好扩展性:当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
    3)高容错性:MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高 的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行, 不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。
    4)适合海量数据计算(TB/PB):可以实现上千台服务器集群并发工作,提供数据处理能力。
  • 缺点:
    1)不擅长实时计算:MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。
    2)不擅长流式计算:流式计算的输入数据(数据一条一条输入)是动态的,而 MapReduce 的输入数据集是静态的(整批数据),不能动态变化。 这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。
    3)不擅长 DAG(有向无环图)计算:多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下, MapReduce 并不是不能做,而是使用后,每个 MapReduce 作业的输出结果都会写入到磁盘, 会造成大量的磁盘 IO,导致性能非常的低下。

6.1.2 核心编程思想:

  • 1)MapReduce运行程序一般分为两个阶段:Map阶段和Reduce阶段
    2)Map阶段的并发MapTask,完全并行运行,互不相干
    3)Reduce阶段的并发Reduce Task,完全互不相干,但是他们的数据依赖于上一个阶段所有MapTask并发实例的输出
    4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7psdwuZf-1658641856526)(…/…/images/image-20220528130914602.png)]

  • 一个完整的 MapReduce 程序在分布式运行时有三类实例进程:
    1)MrAppMaster:负责整个程序的过程调度及状态协调。
    2)MapTask:负责 Map 阶段的整个数据处理流程。
    3)ReduceTask:负责 Reduce 阶段的整个数据处理流程。

  • 采用反编译工具jd-gui反编译源码jar包/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar,发现 WordCount 案例有 Map 类、Reduce 类和驱动类。且 数据的类型是 Hadoop 自身封装的序列化类型。Hadoop常用的数据序列化类型
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hPAEO4Yx-1658641856526)(…/…/images/image-20220528132206483.png)]

6.1.3 MapReduce编程规范

  • 用户编写的程序分成三个部分:Mapper、Reducer 和 Driver。
  • Mapper阶段:
    1)用户字定义的Mapper要继承自己的父类
    2)Mapper的输入数据是KV对的形式(KV的类型自定义),K是偏移量,即存在哪个位置,V是值
    3)Mapper中的业务逻辑写在Map方法中
    4)Mapper的输出数据是KV对的形式(KV的类型自定义)
    5)map方法(MapTask进程)对每一个KV对调用一次
  • Reduce阶段:
    1)用户自定义的Reducer要继承自己的父类
    2)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
    3)Reducer的业务逻辑写在reduce()方法中
    4)ReduceTask进程对每一组相同K的KV对调用一次reduce方法
  • Driver阶段:
    相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是 封装了MapReduce程序相关运行参数的job对象

6.1.4 MapReduce案例

6.1.4.1 WordCount案例
  • 添加依赖和日志配置文件
    在pom.xml下添加依赖

    <dependencies>
     <dependency>
         <groupId>org.apache.hadoop</groupId>
         <artifactId>hadoop-client</artifactId>
         <version>3.1.3</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>
    

    在resources目录下新建log4j.properties,添加以下内容

    log4j.rootLogger=INFO, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
    log4j.appender.logfile=org.apache.log4j.FileAppender
    log4j.appender.logfile.File=target/spring.log
    log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
    log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
    
  • 新建WordCountMapper继承Mapper类(有两个包,一个是mapred包,对应1.x的hadoop,一个是mapreduce包,对应2.x和3.x的hadoop),继承的Mapper需要传入四个泛型

    package com.F.mapreduce.wordcount;
    
    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;
    
    /**
     * KEYIN, map阶段输入的key的类型(偏移量):LongWritable
     * VALUEIN, map阶段输入的value类型(值,这里即文本):Text
     * KEYOUT, map阶段输出的Key类型(值,这里即文本):Text
     * VALUEOUT, map阶段输出的value类型(文本的个数):IntWritable
     */
    public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
        // 用于将字符串转化成Text类型
        private Text outK = new Text();
        // 输出的值
        private IntWritable outV = 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) {
                // 将word封装成Text类型后输出
                outK.set(word);
                
                // 写出
                context.write(outK, outV);
            }
        }
    }
    
    
  • 新建WordCountReducer继承Reducer类,同样要继承mapreduce包下的Reducer类

    package com.F.mapreduce.wordcount;
    
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    /**
     * KEYIN, reduce阶段输入的key的类型(偏移量):Text
     * VALUEIN, reduce阶段输入的value类型(值,这里即文本):IntWritable
     * KEYOUT, reduce阶段输出的Key类型(值,这里即文本):Text
     * VALUEOUT, reduce阶段输出的value类型(文本的个数):IntWritable
     */
    public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        // 用于将int封装成IntWritable写出
        private IntWritable outV = new IntWritable();
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
    
            int sum = 0;
    
            // 累加
            for (IntWritable value : values) {
                sum += value.get()
            }
    
            outV.set(sum);
    
            // 写出
            context.write(key, outV);
        }
    }
    
    
  • 新建一个WordCountDriver

    package com.F.mapreduce.wordcount;
    
    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;
    
    public class WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\words.txt"));
            FileOutputFormat.setOutputPath(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\output1"));
    	
        	// 打包时更改为    
            FileInputFormat.setInputPaths(job, new Path(args[0]));
            FileOutputFormat.setOutputPath(job, new Path(args[1]));
            
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    
  • 执行WordCountDriver的main函数,则会得到输出,以上是在windows本地运行的案例,利用的是本地hadoop包执行,没有用到集群,因此要将该项目打包上传到集群上

  • 将maven项目打成jar包
    在pom.xml中添加插件依赖,若不需要项目的依赖包则添加以下内容

    <build>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.6.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
         
        </plugins>
    </build>
    

    若需要项目的依赖包则添加以下内容

    <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>
    

    然后点击右侧的Maven-Lifecycle-package进行打包,会得到一个没有依赖的项目jar包,还有会得到一个有依赖的项目jar包[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g8wO8qXx-1658641856526)(…/…/images/image-20220529171317006.png)]

    如果集群有依赖包的话就选择第一个进行上传,如果没有的话就选择第二个,上传到集群后,利用hadoop jar命令执行这个jar包,同时给出jar包名,main函数所在类的全类名,输入HDFS路径和输出HDFS路径,如

    hadoop jar wc.jar com.F.mapreduce.wordcount2.WordCountDriver /word2.txt /wcoutput
    

    一般都是在windows下编写代码后上传到集群进行运行,当需要运行的jar包比较多的话就编写脚本,利用任务调度器来运行多个任务

6.2 序列化

6.2.1 序列化概述

  • 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁 盘(持久化)和网络传输。 反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换 成内存中的对象。
  • Java 的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带 很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以, Hadoop 自己开发了一套序列化机制(Writable)。
  • Hadoop 序列化特点:
    (1)紧凑 :高效使用存储空间。(序列化数据带的校验信息较少)
    (2)快速:读写数据的额外开销小。 (传输速度快)
    (3)互操作:支持多语言的交互。(支持多语言的使用,可以用一种语言序列化,另一种语言反序列化)

### 6.2.2 自定义bean对象实现序列化接口(Writable)

  1. 实现 Writable 接口

  2. 反序列化时,需要反射调用空参构造函数,所以必须有空参构造

    public FlowBean() {
    	super();
    }		
    
  3. 重写序列化方法

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }
    
  4. 重写反序列化方法,注意反序列化的顺序和序列化的顺序完全一致

    @Override
    public void readFields(DataInput in) throws IOException {
        upFlow = in.readLong();
        downFlow = in.readLong();
        sumFlow = in.readLong();
    }
    
  5. 要想把结果显示在文件中,需要重写 toString(),可用"\t"分开,方便后续用。

  6. 如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序。

    @Override
    public int compareTo(FlowBean o) {
        // 倒序排列,从大到小
    	return this.sumFlow > o.getSumFlow() ? -1 : 1;
    }
    
    

6.2.3 序列化案例(统计手机号的总上行流量、总下行流量、总流量)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wMVpouJ-1658641856527)(…/…/images/image-20220529213300906.png)]

  • 由于输出数据包含多个数据,所以自定义一个FlowBean类来实现需求

    package com.F.mapreduce.writable;
    
    import org.apache.hadoop.io.Writable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    /**
     * 1. 定义类实现Writable接口
     * 2. 重写序列化和反序列化方法
     * 3. 重写空参构造
     * 4. 重写toString方法
     */
    public class FlowBean implements Writable {
    
        private long upFlow;//上行流量
    
        public long getUpFlow() {
            return upFlow;
        }
    
        public void setUpFlow(long upFlow) {
            this.upFlow = upFlow;
        }
    
        public long getDownFlow() {
            return downFlow;
        }
    
        public void setDownFlow(long downFlow) {
            this.downFlow = downFlow;
        }
    
        public long getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow() {
            this.sumFlow = this.upFlow + this.downFlow;
        }
    
        private long downFlow; //下行流量
        private long sumFlow; //总流量
    
    
        // 空参构造
        public FlowBean() {
    
        }
    
        @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 {
            this.upFlow = in.readLong();
            this.downFlow = in.readLong();
            this.sumFlow = in.readLong();
        }
    
        @Override
        public String toString() {
            return upFlow + "\t" + downFlow + "\t" + sumFlow;
        }
    }
    
    
  • 定义FlowMapper类

    package com.F.mapreduce.writable;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.IOException;
    
    public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    
        private Text outK = new Text();
        private FlowBean outV = new FlowBean();
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    
            // 1 获取一行
            String line = value.toString();
    
            // 2 切割
            String[] sp = line.split("\t");
    
            // 3 抓取想要的数据
            String phone = sp[1];
            String up = sp[sp.length - 3];
            String down = sp[sp.length - 2];
    
            // 4 封装
            outK.set(phone);
            outV.setUpFlow(Long.parseLong(up));
            outV.setDownFlow(Long.parseLong(down));
            outV.setSumFlow();
    
            context.write(outK, outV);
        }
    
    }
    
  • 定义FlowReducer类

    package com.F.mapreduce.writable;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
    
        private FlowBean outV = new FlowBean();
    
        @Override
        protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
    
            // 1 遍历集合累加值
            long totalUp = 0;
            long totaldown = 0;
            for (FlowBean value : values) {
                totalUp += value.getUpFlow();
                totaldown += value.getDownFlow();
            }
    
            // 2 封装outk,outv
            outV.setUpFlow(totalUp);
            outV.setDownFlow(totaldown);
            outV.setSumFlow();
            
            context.write(key, outV);
        }
    }
    
    
  • 定义FlowDriver类

    package com.F.mapreduce.writable;
    
    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 FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            
            // 1 获取job
            Configuration conf = new Configuration();
            Job job = Job.getInstance();
            
            // 2 设置jar
            job.setJarByClass(FlowDriver.class);
            
            // 3 关联mapper和reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
            
            // 4 设置mapper输出的kv类型
            job.setMapOutputValueClass(Text.class);
            job.setMapOutputValueClass(FlowBean.class);
            
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
            
            // 6 设置数据的输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\phone_data.txt"));
            FileOutputFormat.setOutputPath(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main"));
            
            // 7 提交job
            job.waitForCompletion(true);
        }
    }
    
    

6.3 核心框架原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJ7FFOoi-1658641856527)(…/…/images/image-20220530103758932.png)]

  • 利用InputFormat将数据输入给Mapper,默认是以<偏移量,值>的形式按行读取数据,也可以设置其他读取方式
  • Reducer获取Mapper输出的数据前需要经过Shuffle阶段,可以进行排序,分区,压缩等操作
  • 利用OutputFormat将数据从Reducer输出,默认是将结果输出到文件中,也可以设置输出到数据库等

6.3.1 InputFormat数据输入

6.3.1.1 切片与MapTask并行度决定机制
  • MapTask 的并行度(MapTask个数)决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
    思考:1G 的数据,启动 8 个 MapTask,可以提高集群的并发处理能力。那么 1K 的数 据,也启动 8 个 MapTask,会提高集群性能吗?MapTask 并行任务是否越多越好呢?哪些因 素影响了 MapTask 并行度?
  • 数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行 存储。数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个 MapTask。
  • 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定,每一个切片分配一个MapTask并行实例处理,默认情况下切片大小等于块大小(这样效率高),切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ESypGDsr-1658641856527)(…/…/images/image-20220530113150136.png)]
6.3.1.2 Job提交源码
waitForCompletion()
submit();
// 1 建立连接
connect();
// 1)创建提交 Job 的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是 yarn 集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交 job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的 Stag 路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取 jobid ,并创建 Job 路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝 jar 包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向 Stag 路径写 XML 配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交 Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(),job.getCredentials());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JtxjAP8u-1658641856528)(…/…/images/image-20220603121229795.png)]

6.3.1.3 FileInputFormat切片源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qNgEZUhP-1658641856528)(…/…/images/image-20220603121318651.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ym32KYX-1658641856528)(…/…/images/image-20220603122543166.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zlsw6NCW-1658641856528)(…/…/images/image-20220603122854506.png)]

6.3.1.4 TextInputFormat
  • 思考:在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制 格式文件、数据库表等。那么,针对不同的数据类型,MapReduce 是如何读取这些数据的呢? FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、 NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。
  • TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止 符(换行符和回车符),Text 类型。
6.3.1.5 CombineTextInputFormat切片机制
  • 框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会 是一个单独的切片,都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的 MapTask,处理效率极其低下。

  • CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到 一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。

  • 生成切片过程包括:虚拟存储过程和切片过程二部分。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SNJcZko-1658641856528)(…/…/images/image-20220603141037534.png)]

  • 使用方法:在设置输入输出路径之前设置InputFormatClass和虚拟存储切片最大值来控制切片的数量和切割的方式

    package com.F.mapreduce.combineTextInputFormat;
    
    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.CombineTextInputFormat;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
            job.setInputFormatClass(CombineTextInputFormat.class);
            //虚拟存储切片最大值设置 20m
            CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("F:\\大数据\\尚硅谷大数据技术之Hadoop3.x\\资料\\资料\\11_input\\inputcombinetextinputformat"));
            FileOutputFormat.setOutputPath(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\output3"));
    
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    

6.3.2 MapReduce详细工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lO9EqBB3-1658641856529)(D:\documents\notes\md\images\hadoop\image-20220617150551054.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HnIYEUYi-1658641856529)(D:\documents\notes\md\images\hadoop\image-20220617164923151.png)]

  1. 对原始数据进行切片分析
  2. 客户端向YARN提交job切片、job运行相关参数(xml)以及jar包(本地模式不用提交jar包)
  3. YARN开启一个Mrappmaster读取信息,计算MapTask数量,并启动MapTask
  4. MapTask利用InputFormat读取数据(默认是TextInputFormat),将读取内容传递给Mapper(用户自己编写的逻辑)
  5. 将Mapper输出的内容输入到环形缓冲区(outputCollector)(一块内存),环形缓冲区一半用于存储数据,一半用于存储索引(元数据);环形缓冲区默认大小是100M,每一半都是写到80%后就反向写数据。
    (这样写是为了提高写数据的速度,因为如果全部是正向写的话,当一半的环形缓冲区达到100%时,会向磁盘将环形缓冲区的数据写出磁盘,这样的话需要等待将环形缓冲区的数据写入磁盘后才可以向环形缓冲区写入,影响效率;如果当一半的环形缓冲区写到80%时就逆向开始写数据,然后将80%的数据写入磁盘,这样效率会提高;如果逆向写的数据写到20%的位置时,%80的数据还没写入磁盘,则需要等待写入磁盘后才能接着写数据)
  6. 对数据进行分区,当环形缓冲区到达80%向磁盘进行溢写前,对数据进行排序(对索引进行排序)
  7. 将数据溢写到文件,得到溢写文件(文件内会标记数据属于哪个分区,分区且区内有序),对各个分区进行归并排序,使得分区内部有序
  8. 对分区内溢写数据进行合并后再进行归并排序(这是在某些场景下进行的优化)
  9. 所有MapTask任务完成后(可以设置提前,先计算一部分MapTask计算完成的结果),启动相应数量的ReduceTask,并告知ReduceTask处理数据范围(数据分区)
  10. ReduceTask拉取MapTask的数据到本地磁盘,对拉取来的数据文件进行归并排序,再传递给Reducer(用户自己编写的处理逻辑)
  11. Reducerc处理完数据后使用OutPutFormat输出数据(默认使用TextOutPutFormat)
  • MapTask有五个阶段,在将数据传递给Mapper前是Read阶段,传递给Mapper后到输出数据到环形缓冲区前是Map阶段,在将数据输出到环形缓冲区后到溢写到磁盘前是Collect阶段,在将数据溢写到磁盘到对溢写文件进行归并前是溢写阶段,在对溢写文件进行归并是Merge阶段
  • ReduceTask有3个阶段,拉取溢写在磁盘中的数据到ReduceTask本地磁盘到进行归并排序的阶段称为Copy阶段,对拉取过来的数据进行归并排序到传递到Reducer前的阶段叫做Sort阶段,将数据传递到Reducer后的阶段叫Reduce阶段

6.3.3 Shuffle机制(面试重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KAPZLXL-1658641856529)(D:\documents\notes\md\images\hadoop\image-20220618215435147.png)]

  • Shuffle是Map方法之后,Reduce方法之前的数据处理过程
  1. 数据从map方法出来后,经过一个getPartition,对数据进行标记分区(后面对数据进行处理都是按分区进行处理,不同分区的数据会进入到不同的reduce方法中),然后数据再进入环形缓冲区
  2. 当环形缓冲区达到溢写峰值时,对数据进行排序(快排,对key的索引按照字典顺序进行排序)
  3. 对数据进行溢写,获得两个溢写文件,一个是溢写的索引文件,一个是溢写的数据文件,期间可以选择经过一个Combiner,对可合并的数据进行合并
  4. 经过多次溢写后得到多个溢写索引文件和溢写数据文件,对他们进行归并排序,得到两个分区的文件,一个是索引文件,一个是数据文件;归并后得到的数据也可以选择经过Combiner对数据进行合并,然后还能选择对数据进行压缩。
  5. 等待reduceTask拉取数据
  6. reduceTask对拉取来的数据放入内存跟磁盘,并对它们进行归并排序,然后按照相同的key分组(可选,现在用的较少),再传递给reduce方法

6.3.4 Partition分区

  • map方法写出之后会经过一个getPartition方法,对数据进行分区,默认分区采用了一个HashPartitioner来根据key的hashCode对ReduceTasks个数取模得到的。用户可以通过设置ReduceTask的个数来控制有多少个分区,但是如果采用默认分区的话用户没法控制哪个 key存储到哪个分区。

    package org.apache.hadoop.mapreduce.lib.partition;
    
    import org.apache.hadoop.classification.InterfaceAudience;
    import org.apache.hadoop.classification.InterfaceStability;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    /** Partition keys by their {@link Object#hashCode()}. */
    @InterfaceAudience.Public
    @InterfaceStability.Stable
    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;
      }
    
    }
    
    
    package com.F.mapreduce.partition;
    
    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;
    
    public class WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 设置reduceTask个数
            job.setNumReduceTasks(2);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\words.txt"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output4"));
    
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    
  • 如果要让数据按照一定规则存储到某个分区,需要自定义一个Partitioner
    1)自定义类继承Partitioner,重写getPartition()方法

    public class CustomPartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text key, FlowBean value, int numPartitions) {
        // 控制分区代码逻辑
        … …
        return partition;
    	}	
    }
    

    2)在Job驱动中,设置自定义Partitioner

    job.setPartitionerClass(CustomPartitioner.class);
    

    3)自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask

    ob.setNumReduceTasks(5)
    
  • 案例代码,按照手机号前三位进行分区
    新建Partitioner类

    package com.F.mapreduce.partition2;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
    
        @Override
        public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
            // text 是手机号
            String phone = text.toString();
            String prePhone = phone.substring(0, 3);
            int partition;
            if ("136".equals(prePhone)) {
                partition = 0;
            } else if ("137".equals(prePhone)) {
                partition = 1;
            } else if ("138".equals(prePhone)) {
                partition = 2;
            } else if ("139".equals(prePhone)) {
                partition = 3;
            } else {
                partition = 4;
            }
            return partition;
        }
    }
    
    

    设置自定义Partition和ReduceTask个数

    package com.F.mapreduce.partition2;
    
    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 FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job
            Configuration conf = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar
            job.setJarByClass(FlowDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            // 4 设置mapper输出的kv类型
            job.setMapOutputValueClass(Text.class);
            job.setMapOutputValueClass(FlowBean.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            job.setPartitionerClass(ProvincePartitioner.class);
            job.setNumReduceTasks(5);
    
            // 6 设置数据的输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\phone_data.txt"));
            FileOutputFormat.setOutputPath(job, new Path("F:\\java\\新建文件夹\\MapReduceDemo\\src\\main\\output3"));
    
            // 7 提交job
            job.waitForCompletion(true);
        }
    }
    
    
  • 分区总结
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Q9G178Z-1658641856529)(D:\documents\notes\md\images\hadoop\image-20220620122347395.png)]

### 6.3.5 MapReduce排序

  • 排序是MapReduce框架中最重要的操作之一。 MapTask和ReduceTask均会对数据按 照key进行排序。该操作属于 Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是 否需要。 默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

  • 对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使 用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数 据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。

  • 对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大 小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到 一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者 数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完 毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。(为了提升reduce方法对数据使用的效率)

  • 排序分类
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4bEMFFj-1658641856529)(D:\documents\notes\md\images\hadoop\image-20220620123852034.png)]

  • 如果要使用bean 对象做为 key 传输,需要实现 WritableComparable 接口重写 compareTo 方法,就可 以实现排序,否则会报错。

  • 自定义Bean实现WritableComparable接口用于作为key传输案例:手机号总流量倒序排序(全排序)
    FlowBean类

    package com.F.mapreduce.writeableComparable;
    
    import org.apache.hadoop.io.Writable;
    import org.apache.hadoop.io.WritableComparable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    /**
     * 1. 定义类实现Writable接口
     * 2. 重写序列化和反序列化方法
     * 3. 重写空参构造
     * 4. 重写toString方法
     */
    public class FlowBean implements WritableComparable<FlowBean> {
    
        private long upFlow;//上行流量
    
        public long getUpFlow() {
            return upFlow;
        }
    
        public void setUpFlow(long upFlow) {
            this.upFlow = upFlow;
        }
    
        public long getDownFlow() {
            return downFlow;
        }
    
        public void setDownFlow(long downFlow) {
            this.downFlow = downFlow;
        }
    
        public long getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow() {
            this.sumFlow = this.upFlow + this.downFlow;
        }
    
        private long downFlow; //下行流量
        private long sumFlow; //总流量
    
    
        // 空参构造
        public FlowBean() {
    
        }
    
        @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 {
            this.upFlow = in.readLong();
            this.downFlow = in.readLong();
            this.sumFlow = in.readLong();
        }
    
        @Override
        public String toString() {
            return upFlow + "\t" + downFlow + "\t" + sumFlow;
        }
    
        @Override
        public int compareTo(FlowBean o) {
            // 按照手机号总流量的倒序排序
            if (this.sumFlow > o.sumFlow) {
                return -1;
            } else if (this.sumFlow < o.sumFlow) {
                return 1;
            } else {
                
                return 0;
            }
        }
    }
    
    

    Mapper类

    package com.F.mapreduce.writeableComparable;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.IOException;
    
    public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
    
        private FlowBean outK = new FlowBean();
        private Text outV = new Text();
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 获取一行
            String line = value.toString();
            // 切割
            String[] split = line.split("\t");
            // 封装
            outV.set(split[0]);
            outK.setUpFlow(Long.parseLong(split[1]));
            outK.setDownFlow(Long.parseLong(split[2]));
            outK.setSumFlow();
            // 写出
            context.write(outK, outV);
        }
    
    }
    
    

    Reducer类

    package com.F.mapreduce.writeableComparable;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
    
        private FlowBean outV = new FlowBean();
    
        @Override
        protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
            for (Text value : values) {
                context.write(value, key);
            }
        }
    }
    
    

    Driver 类

    package com.F.mapreduce.writeableComparable;
    
    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 FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job
            Configuration conf = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar
            job.setJarByClass(FlowDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            // 4 设置mapper输出的kv类型
            job.setMapOutputKeyClass(FlowBean.class);
            job.setMapOutputValueClass(Text.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            // 6 设置数据的输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output3"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output7"));
    
            // 7 提交job
            job.waitForCompletion(true);
        }
    }
    
    
  • 自定义Bean实现WritableComparable接口用于作为key传输案例:按手机号总流量倒序排序,若总流量相同,按上行流量升序排序(二次排序)
    只需要对上面的FlowBean类的compareTo方法进行修改

    package com.F.mapreduce.writeableComparable;
    
    import org.apache.hadoop.io.Writable;
    import org.apache.hadoop.io.WritableComparable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    /**
     * 1. 定义类实现Writable接口
     * 2. 重写序列化和反序列化方法
     * 3. 重写空参构造
     * 4. 重写toString方法
     */
    public class FlowBean implements WritableComparable<FlowBean> {
    
        private long upFlow;//上行流量
    
        public long getUpFlow() {
            return upFlow;
        }
    
        public void setUpFlow(long upFlow) {
            this.upFlow = upFlow;
        }
    
        public long getDownFlow() {
            return downFlow;
        }
    
        public void setDownFlow(long downFlow) {
            this.downFlow = downFlow;
        }
    
        public long getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow() {
            this.sumFlow = this.upFlow + this.downFlow;
        }
    
        private long downFlow; //下行流量
        private long sumFlow; //总流量
    
    
        // 空参构造
        public FlowBean() {
    
        }
    
        @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 {
            this.upFlow = in.readLong();
            this.downFlow = in.readLong();
            this.sumFlow = in.readLong();
        }
    
        @Override
        public String toString() {
            return upFlow + "\t" + downFlow + "\t" + sumFlow;
        }
    
        @Override
        public int compareTo(FlowBean o) {
            // 按照手机号总流量的倒序排序
            if (this.sumFlow > o.sumFlow) {
                return -1;
            } else if (this.sumFlow < o.sumFlow) {
                return 1;
            } else {
                // 按照上行流量正序排序
                if (this.upFlow > o.upFlow)
                    return 1;
                else if (this.upFlow < o.upFlow)
                    return -1;
                else 
                    return 0;
            }
        }
    }
    
    
  • 自定义Bean实现WritableComparable接口用于作为key传输案例:按手机号前缀分成多个文件,文件按手机号总流量倒序排序,若总流量相同,按上行流量升序排序(二次排序)(区内排序)
    需要自定义一个Partitioner类

    package com.F.mapreduce.partitionAndWritableComparable;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> {
    
        @Override
        public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
    
            String phone = text.toString();
            String prePhone = phone.substring(0, 3);
            int partition;
            if ("136".equals(prePhone)) {
                partition = 0;
            } else if ("137".equals(prePhone)) {
                partition = 1;
            } else if ("138".equals(prePhone)) {
                partition = 2;
            } else if ("139".equals(prePhone)) {
                partition = 3;
            } else {
                partition = 4;
            }
            return partition;
        }
    }
    
    

    Bean类、Mapper类、Reducer类同上
    在Driver类中设置自定义的Partitioner类,同时设置ReduceTask个数

    package com.F.mapreduce.partitionAndWritableComparable;
    
    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 FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job
            Configuration conf = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar
            job.setJarByClass(FlowDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            // 4 设置mapper输出的kv类型
            job.setMapOutputKeyClass(FlowBean.class);
            job.setMapOutputValueClass(Text.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            job.setPartitionerClass(ProvincePartitioner2.class);
            job.setNumReduceTasks(5);
    
            // 6 设置数据的输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output3"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output7"));
    
            // 7 提交job
            job.waitForCompletion(true);
        }
    }
    
    

6.3.6 Combiner

  • 环形缓冲区在溢写数据到磁盘前时,有一个可选的Container步骤,对分区内的数据进行一个合并计算;在对所有的溢写文件进行归并排序后,得到的文件也可以选择Combiner流程进行合并计算。

  • 简介:
    1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
    2)Combiner组件的父类就是Reducer。
    3)Combiner和Reducer的区别在于运行的位置,Combiner是在每一个MapTask所在的节点运行;Reducer是接收全局所有Mapper的输出结果;
    4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
    5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv 应该跟Reducer的输入kv类型要对应起来。

  • 自定义Combiner案例:对WordCount案例中每一个MapTask的输出进行局部汇总
    需要增加自定义一个Combiner,Reducer、Mapper不变

    package com.F.mapreduce.combiner;
    
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    
        private IntWritable outV = new IntWritable();
    
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable value : values) {
                sum +=value.get();
            }
            outV.set(sum);
            context.write(key, outV);
        }
    }
    
    

    在Driver类中使用自定义的Combiner

    package com.F.mapreduce.combiner;
    
    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;
    
    public class WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            job.setCombinerClass(WordCountCombiner.class);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\words.txt"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output1"));
    
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    
  • 发现其实Combiner完全一模一样,所以其实可以不用多写一个Combiner类,直接在Driver类设置CombinerClass为Reducer类即可

    job.setCombinerClass(WordReducer.class)
    

6.3.7 OutputFormat数据输出

  • Reducer输出的内容并不直接输出到文件,而是要交友OutputFormat进行输出(默认是TextOutputFormat),OutPutFormat中有一个RecordWriter进行数据写出。

  • OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat 接口。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KmW2ReB7-1658641856530)(D:\documents\notes\md\images\hadoop\image-20220623210925909.png)]

  • 自定义OutputFormat,需要自定义一个类继承FileOutputFormat,同时改写RecordWriter,具体改写输出数据的方法write()

  • 自定义OutputFormat案例,过滤输入的log日志,得到两个日志文件
    定义LogMapper类

    package com.F.mapreduce.outputformat;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.IOException;
    
    public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 不做任何处理,直接把value作为输出的key输出,输出的value为空
            context.write(value, NullWritable.get());
        }
    }
    
    

    定义LogReducer

    package com.F.mapreduce.outputformat;
    
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
        @Override
        protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
            // 有相同key的数据回进入同个reduce方法,不写for循环的话会导致丢失数据
            for (NullWritable value : values) {
               context.write(key, NullWritable.get()); 
            }
        }
    }
    
    

    定义LogRecordWriter,继承RecordWriter,泛型要跟Reducer输出的泛型一样,实现getRecordWriter方法 ,实现write和close方法

    package com.F.mapreduce.outputformat;
    
    import org.apache.hadoop.fs.FSDataOutputStream;
    import org.apache.hadoop.fs.FileSystem;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IOUtils;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.RecordWriter;
    import org.apache.hadoop.mapreduce.TaskAttemptContext;
    
    import java.io.IOException;
    
    public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    
        private FSDataOutputStream otherOut;
        private FSDataOutputStream atguiguOut;
    
        public LogRecordWriter(TaskAttemptContext job) {
            // 创建两条流
            try {
                FileSystem fs = FileSystem.get(job.getConfiguration());
                atguiguOut = fs.create(new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\atguigu.log"));
                otherOut = fs.create(new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\other.log"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void write(Text key, NullWritable value) throws IOException, InterruptedException {
            String log = key.toString();
            // 具体写
            if (log.contains("atguigu")) {
                atguiguOut.writeBytes(log);
            } else {
                otherOut.writeBytes(log);
            }
        }
    
        @Override
        public void close(TaskAttemptContext context) throws IOException, InterruptedException {
            // 关流
            IOUtils.closeStream(atguiguOut);
            IOUtils.closeStream(otherOut);
        }
    }
    
    

    定义LogOutputFormat,继承FileOutputFormat,泛型要跟Reducer输出的泛型一样,实现getRecordWriter方法,返回自定的RecordWriter

    package com.F.mapreduce.outputformat;
    
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.RecordWriter;
    import org.apache.hadoop.mapreduce.TaskAttemptContext;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
    
        @Override
        public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
            LogRecordWriter lrw = new LogRecordWriter(job);
            return lrw;
        }
    }
    
    

    定义LogDriver类

    package com.F.mapreduce.outputformat;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class LogDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException, IOException {
                Configuration conf = new Configuration();
                Job job = Job.getInstance(conf);
    
                job.setJarByClass(LogDriver.class);
                job.setMapperClass(LogMapper.class);
                job.setReducerClass(LogReducer.class);
    
                job.setMapOutputKeyClass(Text.class);
                job.setMapOutputValueClass(NullWritable.class);
    
                job.setOutputKeyClass(Text.class);
                job.setOutputValueClass(NullWritable.class);
    
                //设置自定义的 outputformat
                job.setOutputFormatClass(LogOutputFormat.class);
                FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\log.txt"));
                // 虽 然 我 们 自 定 义 了 outputformat , 但 是 因 为 我 们 的 outputformat 继承自fileoutputformat
                //而 fileoutputformat 要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录
                FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output11"));
                boolean b = job.waitForCompletion(true);
                System.exit(b ? 0 : 1);
        }
    }
    
    

6.3.8 ReduceTask并行度决定机制

  • ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并 发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置:

    // 默认值是 1,手动设置为 4
    job.setNumReduceTasks(4);
    
  • ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。

  • ReduceTask默认值就是1,所以输出文件个数为一个。

  • 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜(reduce函数的输入数据是根据key进行划分的,可能会有些数据过多,有些数据过少)

  • ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全 局汇总结果,就只能有1个ReduceTask。

  • 具体多少个ReduceTask,需要根据集群性能而定。

  • 如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过 程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1 肯定不执行。

6.3.9 MapTask源码

6.3.10 ReduceTask源码

6.3.11 Join应用案例

6.3.11.1 Reduce Join应用案例
  • 利用hadoop来实现不同文件或者表之间的join操作

  • Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记 录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。 Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要 在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进 行合并就 ok 了。

  • 案例需求:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mS6JuREd-1658641856530)(D:\documents\notes\md\images\hadoop\image-20220707170125900.png)]

  • 自定义Bean对象TableBean

    package com.F.mapreduce.reduceJoin;
    
    import org.apache.hadoop.io.Writable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    public class TableBean implements Writable {
    
        private String id; //订单id
        private String pid; //商品id
        private int amount; //商品数量
        private String pname; //商品名称
        private String flag; //标记是什么表order、pd
    
        //空参构造
        public TableBean(){
        }
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getPid() {
            return pid;
        }
    
        public void setPid(String pid) {
            this.pid = pid;
        }
    
        public int getAmount() {
            return amount;
        }
    
        public void setAmount(int amount) {
            this.amount = amount;
        }
    
        public String getPname() {
            return pname;
        }
    
        public void setPname(String pname) {
            this.pname = pname;
        }
    
        public String getFlag() {
            return flag;
        }
    
        public void setFlag(String flag) {
            this.flag = flag;
        }
    
        @Override
        public void write(DataOutput out) throws IOException {
            out.writeUTF(id);
            out.writeUTF(pid);
            out.writeInt(amount);
            out.writeUTF(pname);
            out.writeUTF(flag);
            
        }
    
        @Override
        public void readFields(DataInput in) throws IOException {
            this.id = in.readUTF();
            this.pid = in.readUTF();
            this.amount = in.readInt();
            this.pname = in.readUTF();
            this.flag = in.readUTF();
        }
    
        @Override ng toString() {
            return id + "\t" + pname + "\t" + amount;
        }
    }
    
    

    新建Mapper类,通过初始化函数setup获取文件名称,不同的文件是不同的表,对不同的表进行不同的操作。

    package com.F.mapreduce.reduceJoin;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    import org.apache.hadoop.mapreduce.lib.input.FileSplit;
    
    import java.io.IOException;
    
    public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
    
        private String fileName;
        private Text outK = new Text();
        private TableBean outV = new TableBean();
    
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            // 初始化, 通过切片获取文件名称
            FileSplit split = (FileSplit) context.getInputSplit();
            fileName = split.getPath().getName();
        }
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 1.获取一行
            String line = value.toString();
            // 2.判断是哪个文件的
            if (fileName.contains("order")) { //处理的是订单表
                String[] split = line.split("\t");
    
                // 封装kv
                outK.set(split[1]);
                outV.setId(split[0]);
                outV.setPid(split[1]);
                outV.setAmount(Integer.parseInt(split[2]));
                outV.setPname("");
                outV.setFlag("order");
    
            } else { // 处理的是商品表
                String[] split = line.split("\t");
    
                outK.set(split[0]);
                outV.setId("");
                outV.setPid(split[0]);
                outV.setAmount(0);
                outV.setPname(split[1]);
                outV.setFlag("pd");
            }
            context.write(outK, outV);
        }
    }
    
    

    新建Reducer类

    package com.F.mapreduce.reduceJoin;
    
    import org.apache.commons.beanutils.BeanUtils;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.util.ArrayList;
    
    public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
        @Override
        protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
            // 准备初始化集合
            ArrayList<TableBean> orderBeans = new ArrayList<>();
            TableBean pdBean = new TableBean();
    
            for (TableBean value : values) {
                if ("order".equals(value.getFlag())) {
                    // 由于这里获得的value是一个对象的地址,所以不能直接添加到集合中去,这里需要new一个对象来存储值
                    TableBean tempTableBean = new TableBean();
                    try {
                        BeanUtils.copyProperties(tempTableBean, value);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    orderBeans.add(tempTableBean);
                } else {
                    try {
                        BeanUtils.copyProperties(pdBean, value);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            } 
            // 循环遍历orderBeans,赋值pdname
            for (TableBean orderBean : orderBeans) {
                orderBean.setPname(pdBean.getPname());
                context.write(orderBean, NullWritable.get());
            }
        }
    }
    
    

    Driver类

    package com.F.mapreduce.reduceJoin;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class TableDriver {
        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);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(TableBean.class);
            job.setOutputKeyClass(TableBean.class);
            job.setOutputValueClass(NullWritable.class);
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\inputtable"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output12"));
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
    
        }
    }
    
    
  • 在Reduce阶段进行Join操作Reduce 端的处理压力太大,Map 节点的运算负载则很低,资源利用率不高,且在 Reduce 阶段极易产生数据倾斜。解决方案:Map 端实现数据合并。

6.3.11.2 Map Join应用案例
  • 在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数 据的压力,尽可能的减少数据倾斜。

  • Map Join 适用于一张表十分小、一张表很大的场景。

  • 具体办法:采用 DistributedCache (1)在 Mapper 的 setup 阶段,将文件读取到缓存集合中。 (2)在 Driver 驱动类中加载缓存。

  • 对6.3.11.1案例进行改进
    Driver类修改,在Driver类中加载缓存文件,设置ReduceTask为0

    package com.F.mapreduce.mapJoin;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    import java.net.URI;
    import java.net.URISyntaxException;
    
    public class MapJoinDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException {
            // 1 获取 job 信息
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            // 2 设置加载 jar 包路径
            job.setJarByClass(MapJoinDriver.class);
            // 3 关联 mapper
            job.setMapperClass(MapJoinMapper.class);
            // 4 设置 Map 输出 KV 类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(NullWritable.class);
            // 5 设置最终输出 KV 类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(NullWritable.class);
            // 加载缓存数据
            job.addCacheFile(new URI("file:///D:/documents/javaProjects/MapReduceDemo/src/main/inputtable/pd.txt"));
            // Map 端 Join 的逻辑不需要 Reduce 阶段,设置 reduceTask 数量为 0
            job.setNumReduceTasks(0);
            // 6 设置输入输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\inputtable2"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output13"));
            // 7 提交
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
        }
    }
    
    

    Mapper修改

    package com.F.mapreduce.mapJoin;
    
    import org.apache.commons.lang.StringUtils;
    import org.apache.hadoop.fs.FSDataInputStream;
    import org.apache.hadoop.fs.FileSystem;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IOUtils;
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.URI;
    import java.util.HashMap;
    
    public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    
        private HashMap<String, String> pdMap = new HashMap<String, String>();
        private Text outK = new Text();
    
        @Override
        protected void setup(Mapper<LongWritable, Text, Text, Context context) throws IOException, InterruptedException {
            // 获取缓存的文件,并把文件内容封装到集合pd.txt
            URI[] cacheFiles = context.getCacheFiles();
            FileSystem fs = FileSystem.get(context.getConfiguration());
            FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
    
            // 从流中读取数据
            BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
            String line;
            while (StringUtils.isNotEmpty(line = reader.readLine())) {
                String[] fileds = line.split("\t");
    
                pdMap.put(fileds[0], fileds[1]);
            }
    
            // 关流
            IOUtils.closeStream(reader);
        }
    
        @Override
        protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, Context context) throws IOException, InterruptedException {
            // 处理order.txt
            String line = value.toString();
            String[] fields = line.split("\t");
    
            // 获取pid
            String pname = pdMap.get(fields[1]);
    
            // 获取订单id和订单数量
            // 封装
            outK.set(fields[0] + "\t" + pname + "\t" + fields[2]);
            context.write(outK, NullWritable.get());
        }
    }
    
    

    不需要Reducer

6.3.12 数据清洗ETL

  • “ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取 (Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL 一词较常用在数据仓 库,但其对象并不限于数据仓库。在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户 要求的数据。清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序。

  • hadoop可以用来进行ETL

  • 案例需求:去除日志中字段个数小于等于 11 的日志。

  • Mapper

    package com.F.mapreduce.etl;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.IOException;
    
    public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 1 获取一行
            String line = value.toString();
            // 3 ETL
            boolean result = parseLog(line, context);
    
            if (!result) {
                return;
            }
    //         4 写出
            context.write(value, NullWritable.get());
        }
    
        private boolean parseLog(String line, Context context) {
            // 切割
            String[] fields = line.split(" ");
            
            // 判断日志的长度是否大于11
            if (fields.length > 11)
                return true;
            return false;
        }
    
    }
    
    

    Driver

    package com.F.mapreduce.etl;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class WebLogDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
            // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
            args = new String[] { "D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\inputlog", "D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output14" };
            // 1 获取 job 信息
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            // 2 加载 jar 包
            job.setJarByClass(WebLogDriver.class);
            // 3 关联 map
            job.setMapperClass(WebLogMapper.class);
            // 4 设置最终输出类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(NullWritable.class);
            // 设置 reducetask 个数为 0
            job.setNumReduceTasks(0);
            // 5 设置输入和输出路径
            FileInputFormat.setInputPaths(job, new Path(args[0]));
            FileOutputFormat.setOutputPath(job, new Path(args[1]));
            // 6 提交
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
    
        }
    }
    
    

6.4 数据压缩

  • 压缩的优点:以减少磁盘 IO、减少磁盘存储空间。 压缩的缺点:增加 CPU 开销。

  • 压缩原则(1)运算密集型的 Job,少用压缩 (2)IO 密集型的 Job,多用压缩

  • 压缩算法对比

    压缩格式Hadoop 自带?算法文件扩展 名是否可 切片换成压缩格式后,原来的 程序是否需要修改
    DEFLATE是,直接使用DEFLATE.deflate和文本处理一样,不需要 修改
    Gzip是,直接使用DEFLATE.gz和文本处理一样,不需要 修改
    bzip2是,直接使用bzip2.bz2和文本处理一样,不需要 修改
    LZO否,需要安装LZO.lzo需要建索引,还需要指定输入格式
    Snappy是,直接使用Snappy.snappy和文本处理一样,不需要 修改

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8GQTc4s-1658641856530)(D:\documents\notes\md\images\hadoop\image-20220708140349246.png)]

  • 压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否 可以支持切片。

  • 压缩算法优缺点:
    Gzip 压缩 优点:压缩率比较高; 缺点:不支持 Split;压缩/解压速度一般;
    Bzip2 压缩 优点:压缩率高;支持 Split; 缺点:压缩/解压速度慢。
    Lzo 压缩 优点:压缩/解压速度比较快;支持 Split; 缺点:压缩率一般;想支持切片需要额外创建索引。
    Snappy 压缩 优点:压缩和解压缩速度快; 缺点:不支持 Split;压缩率一般;

  • 压缩可以在 MapReduce 作用的任意阶段启用。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gBYvGsMv-1658641856530)(D:\documents\notes\md\images\hadoop\image-20220708141538021.png)]

  • 为了支持多种压缩/解压缩算法,Hadoop 引入了编码/解码器
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qFFZhN4z-1658641856531)(D:\documents\notes\md\images\hadoop\image-20220708151301735.png)]

  • 要在 Hadoop 中启用压缩,可以配置如下参数
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nM2fvoam-1658641856531)(D:\documents\notes\md\images\hadoop\image-20220708152052476.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zg0XL7fj-1658641856531)(D:\documents\notes\md\images\hadoop\image-20220708152122511.png)]

  • 压缩案例:wordcount案例开启map端输出压缩
    对WordCountDriver修改

    package com.F.mapreduce.compression;
    
    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.io.compress.BZip2Codec;
    import org.apache.hadoop.io.compress.CompressionCodec;
    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 WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 开启map端输出压缩
            configuration.setBoolean("mapreduce.map.output.compress", true);
    
            //设置map端输出压缩方式
            configuration.setClass("mapreduce.map.out.compress.codec", BZip2Codec.class, CompressionCodec.class);
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\words.txt"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output15"));
    
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    
  • 压缩案例:wordcount案例开启reduce端输出压缩
    对WordCountDriver进行修改

    package com.F.mapreduce.compression;
    
    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.io.compress.BZip2Codec;
    import org.apache.hadoop.io.compress.CompressionCodec;
    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 WordCountDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            // 1 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance();
    
            // 开启map端输出压缩
            configuration.setBoolean("mapreduce.map.output.compress", true);
    
            //设置map端输出压缩方式
            configuration.setClass("mapreduce.map.out.compress.codec", BZip2Codec.class, CompressionCodec.class);
    
            // 2 设置jar包路径
            job.setJarByClass(WordCountDriver.class);
    
            // 3 关联mapper和reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
    
            // 4 设置map输出的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            // 5 设置最终输出的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 6 设置输入路径和输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\words.txt"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\documents\\javaProjects\\MapReduceDemo\\src\\main\\output16"));
    
            // 设置 reduce 端输出压缩开启
            FileOutputFormat.setCompressOutput(job, true);
            // 设置压缩的方式
            FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
            //FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class); 
    	   //FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
    
    
            // 7 提交job
            boolean result = job.waitForCompletion(true);
            System.exit(result? 0:1);
        }
    }
    
    
  • hadoop3.x采用Snappy压缩算法需要系统是centos7.5,否则会报错

6.5 MapReduce生产调优

  • MapReduce 程序效率的瓶颈在于两点
    1)计算机性能 CPU、内存、磁盘、网络
    2)I/O 操作优化 (1)数据倾斜 (2)Map 运行时间太长,导致 Reduce 等待过久 (3)小文件过多
  • 可以调优的参数
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t2iN7iS0-1658641856531)(D:\documents\notes\md\images\hadoop\image-20220717145429326.png)]
    1. 自定义分区,减少数据倾斜; 定义类,继承Partitioner接口,重写getPartition方法。在map方法结束后到输入环形缓冲区之前,会通过getPartition方法对数据标记分区,由于key相同的数据会进入到同一个reduce方法,那么如果某个key对应的数据过多的话,某个reduce方法处理的数据就会过多,造成数据倾斜,所以在getPartition方法可以通过对key进行处理,将数据分散来达到解决数据倾斜的问题。
    2. 通过增大环形缓冲区的大小和增加环形缓冲区溢写的阈值,减少环形缓冲区对外溢写的次数,mapreduce.task.io.sort.mb Shuffle的环形缓冲区大小,默认100m,可以提高到200m mapreduce.map.sort.spill.percent 环形缓冲区溢出的阈值,默认80% ,可以提高到90%。
    3. 增加归并排序的溢写文件个数,缓冲区写出溢写文件到磁盘后,会对溢写文件进行归并排序,默认是10个,内存足够的情况下,消耗更多内存提高归并排序的溢写文件个数,能够提高速度。
    4. 不影响业务的前提下采用Combier。
    5. 采用Snappy或LZO压缩map输出到磁盘的数据,这样reduce在拉取数时能够减少磁盘IO。
    6. 可以提高MapTask的内存上限,可以根据128m数据对应1G内存原则提高该内存。
    7. 提高MapTask堆内存大小,跟第6保持一致。
    8. 增加MapTask的CPU核数,mapreduce.map.cpu.vcores 默认MapTask的CPU核数1。计算密集型任 务可以增加CPU核数
    9. 增加异常重试次数,每个mapTask默认会有4次异常重试次数,根据机器 性能适当提高。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nnh1B250-1658641856531)(D:\documents\notes\md\images\hadoop\image-20220717154032202.png)]
    10. 提高reduce拉取数据的并行数。
    11. reduce拉取数据后会先放到内存,内存不够用的话会溢出到磁盘,可以提高reduce可用内存的比例。
    12. reduce从map拉取数据后会进行归并排序,这个操作也是在内存进行,可以提高进行归并排序占用的最大内存比例。
    13. 提高reduceTask内存,可以根据128m数据对应1G内存原则,适当提高内存到4-6G。
    14. 提高reduceTask的堆内存大小,跟13设置保持一致。
    15. 提高reduceTask的CPU核数。
    16. 提高reduceTask最大重试次数。
    17. 设置当mapTask完成多少比例时为reduceTask申请资源。
    18. 调整task超时时间
    19. 可以不用reduce就不用
  • 减少数据倾斜的方法:
    (1)首先检查是否空值过多造成的数据倾斜。生产环境,可以直接过滤掉空值;如果想保留空值,就自定义分区,将空值加随机数打 散。最后再二次聚合。
    (2)能在 map 阶段提前处理,最好先在 Map 阶段处理。如:Combiner、MapJoin
    (3)设置多个 reduce 个数

6.6 MapReduce计算性能测试

  • (1)使用 RandomWriter 来产生随机数,每个节点运行 10 个 Map 任务,每个 Map 产 生大约 1G 大小的二进制随机数,10台服务器差不多1分钟执行完,属于正常

    [atguigu@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar randomwriter random-data
    

    (2)执行 Sort 程序

    [atguigu@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar sort random-data sorted-dat
    

    (3)验证数据是否真正排好序了

    [atguigu@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar testmapredsort -sortInput random-data -sortOutput sorted-data
    

7 Yarn

7.1 Yarn的基础架构

  • Yarn 是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式 的操作系统平台,而 MapReduce 等运算程序则相当于运行于操作系统之上的应用程序。
  • YARN 主要由 ResourceManager、NodeManager、ApplicationMaster 和 Container 等组件 构成。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VuvCRtIA-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220708174842997.png)]

7.2 Yarn的工作机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oq9Pbrmb-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220708175255050.png)]

  1. MR 程序提交到客户端所在的节点。
  2. YarnRunner 向 ResourceManager 申请一个 Application。
  3. RM 将该应用程序的资源路径返回给 YarnRunner。
  4. 该程序将运行所需资源(切片,xml配置、jar包)提交到 HDFS 上。
  5. 程序资源提交完毕后,申请运行 mrAppMaster。
  6. RM 将用户的请求初始化成一个 Task。这些请求会放到一个任务队列中。
  7. 其中一个 NodeManager 领取到 Task 任务。
  8. 该 NodeManager 创建容器 Container(有cpu、内存、磁盘等资源),并产生 MRAppmaster。
  9. Container 从 HDFS 上拷贝资源到本地。
  10. MRAppmaster 向 RM 申请运行 MapTask 资源。
  11. 假设有两个切片,RM 将运行 MapTask 任务分配给另外两个 NodeManager,另两个 NodeManager 分 别领取任务并创建容器。(两个容器也有可能创建在一个NodeManager上)
  12. MR 向两个接收到任务的 NodeManager 发送程序启动脚本,这两个 NodeManager 分别启动 MapTask,MapTask 对数据分区排序。
  13. MrAppMaster 等待所有 MapTask 运行完毕后,向 RM 申请容器,运行 ReduceTask。
  14. ReduceTask 向 MapTask 获取相应分区的数据。
  15. 程序运行完毕后,MR 会向 RM 申请注销自己并释放之前所申请的资源。

7.3 作业提交全流程

  • 两个图结合来看,第一个图是YARN和MapReduce之间的协调关系,第二个图是HDFS和MapReduce之间的协调关系
  • 作业提交全过程详解
    (1)作业提交
    第 1 步:Client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业。
    第 2 步:Client 向 RM 申请一个作业 id。
    第 3 步:RM 给 Client 返回该 job 资源的提交路径和作业 id。
    第 4 步:Client 提交 jar 包、切片信息和配置文件到指定的资源提交路径。
    第 5 步:Client 提交完资源后,向 RM 申请运行 MrAppMaster。
    (2)作业初始化
    第 6 步:当 RM 收到 Client 的请求后,将该 job 添加到容量调度器中。
    第 7 步:某一个空闲的 NM 领取到该 Job。
    第 8 步:该 NM 创建 Container,并产生 MRAppmaster。
    第 9 步:下载 Client 提交的资源到本地。
    (3)任务分配
    第 10 步:MrAppMaster 向 RM 申请运行多个 MapTask 任务资源。
    第 11 步:RM 将运行 MapTask 任务分配给另外两个 NodeManager,另两个 NodeManager 分别领取任务并创建容器。
    (4)任务运行
    第 12 步:MR 向两个接收到任务的 NodeManager 发送程序启动脚本,这两个 NodeManager 分别启动 MapTask,MapTask 对数据分区排序。
    第13步:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
    第 14 步:ReduceTask 向 MapTask 获取相应分区的数据。
    第 15 步:程序运行完毕后,MR 会向 RM 申请注销自己。
    (5)进度和状态更新
    YARN 中的任务将其进度和状态(包括 counter)返回给应用管理器, 客户端每秒(通过 mapreduce.client.progressmonitor.pollinterval 设置)向应用管理器请求进度更新, 展示给用户。
    (6)作业完成
    除了向应用管理器请求作业进度外, 客户端每 5 秒都会通过调用 waitForCompletion()来 检查作业是否完成。时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业 完成之后, 应用管理器和 Container 会清理工作状态。作业的信息会被作业历史服务器存储 以备之后用户核查。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qHC51ceB-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220710111804149.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yZy7UHWQ-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220710111816089.png)]

7.4 调度器和调度算法

  • ResourceManager如果收到多个任务的话,会将多个任务放到一个任务队列中,由调度器进行管理,管理哪个任务先执行已经每个任务所需分配的资源
  • 目前,Hadoop 作业调度器主要有三种:FIFO、容量(Capacity Scheduler)和公平(Fair Scheduler)。Apache Hadoop3.1.3 默认的资源调度器是 Capacity Scheduler。CDH框架默认调度器是Fair Scheduler

7.4.1 FIFO

  • FIFO 调度器(First In First Out):单队列,根据提交作业的先后顺序,先来先服务。大数据场景下基本不会用,因为不支持任务的并发。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MZL2ONoo-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220710113539257.png)]

7.4.2 容量调度器

  • Capacity Scheduler 是 Yahoo 开发的多用户调度器。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zpdm3TNM-1658641856532)(D:\documents\notes\md\images\hadoop\image-20220710113753147.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvHaolSx-1658641856533)(D:\documents\notes\md\images\hadoop\image-20220710114443130.png)]

7.4.3 公平调度器

  • Fair Schedulere 是 Facebook 开发的多用户调度器。

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYHPnDZ4-1658641856533)(D:\documents\notes\md\images\hadoop\image-20220710114925579.png)]

  • 任务队列中的任务获得公平的资源,但是当某一时刻突然增加了一个任务后,需要一定时间从已有任务中取回部分资源来分配给新任务,这个所需取回的资源就叫做缺额,任务调度器会优先为缺额大的作业分配任务
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QNCNWhYS-1658641856533)(D:\documents\notes\md\images\hadoop\image-20220710115937795.png)]

  • 公平调度器队列资源分配方式
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qcIwnOU8-1658641856533)(D:\documents\notes\md\images\hadoop\image-20220710120442611.png)]

    队列资源公平分配策略主要分两步,第一步将资源平均分配给每个队列,若有队列获得的资源多于所需资源的话,则将多余的资源汇集起来平均分配给缺少资源的队列

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CthV187L-1658641856533)(D:\documents\notes\md\images\hadoop\image-20220710121042537.png)]

    作业资源公平分配策略有两种方式,一种是不加权方式,给每个作业平均分配资源,并把多余的资源收集回来再平均分配给缺少资源的作业,一直重复计算到没有空闲资源;
    另一种是加权方式,每个作业赋予一个权重,按照权重给每个作业分配资源,然后把多余的资源收集回来并按照缺少资源的作业的权重,给缺少资源的作业分配资源,重复计算到没有空闲资源。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5of9zKG-1658641856534)(D:\documents\notes\md\images\hadoop\image-20220710121054529.png)]

    DRF(Dominant Resource Fairness),我们之前说的资源,都是单一标准,例如只考虑内存(也是Yarn默 认的情况)。但是很多时候我们资源有很多种,例如内存,CPU,网络带宽等,这样我们很难衡量两个应用 应该分配的资源比例。 那么在YARN中,我们用DRF来决定如何调度: 假设集群一共有100 CPU和10T 内存,而应用A需要(2 CPU, 300GB),应用B需要(6 CPU,100GB)。 则两个应用分别需要A(2%CPU, 3%内存)和B(6%CPU, 1%内存)的资源,这就意味着A是内存主导的, B是 CPU主导的,针对这种情况,我们可以选择DRF策略对不同应用进行不同资源(CPU和内存)的一个不同比 例的限制。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DqmT0Hz-1658641856534)(D:\documents\notes\md\images\hadoop\image-20220710121106711.png)]

7.5 Yarn常用命令

# 列出所有application
yarn application -list
# 根据 Application 状态过滤 所有状态:ALL、NEW、NEW_SAVING、SUBMITTED、ACCEPTED、RUNNING、FINISHED、FAILED、KILLED
# 查看正在运行的任务
yarn application -list -appStates RUNNING
# kill 应用
yarn application -kill <application-ID>
# 查询Application日志
yarn logs -applicationId <application-ID>
# 查询 Container 日志
yarn logs -applicationId <ApplicationId> -containerId <ContainerId> 
# 列出所有 Application 尝试的列表
yarn applicationattempt -list <ApplicationId>
# 打印 ApplicationAttemp 状态
yarn applicationattempt -status <ApplicationAttemptId>
# 列出所有 Container
yarn container -list <ApplicationAttemptId>
# 打印 Container 状态
yarn container -status <ContainerId>
# 列出所有节点
yarn node -list -all
# 重新加载加载队列配置
yarn rmadmin -refreshQueues
# 打印队列信息
yarn queue -status <QueueName>

7.6Yarn在生产环境下配置参数

  • 需要根据生产环境对yarn参数进行相应配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dAusIIsP-1658641856534)(D:\documents\notes\md\images\hadoop\image-20220710160336672.png)]l

7.7 容量调度器在生产环境下配置

7.7.1 多队列配置

  • 容量调度器默认只有一个default队列,不能够满足 生产要求,配置多队列的要求以及好处:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8F5JIJb-1658641856534)(D:\documents\notes\md\images\hadoop\image-20220710171501395.png)]

    • 容量调度器多队列提交案例:需求 1:default 队列占总内存的 40%,最大资源容量占总资源 60%,hive 队列占总内存 的 60%,最大资源容量占总资源 80%。 需求 2:配置队列优先级
      在HADOOP_HOME/etc/hadoop/capacity-scheduler.xml做如下配置

      (1)修改如下配置
      <!-- 指定多队列,增加 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>
      (2)为新加队列添加必要属性:
      <!-- 指定 hive 队列的资源额定容量 -->
      <property>
       <name>yarn.scheduler.capacity.root.hive.capacity</name>
       <value>60</value>
      </property>
      <!-- 用户最多可以使用队列多少资源,取值范围为0到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</name>
       <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-application-lifetime</name>
       <value>-1</value> <!-- -1代表不做限制 -->
      </property>
      <!-- 如果 application 没指定超时时间,则用 default-application-lifetime 作为默认
      值 -->
      <property>
       <name>yarn.scheduler.capacity.root.hive.default-application-lifetime</name>
       <value>-1</value> <!-- -1代表不做限制 -->
      </property>
      

      分发配置文件到各个节点,重启 Yarn 或者执行 yarn rmadmin -refreshQueues 刷新队列,就可以看到两条队列

      向新建的hive队列提交wordcount任务

      # -D 表示运行时改变参数值
      hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount -D mapreduce.job.queuename=hive /input /output
      

      如果是在windows程序中向指定队列提交任务的话,则需要在Driver类中声明

      public class WcDrvier {
       public static void main(String[] args) throws IOException, 
      ClassNotFoundException, InterruptedException {
       Configuration conf = new Configuration();
       conf.set("mapreduce.job.queuename","hive");
       //1. 获取一个 Job 实例
       Job job = Job.getInstance(conf);
       。。。 。。。
       //6. 提交 Job
       boolean b = job.waitForCompletion(true);
       System.exit(b ? 0 : 1);
       }
      }
      

7.7.2 队列中任务优先级设置

  • 容量调度器,支持任务优先级的配置,在资源紧张时,优先级高的任务将优先获取资源。 默认情况,Yarn 将所有任务的优先级限制为 0,若想使用任务的优先级功能,须开放该限制。
  1. 修改 yarn-site.xml 文件,增加以下参数
<!-- 配置了五个任务等级 -->
<property>
 <name>yarn.cluster.max-application-priority</name>
 <value>5</value>
</property>
  1. 分发配置,并重启 Yarn

    xsync yarn-site.xml
    sbin/stop-yarn.sh
    sbin/start-yarn.sh
    
  2. 在提交任务的时候设置优先级

    # hadoop内置的计算圆周率任务,第一个5是运行5次map任务,第二个2000000是每个map任务投掷次数,所以总投掷次数是10000000。
    hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar pi -D mapreduce.job.priority=5 5 2000000
    
  3. 也可以通过以下命令修改正在执行的任务的优先级。

    yarn application -appID application_1611133087930_0009 -updatePriority 5
    

7.8公平调度器在生产环境下配置

  • 公平调度器大型公司常用,容量调度器中小型公司常用

  • 公平调度器案例:创建两个队列,分别是 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-queue-basics/

  • 配置流程
    1)修改 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>
    

​ 2)新建并配置 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 不存在,允许自动创建 -->
         <rule name="nestedUserQueue" create="true">
         	<rule name="primaryGroup" create="false"/>
		 </rule>
 		<!-- 最后一个规则必须为 reject 或者 default。Reject 表示拒绝创建提交失败,default 表示把任务提交到 default 队列 -->
		 <rule name="reject" />
 	</queuePlacementPolicy>
</allocations>

3)分发配置并重启 Yarn

xsync yarn-site.xml
xsync fair-scheduler.xml
sbin/stop-yarn.sh
sbin/start-yarn.sh

7.9 Yarn的Tool接口

  • 运行windows编写得来的jar包,后面跟的参数一般在Driver类都是固定好了,这样的话就没有办法动态传参,比如要指定运行jar包到某个队列,会因为参数过多导致读取参数错误

    # 原先, 后边跟着输入路径跟输出路径
    hadoop jar wc.jar com.F.mapreduce.wordcount2.WordCountDriver /input /output1
    # 指定队列时误将指定队列的参数以为是输入路径
    hadoop jar wc.jar com.F.mapreduce.wordcount2.WordCountDriver -D mapreduce.job.queuename=root.test /input /output1
    
  • 为了实现动态传参,需要编写Yarn的Tool接口

  • 步骤:(1)新建 Maven 项目 YarnDemo,pom 如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
     	<groupId>com.atguigu.hadoop</groupId>
     	<artifactId>yarn_tool_test</artifactId>
     	<version>1.0-SNAPSHOT</version>
     <dependencies>
     	<dependency>
             <groupId>org.apache.hadoop</groupId>
             <artifactId>hadoop-client</artifactId>
             <version>3.1.3</version>
     	</dependency>
     </dependencies>
    </project>
    

    (2)新建 com.atguigu.yarn 报名
    (3)创建类 WordCount 并实现 Tool 接口:

    package com.atguigu.yarn;
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.Mapper;
    import org.apache.hadoop.mapreduce.Reducer;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import 
    org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    import org.apache.hadoop.util.Tool;
    import java.io.IOException;
    public class WordCount implements Tool {
     private Configuration conf;
     @Override
     public int run(String[] args) throws Exception {
         Job job = Job.getInstance(conf);
         job.setJarByClass(WordCountDriver.class);
         job.setMapperClass(WordCountMapper.class);
         job.setReducerClass(WordCountReducer.class);
         job.setMapOutputKeyClass(Text.class);
         job.setMapOutputValueClass(IntWritable.class);
         job.setOutputKeyClass(Text.class);
         job.setOutputValueClass(IntWritable.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, IntWritable> {
         private Text outK = new Text();
         private IntWritable outV = new IntWritable(1);
         @Override
         protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
             String line = value.toString();
             String[] words = line.split(" ");
             for (String word : words) {
                 outK.set(word);
                 context.write(outK, outV);
         		}
         	}
         }
         public static class WordCountReducer extends Reducer<Text,IntWritable, Text, IntWritable> {
         private IntWritable outV = new IntWritable();
         @Override
         protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
         int sum = 0;
         for (IntWritable value : values) {
         	sum += value.get();
         }
         outV.set(sum);
         context.write(key, outV);
         }
     }
    }
    
    

    (4)新建 WordCountDriver

    package com.atguigu.yarn;
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.util.Tool;
    import org.apache.hadoop.util.ToolRunner;
    import java.util.Arrays;
    public class WordCountDriver {
     private static Tool tool;
     public static void main(String[] args) throws Exception {
         // 1. 创建配置文件
         Configuration conf = new Configuration();
     	// 2. 判断是否有 tool 接口
         switch (args[0]){
            case "wordcount":
                tool = new WordCount();
                break;
            default:
                throw new RuntimeException(" No such tool: "+ args[0] );
         }
         // 3. 用 Tool 执行程序
         // Arrays.copyOfRange 将老数组的元素放到新数组里面
         int run = ToolRunner.run(conf, tool, Arrays.copyOfRange(args, 1, args.length));
         System.exit(run);
     }
    }
    
    

    (5)打包成jar包上传,向集群提交jar包

    yarn jar YarnDemo.jar com.atguigu.yarn.WordCountDriver wordcount -Dmapreduce.job.queuename=root.test /input /output1
    

8 Hadoop综合调优

8.1 Hadoop小文件优化方法

  • HDFS 上每个文件都要在 NameNode 上创建对应的元数据,这个元数据的大小约为 150byte,这样当小文件比较多的时候,就会产生很多的元数据文件,一方面会大量占用 NameNode 的内存空间,另一方面就是元数据文件过多,使得寻址索引速度变慢。
    小文件过多,在进行 MR 计算时,会生成过多切片,需要启动过多的 MapTask。每个 MapTask 处理的数据量小,导致 MapTask 的处理时间比启动时间还小,白白消耗资源。

  • 解决方案:
    1)在数据采集的时候,就将小文件或小批数据合成大文件再上传 HDFS(数据源头)
    2)Hadoop Archive(存储方向) 是一个高效的将小文件放入 HDFS 块中的文件存档工具,能够将多个小文件打包成一 个 HAR 文件,从而达到减少 NameNode 的内存使用
    3)CombineTextInputFormat(计算方向) CombineTextInputFormat 用于将多个小文件在切片过程中生成一个单独的切片或者少 量的切片。
    4)开启 uber 模式,实现 JVM 重用(计算方向);默认情况下,每个 Task 任务都需要启动一个 JVM 来运行,如果 Task 任务计算的数据 量很小,我们可以让同一个 Job 的多个 Task 运行在一个 JVM 中,不必为每个 Task 都开启 一个 JVM。开启uber模式的配置如下:

    # 开启 uber 模式,在 mapred-site.xml 中添加如下配置
    <!-- 开启 uber 模式,默认关闭 -->
    <property>
         <name>mapreduce.job.ubertask.enable</name>
         <value>true</value>
    </property>
    <!-- uber 模式中最大的 mapTask 数量,可向下修改 --> 
    <property>
         <name>mapreduce.job.ubertask.maxmaps</name>
         <value>9</value>
    </property>
    <!-- uber 模式中最大的 reduce 数量,可向下修改 -->
    <property>
         <name>mapreduce.job.ubertask.maxreduces</name>
         <value>1</value>
    </property>
    <!-- uber 模式中最大的输入数据量,默认使用 dfs.blocksize 的值,可向下修
    改 -->
    <property>
         <name>mapreduce.job.ubertask.maxbytes</name>
         <value></value>
    </property>
    

9 Hadoop源码

9.1 RPC通信原理解析

  • Hadoop各个组件在底层是通过RPC来进行通信的

  • 要模拟RPC的客户端、服务端、通信协议三者工作流程,需要提前定义好接口协议,然后服务端去实现接口协议,重写抽象方法,并创建RPC服务(包括服务器地址、端口号、通信协议);客户端获取服务器代理(包括服务器地址、端口号、通信协议),客户端使用服务器代理去调用服务端提供的方法。

  • 具体步骤:
    新建包,创建RPCProtocol接口,定义版本号和抽象方法

    package com.F.mapreduce.rpc;
    
    public interface RPCProtocol {
        long versionID = 666;
        void mkdirs(String path);
    }
    

    新建服务端类实现RPCProtocol接口,实现抽象方法,并启动服务

    package com.F.mapreduce.rpc;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.ipc.RPC;
    
    import java.io.IOException;
    
    public class NNServer implements RPCProtocol{
    
        public static void main(String[] args) throws IOException {
            // 启动服务
            RPC.Server server = new RPC.Builder(new Configuration())
                    .setBindAddress("localhost")
                    .setPort(8888)
                    .setProtocol(RPCProtocol.class)
                    .setInstance(new NNServer())
                    .build();
            System.out.println("服务器开始工作");
            server.start();
        }
    
        @Override
        public void mkdirs(String path) {
            System.out.println("服务端接受到了客户端请求" + path);
        }
    }
    
    

    新建客户端类,获取服务端代理对象,调用服务端提供的方法

    package com.F.mapreduce.rpc;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.ipc.RPC;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    
    public class HDFSClient {
        public static void main(String[] args) throws IOException {
            // 获取客户端对象
            RPCProtocol client = RPC.getProxy(RPCProtocol.class, RPCProtocol.versionID, new InetSocketAddress("localhost", 8888), new Configuration());
            System.out.println("客户端开始工作");
            client.mkdirs("/input");
        }
    }
    

    先打开服务端,在打开客户端,服务端就会接受到相应请求

9.2 NameNode启动源码解析

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-53EbPRJN-1658641856534)(D:\documents\notes\md\images\hadoop\image-20220718101224983.png)]

  • 需要在pom.xml中添加以下依赖才能看见以上源码

    <dependencies>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>3.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs</artifactId>
        <version>3.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs-client</artifactId>
        <version>3.1.3</version>
        <scope>provided</scope>
    </dependency>
    </dependencies>
    

9.3 DataNode启动源码解析

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hmlYSyK-1658641856535)(D:\documents\notes\md\images\hadoop\image-20220718222255140.png)]

9.4 HDFS上传源码解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyQIJrXu-1658641856535)(D:\documents\notes\md\images\hadoop\image-20220718223334587.png)]

9.5 Yarn源码解析

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w4WtDrBs-1658641856535)(D:\documents\notes\md\images\hadoop\image-20220718223420802.png)]

9.6 MapReduce源码解析

9.7 Hadoop源码编译

10 Hadoop HA 高可用

10.1 HA概述

  • (1)所谓 HA(High Availablity),即高可用(7*24 小时不中断服务)。
    (2)实现高可用最关键的策略是消除单点故障(唯一的NameNode或ResourceManager所在节点挂掉了整个集群就瘫痪了)。HA 严格来说应该分成各个组件的 HA 机制:HDFS 的 HA 和 YARN 的 HA。
    (3)NameNode 主要在以下两个方面影响 HDFS 集群
    NameNode 机器发生意外,如宕机,集群将无法使用,直到管理员重启;NameNode 机器需要升级,包括软件、硬件升级,此时集群也将无法使用。
    HDFS HA 功能通过配置多个 NameNodes(Active/Standby)实现在集群中对 NameNode 的 热备来解决上述问题。如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种方 式将 NameNode 很快的切换到另外一台机器。

10.2 HDFS-HA集群搭建

  • 集群规划
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4tjE6RJ-1658641856535)(D:\documents\notes\md\images\hadoop\image-20220724100435494.png)]

  • HDFS-HA核心问题
    (1)怎么保证三台 namenode 的数据一致
    a.Fsimage:让一台 nn 生成数据,让其他机器 nn 同步
    b.Edits:需要引进新的模块 JournalNode 来保证 edtis 的文件的数据一致性
    (2)怎么让同时只有一台 nn 是 active,其他所有是 standby 的
    a.手动分配
    b.自动分配
    (3)2nn 在 ha 架构中并不存在,定期合并 fsimage 和 edtis 的活谁来干 由 standby 的 nn 来干
    (4)如果 nn 真的发生了问题,怎么让其他的 nn 上位干活
    a.手动故障转移
    b.自动故障转移

  • 配置并启动HDFS-HA集群
    (1)将/opt/module/下的 hadoop-3.1.3 拷贝到/opt/ha 目录下(记得删除 data 和 log 目录,之前的数据不要)
    (2)配置 core-site.xml

    <configuration>
    	<!-- 把多个 NameNode 的地址组装成一个集群 mycluster,高可用集群不指定namenode的节点为某一个节点的地址 --> 
         <property>
         <name>fs.defaultFS</name>
         <value>hdfs://mycluster</value>
     </property>
    <!-- 指定 hadoop 运行时产生文件的存储目录 -->
     <property>
         <name>hadoop.tmp.dir</name>
         <value>/opt/ha/hadoop-3.1.3/data</value>
     </property>
    </configuration>
    

    (3)配置hdfs-site.xml,高可用集群不需要配置2nn,

    <configuration>
    <!-- NameNode 数据存储目录 -->
     <property>
         <name>dfs.namenode.name.dir</name>
         <value>file://${hadoop.tmp.dir}/name</value>
     </property>
    <!-- DataNode 数据存储目录 -->
     <property>
         <name>dfs.datanode.data.dir</name>
         <value>file://${hadoop.tmp.dir}/data</value>
     </property>
    <!-- JournalNode 数据存储目录,用于同步各个namenode上的edits文件 -->
     <property>
         <name>dfs.journalnode.edits.dir</name>
         <value>${hadoop.tmp.dir}/jn</value>
     </property>
    <!-- 完全分布式集群名称,之后连接集群就不需要指定连接哪一个节点,直接连接mycluster即可 -->
     <property>
         <name>dfs.nameservices</name>
         <value>mycluster</value>
     </property>
    <!-- 集群中 NameNode 节点都有哪些 -->
     <property>
         <name>dfs.ha.namenodes.mycluster</name>
         <value>nn1,nn2,nn3</value>
     </property>
    <!-- NameNode 的 RPC 通信地址,这里要跟dfs.ha.namenodes.mycluster配置的值相对应 -->
     <property>
         <name>dfs.namenode.rpc-address.mycluster.nn1</name>
         <value>hadoop102:8020</value>
     </property>
     <property>
         <name>dfs.namenode.rpc-address.mycluster.nn2</name>
         <value>hadoop103:8020</value>
     </property>
    <property>
         <name>dfs.namenode.rpc-address.mycluster.nn3</name>
         <value>hadoop104:8020</value>
     </property>
    <!-- NameNode 的 http 通信地址 -->
     <property>
         <name>dfs.namenode.http-address.mycluster.nn1</name>
         <value>hadoop102:9870</value>
     </property>
     <property>
         <name>dfs.namenode.http-address.mycluster.nn2</name>
         <value>hadoop103:9870</value>
     </property>
     <property>
         <name>dfs.namenode.http-address.mycluster.nn3</name>
         <value>hadoop104:9870</value>
     </property>
    <!-- 指定 NameNode 元数据在 JournalNode 上的存放位置 -->
     <property>
        <name>dfs.namenode.shared.edits.dir</name>
        <value>qjournal://hadoop102:8485;hadoop103:8485;hadoop104:8485/mycluster</value>
     </property>
    <!-- 访问代理类:client 用于确定哪个 NameNode 为 Active -->
     <property>
        <name>dfs.client.failover.proxy.provider.mycluster</name>
        <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
     </property>
    <!-- 配置隔离机制,即同一时刻只能有一台服务器对外响应 -->
     <property>
         <name>dfs.ha.fencing.methods</name>
         <value>sshfence</value>
     </property>
    <!-- 使用隔离机制时需要 ssh 秘钥登录,用于自动故障转移-->
     <property>
         <name>dfs.ha.fencing.ssh.private-key-files</name>
         <value>/home/F/.ssh/id_rsa</value>
     </property>
    </configuration>
    

    (4)在hadoop103和hadoop104的/opt下创建ha目录,并修改所属用户和所属组为F,然后把hadoop102的/opt/ha下的文件进行分发
    (5)将三台机器的HADOOP_HOME环境变量更改到ha目录

    sudo vim /etc/profile.d/my_env.sh
    
    #HADOOP_HOME
    export HADOOP_HOME=/opt/ha/hadoop-3.1.3
    export PATH=$PATH:$HADOOP_HOME/bin
    export PATH=$PATH:$HADOOP_HOME/sbin
    
    source /etc/profile
    

    (6)在各个 JournalNode 节点上,输入以下命令启动 journalnode 服务

     hdfs --daemon start journalnode
    

    (7)在[nn1]上,对其进行格式化,并启动

    hdfs namenode -format
    hdfs --daemon start namenode
    

    (8)在[nn2]和[nn3]上,同步 nn1 的元数据信息

    hdfs namenode -bootstrapStandby
    

    (9)启动[nn2]和[nn3]

    hdfs --daemon start namenode
    

    浏览器访问每一台节点的9870端口,可以发现每一台节点都是standby状态,需要我们手动设置某一台服务器为active状态[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdjSOs5J-1658641856535)(D:\documents\notes\md\images\hadoop\image-20220724111010087.png)]

​ (10)在所有节点上启动datanode

hdfs --daemon start datanode

​ (11)将[nn1]切换为 Active

 hdfs haadmin -transitionToActive nn1

​ (12)查看是否 Active

hdfs haadmin -getServiceState nn1
  • 上面将hadoop102的namenode设置为active状态,hadoop103和hadoop104为standby状态(手动模式),如果hadoop102挂掉后,hadoop103和104中的某一台并不会自动转化成active状态,并且在103和104上手动转换会报错说102不在线,当把102启动后,才可以在103或104上启动namenode,也就是说手动设置节点为active状态需要保证每个集群的每个节点都在线。(原因:隔离机制,同一时间只能有一个节点为active状态,当要把103或104设置为active状态时,如果跟102连接不上的话,无法确定102是挂掉了还是只与本机通信有问题,假如102只是与本机通信有问题,那么将本机设置为active状态的话,集群中就会存在有两台active状态的节点,这种情况是不允许的)
  • HDFS-HA的手动模式并不是高可用,因为active状态的节点挂掉后没有办法继续工作,因此需要设置HDFS-HA自动模式,让处于namenode处于active状态的节点在发生故障之后,能够自动改变其他的namenode状态为active。

10.3 HDFS-HA 自动故障转移

  • HDFS-HA自动故障转移工作机制
    自动故障转移为 HDFS 部署增加了两个新组件:ZooKeeper 和 ZKFailoverController (ZKFC)进程,如图所示。ZooKeeper 是维护少量协调数据,通知客户端这些数据的改变 和监视客户端故障的高可用服务。ZKFC用于检测每个NameNode节点的状态。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kl8vYsXm-1658641856536)(D:\documents\notes\md\images\hadoop\image-20220724122205675.png)]

  • HDFS-HA自动故障转移的集群规划
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qc9BJEST-1658641856536)(D:\documents\notes\md\images\hadoop\image-20220724123601067.png)]

  • HDFS-HA自动故障转移配置
    (1)在 hdfs-site.xml 中增加,分发到三台服务器

    <!-- 启用 nn 故障自动转移 -->
    <property>
        <name>dfs.ha.automatic-failover.enabled</name>
        <value>true</value>
    </property>
    

    (2)在 core-site.xml 文件中增加,分发到三台服务器

    <!-- 指定 zkfc 要连接的 zkServer 地址 -->
    <property>
        <name>ha.zookeeper.quorum</name>
        <value>hadoop102:2181,hadoop103:2181,hadoop104:2181</value>
    </property>
    
  • 启动集群
    (1)关闭所有 HDFS 服务

    stop-dfs.sh
    

    (2)启动 Zookeeper 集群

    zk.sh start
    

    (3)启动 Zookeeper 以后,然后再初始化 HA 在 Zookeeper 中状态

    hdfs zkfc -formatZK
    

    (4)启动 HDFS 服务

     start-dfs.sh
    

    (5)通过zkCli.sh 客户端查看 Namenode 选举锁节点内容,可以看到hadoop103被选举成了active状态

    get -s /hadoop-ha/mycluster/ActiveStandbyElectorLock
    

    (6)测试关闭hadoop103的namenode节点进程后,其他的节点会不会自动切换成active状态
    (7)若关闭了namenode的节点重启namenode后,该节点的状态则变为仍然是standby

  • 新集群跟之前的使用没有区别

    # 上传wcinput文件夹到集群根目录,
     hadoop fs -put wcinput /
    或
     hadoop fs -put wcinput hdfs://mycluster/
    

10.4 YARN-HA概述

  • YARN-HA官方文档http://hadoop.apache.org/docs/r2.7.2/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html
  • YARN-HA工作机制
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBPqMmlF-1658641856536)(D:\documents\notes\md\images\hadoop\image-20220724132135233.png)]

​ (1)在多个节点上开启ResourceManager,先启动的向zookeeper注册状态为active(创建临时节点),其余的ResourceManager一旦发现zookeeper已有ResourceManager注册节点则将自身状态设置为standby
​ (2)状态为standby的ResourceManager保持对ResourceManager注册的zookeeper节点的监听,一旦节点不存在,就进行注册

10.5 YARN-HA集群搭建

  • YARN-HA集群规划[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PmgoaKpF-1658641856536)(D:\documents\notes\md\images\hadoop\image-20220724132638616.png)]

  • YARN-HA核心问题:
    a .如果当前 active rm 挂了,其他 rm 怎么将其他 standby rm 上位 :核心原理跟 hdfs 一样,利用了 zk 的临时节点
    b. 当前 rm 上有很多的计算程序在等待运行,其他的 rm 怎么将这些程序接手过来接着跑: rm 会将当前的所有计算程序的状态存储在 zk 中,其他 rm 上位后会去读取,然后接 着跑

  • YARN-HA配置
    (1)yarn-site.xml

    <configuration>
     <!-- 读取数据方式 -->
     <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>
     <!--指定 resourcemanager 的逻辑列表-->
     <property>
         <name>yarn.resourcemanager.ha.rm-ids</name>
         <value>rm1,rm2,rm3</value>
    </property>
    <!-- ========== rm1 的配置 ========== -->
    <!-- 指定 rm1 的主机名 -->
     <property>
         <name>yarn.resourcemanager.hostname.rm1</name>
         <value>hadoop102</value>
    </property>
    <!-- 指定 rm1 的 web 端地址 -->
    <property>
         <name>yarn.resourcemanager.webapp.address.rm1</name>
         <value>hadoop102:8088</value>
    </property>
    <!-- 指定 rm1 的内部通信地址 -->
    <property>
         <name>yarn.resourcemanager.address.rm1</name>
         <value>hadoop102:8032</value>
    </property>
    <!-- 指定 AM 向 rm1 申请资源的地址 -->
    <property>
        <name>yarn.resourcemanager.scheduler.address.rm1</name> 
         <value>hadoop102:8030</value>
    </property>
    <!-- 指定供 NM 连接的地址 --> 
    <property>
         <name>yarn.resourcemanager.resource-tracker.address.rm1</name>
         <value>hadoop102:8031</value>
    </property>
    <!-- ========== rm2 的配置 ========== -->
     <!-- 指定 rm2 的主机名 -->
     <property>
         <name>yarn.resourcemanager.hostname.rm2</name>
         <value>hadoop103</value>
    </property>
    <property>
         <name>yarn.resourcemanager.webapp.address.rm2</name>
         <value>hadoop103:8088</value>
    </property>
    <property>
         <name>yarn.resourcemanager.address.rm2</name>
         <value>hadoop103:8032</value>
    </property>
    <property>
         <name>yarn.resourcemanager.scheduler.address.rm2</name>
         <value>hadoop103:8030</value>
    </property>
    <property>
         <name>yarn.resourcemanager.resource-tracker.address.rm2</name>
         <value>hadoop103:8031</value>
    </property>
    <!-- ========== rm3 的配置 ========== -->
    <!-- 指定 rm1 的主机名 -->
     <property>
         <name>yarn.resourcemanager.hostname.rm3</name>
         <value>hadoop104</value>
    </property>
    <!-- 指定 rm1 的 web 端地址 -->
    <property>
         <name>yarn.resourcemanager.webapp.address.rm3</name>
         <value>hadoop104:8088</value>
    </property>
    <!-- 指定 rm1 的内部通信地址 -->
    <property>
         <name>yarn.resourcemanager.address.rm3</name>
         <value>hadoop104:8032</value>
    </property>
    <!-- 指定 AM 向 rm1 申请资源的地址 -->
    <property>
         <name>yarn.resourcemanager.scheduler.address.rm3</name> 
         <value>hadoop104:8030</value>
    </property>
    <!-- 指定供 NM 连接的地址 --> 
    <property>
         <name>yarn.resourcemanager.resource-tracker.address.rm3</name>
         <value>hadoop104:8031</value>
    </property>
     <!-- 指定 zookeeper 集群的地址 --> 
     <property>
         <name>yarn.resourcemanager.zk-address</name>
         <value>hadoop102:2181,hadoop103:2181,hadoop104:2181</value>
     </property>
     <!-- 启用自动恢复 --> 
     <property>
         <name>yarn.resourcemanager.recovery.enabled</name>
         <value>true</value>
     </property>
     <!-- 指定 resourcemanager 的状态信息存储在 zookeeper 集群,当rm挂掉后另外一个rm可以从中读取各种信息 --> 
     <property>
         <name>yarn.resourcemanager.store.class</name> 
         <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</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>
    

    (2)分发到三台服务器

  • 启动YARN
    (1)启动yarn

    start-yarn.sh
    

    (2)查看服务状态

    yarn rmadmin -getServiceState rm1
    

    (3)通过zkCli.sh 客户端查看 ResourceManager 选举锁节点内容

    get -s /yarn-leader-election/cluster-yarn1/ActiveStandbyElectorLock
    

    (4)web页面访问任意节点的8088端口,会自动跳转到resourcemanager状态为active的节点对应的8088端口页面
    (5)测试关闭active状态的resoucemanager,其他resourcemanager能否自动转换成active状态
    (6)关闭了的resourcemanager重新启动后,变成了standby状态

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值