七、Hadoop简明笔记

目录

1、Hadoop入门

1.1 Hadoop优势

  • 高可靠性

    Hadoop底层维护多个数据副本,即使Hadoop某个计算元素或存储出现故障,也不会导致数据丢失

  • 高扩展性

    在集群间分配任务数据,可方便的扩展数以千计的节点

  • 高效性

    在MapReduce的思想下,Hadoop是并行工作的,以加快任务处理速度

  • 高容错性

    能够自动将失败的任务重新分配

1.2 Hadoop1.x、Hadoop2.x、Hadoop3.x的区别

  • 在Hadoop1.x时代,Hadoop中的MapReduce同时处理业务逻辑运算和资源的调度,耦合性较大
  • 在Hadoop2.x时代,增加了Yarn。此时Yarn只负责资源的调度,MapReduce只负责运算
  • 在Hadoop3.x时代,其组成与Hadoop2.x没有变化,功能上进行了优化

1.3 HDFS架构概述

Hadoop Distributed File System,简称HDFS,是一个分布式文件系统

  • 角色
    • NameNode(简称 nn)

      存储文件的元数据:如文件名,文件目录结构,文件属性(生成时间、副本数、文件权限),以及每个文件 的块列表和块所在的DataNode等

    • DataNode(简称 dn)

      在本地文件系统存储文件块数据,以及块数据的校验和

    • Secondary NameNode(简称 2nn)

      每隔一段时间对NameNode元数据备份

1.4 YARN架构概述

Yet Another Resource Negotiator,简称YARN,是一种资源协调者,是Hadoop的资源管理器

  • 角色
    • ResourceManager(RM):整个集群资源(内存、CPU等)的调度者
    • NodeManager(NM):单个节点服务器资源的调度者
    • ApplicationMaster(AM):单个任务运行的调度者
    • Container:容器,里面封装了任务运行所需要的资源(内存、CPU、磁盘、网络等)
  • 说明
    • 客户端可以有多个
    • 集群上可以运行多个ApplicationMaster
    • 每个NodeManager上可以有多个Container

1.5 MapReduce架构概述

MapReduce将计算过程分为两个阶段:Map和Reduce

  • Map阶段并行处理输入数据
  • Reduce阶段对Map结果进行汇总

1.6 HDFS、YARN、MapReduce三者关系

  1. 客户端发送任务请求
  2. YARN接受到请求,开启一个Container1(包含一个ApplicationMaster1),该Container1向YARN请求资源
  3. Container1获取到资源后,开始MapReduce中的Map阶段,开启一个Container(包含一个ApplicationMaster2,即MapTask)
  4. 在Map阶段,ApplicationMaster2与HDFS进行交互
  5. HDFS按NameNode的元数据信息检索,将DataNode中存储的数据返回给ApplicationMaster2
  6. Container1收到MapTask完成的信息后,开启一个Container(包含一个ApplicationMaster3,即ReduceTask),ApplicationMaster3汇总ApplicationMaster2返回的数据
  7. ApplicationMaster3汇总完数据后,通知HDFS完成数据的存储
  8. 任务结束,释放资源

1.7 大数据技术生态体系

1.8 环境准备

1.9 Hadoop运行模式

  • 本地模式

    单机运行,只是用来演示一下官方案例

  • 伪分布式模式

    单机运行,但是具备了Hadoop集群的所有功能,一台服务器模拟一个分布式的环境

  • 完全分布式模式

    多台服务器组成分布式环境

1.10 Hadoop本地模式运行:官方WordCount案例

  1. 创建wcinput文件夹

    mkdir -p ~/learnstation/wcinput
    
  2. 在wcinput文件夹下创建一个word.txt文件并输入以下内容

    vim word.txt
    
    hadoop yarn
    hadoop mapreduce
    test
    test
    
  3. 执行程序

    hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount ~/learnstation/wcinput ~/learnstation/wcoutput
    
  4. 查看结果

    cat ~/learnstation/wcoutput/part-r-00000
    

2、搭建Hadoop集群

2.1 集群部署规划

pc001(192.168.117.101) pc002(192.168.117.102) pc003((192.168.117.101))
HDFS NameNode
DataNode

DataNode
SecondaryNameNode
DataNode
YARN
NodeManager
ResoureManager
NodeManager

NodeManager
  • 注意点

    1. NameNode和SecondaryNameNode很消耗内存,不要安装在同一台服务器
    2. ResourceManager很消耗内存,不要和NameNode、SecondaryNameNode配置在同一台机器上

2.2 配置文件说明

Hadoop配置文件分为两类:默认配置文件和自定义配置文件。

  • 默认配置文件

    默认文件 文件存放在Hadoop的jar包中的位置
    [core-default.xml] $HADOOP_HOME/share/hadoop/common/hadoop-common-xxx.jar
    [hdfs-default.xml] $HADOOP_HOME/share/hadoop/hdfs/hadoop-hdfs-xxx.jar
    [yarn-default.xml] $HADOOP_HOME/share/hadoop/yarn/hadoop-yarn-common-xxx.jar
    [mapred-default-xml] $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-core-xxx.jar
  • 自定义配置文件

    core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml四个配置文件存放在$HADOOP_HOME/etc/hadoop路径下,用户想修改某一默认配置值时,可以修改自定义配置文件,更改相应属性值。

2.3 配置自定义文件

2.3.1 配置核心文件

  • 配置core-sit.xml

    cd $HADOOP_HOME/etc/hadoop
    vim core-site.xml
    
  • 在标签configuration中添加以下内容

    <configuration>
        <!-- 指定NameNode的地址 HDFS内部的通信地址-->
        <property>
            <name>fs.defaultFS</name>
            <value>hdfs://pc001:8082</value>
        </property>
        <!-- 指定Hadoop本地数据的存储地址-->
        <property>
            <name>hadoop.tmp.dir</name>
            <value>/opt/software/hadoop-3.1.3/data</value>
        </property>
        <!-- 配置HDFS网页登录使用的静态用户 -->
        <property>
            <name>hadoop.http.staticuser.user</name>
            <value>nuochengze</value>
        </property>
    
    </configuration>
    
  • 分发给其他服务器

    ~/bin/xsync.sh core-site.xml
    

2.3.2 配置HDFS文件

  • 配置hdfs-site.xml文件

    cd $HADOOP_HOME/etc/hadoop
    vim hdfs-site.xml
    
  • 在标签configuration中添加以下内容

    <configuration>
        <!-- NameNode nn web端访问地址-->
        <property>
            <name>dfs.namenode.http-address</name>
            <value>pc001:9870</value>
        </property>
        <!-- SecondaryNameNode 2nn web端访问地址 -->
        <property>
            <name>dfs.namenode.secondary.http-address</name>
            <value>pc003:9868</value>
        </property>
    </configuration>
    
  • 分发给其他服务器

2.3.3 配置YARN文件

  • 配置yarn-site.xml文件

    cd $HADOOP_HOME/etc/hadoop
    vim yarn-site.xml
    
  • 在标签configuration中添加以下内容

    <configuration>
    
    <!-- Site specific YARN configuration properties -->
        <!-- 指定MR走 shuffle-->
        <property>
            <name>yarn.nodemanager.aux-services</name>
            <value>mapreduce_shuffle</value>
        </property>
        <!-- 指定 ResourceManager 的地址-->
        <property>
            <name>yarn.resourcemanager.hostname</name>
            <value>pc002</value>
        </property>
        <!-- 环境变量的继承-->
        <property>
            <name>yarn.nodenamager.env-whitelist</name>
            <value>JAVA_HOEM,HADOOP_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
        </property>
    </configuration>
    
  • 分发给其他服务器

2.3.4 配置MapReduce文件

  • 配置mapred-site.xml文件

    cd $HADOOP_HOME/etc/hadoop
    vim mapred-site.xml
    
  • 在标签configuration中添加以下内容

    <configuration>
        <!-- 指定MapReduce 程序运行在Yarn上-->
        <property>
            <name>mapreduce.framework.name</name>
            <value>yarn</value>
        </property>
    </configuration>
    
  • 分发给其他服务器

2.4 群起集群

2.4.1 配置workers

  • 编辑workers文件

    cd $HADOOP_HOME/etc/hadoop
    vim workers
    
  • 修改内容为如下

    pc001
    pc002
    pc003
    

    说明:workers文件中,添加的内容结尾不允许有空格,文件中不允许有空行

  • 分发给其他服务器

2.4.2 启动集群

注意!!! 在root用户下启动集群时,会报”ERROR: Attempting to operate on hdfs namenode as root“

需要用非root用户启动

useradd nuochengze // 添加用户nuochengze
passwd nuochengze // 修改密码
配置 nuochengze 用户具有 root 权限,方便后期加 sudo 执行 root 权限的命令
1、vim /etc/sudoers
2、vim /etc/sudoers
...
## Allow root to run any commands anywhere
root ALL=(ALL) ALL
## Allows people in group wheel to run all commands
%wheel ALL=(ALL) ALL
nuochengze ALL=(ALL) NOPASSWD:ALL
...
3、注意:nuochengze 这一行不要直接放到 root 行下面,因为所有用户都属于 wheel 组,你先
配置了 nuochengze 具有免密功能,但是程序执行到%wheel 行时,该功能又被覆盖回需要
密码。所以 nuochengze 要放到%wheel 这行下面
  • 格式化namenode

    hdfs namenode -format
    

    说明:

    1. 如果集群第一次启动,需要在pc001节点格式化NameNode
    2. 格式化NameNode,会产生新的集群ID,导致NameNode和DataNode的集群ID不一致,集群找不到以往数据
    3. 如果集群在运行过程中报错,需要重新格式化NameNode的话,一定要先停止namenode和datanode进程,并且要删除所有机器的data和logs目录,然后再进行格式化
  • 在pc001上启动HDFS

    ./sbin/start-dfs.sh
    

    报错参考:https://www.cnblogs.com/nuochengze/p/14883150.html

  • 查看启动的active internet connections

    netstat -nltp
    
  • 在pc002(布置了ResoureceManager的节点)上启动yarn

    ./sbin/start-yarn.sh
    
  • web端查看HDFS的NameNode

    浏览器中输入:http://pc001:9870
    
  • web端查看YARN的ResourceManager

    浏览器中输入:http://pc002:8088
    

2.4.3 配置历史服务器

  • 作用

    查看程序的历史运行情况

  • 配置mapred-site.xml,添加以下内容

     <!-- 历史服务器地址-->
        <property>
            <name>mapreduce.jobhistory.address</name>
            <value>pc001:10020</value>
        </property>
        <!-- 历史服务器web端地址-->
        <property>
            <name>mapreduce.jobhistory.webapp.address</name>
            <value>pc001:19888</value>
        </property>
    
  • 在pc001上面启动历史服务器

    mapred --daemon start historyserver
    
  • 查看JobHistory

    http://pc001:19888
    

2.4.4 配置日志的聚集

  • 作用

    应用运行完成后,将程序运行日志信息上传到HDFS系统上,方便开发调试

  • 配置yarn-site.xml,添加以下内容

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

    mapred --daemon start historyserver
    
  • 查看日志

    http://pc001:19888/jobhistory/logs
    

2.4.5 集群启动/停止方式

  • 各个模块分开启动/停止

    start-dfs.sh/stop-dfs.sh  #HDFS的整体启停
    start-yarn.sh/stop-yarn.sh #yarn的整体启停
    
  • 单个服务组件逐一启动/停止

    • HDFS组件

      hdfs --daemon start/stop namenode/datanode/secondarynamenode
      
    • YARN组件

      yarn --daemon start/stop resourcemanager/nodemanager
      

2.4.6 常用脚本

  • 启动集群

    # xhadoopop.sh
    #! /bin/bash
    
    if [ $# -lt 1 ]
        then
            echo "Tips:请输入参数"
            exit
    fi
    
    case $1 in
    "start")
        echo "=================启动集群========================"
        echo "-----------------启动历史服务器-----------"
        ssh pc001 "mapred --daemon start historyserver"
        echo "-----------------启动HDFS-----------------"
        ssh pc001 "$HADOOP_HOME/sbin/start-dfs.sh"
        echo "-----------------启动YARN-----------------"
        ssh pc002 "$HADOOP_HOME/sbin/start-yarn.sh"
    ;;
    "stop")
        echo "=================关闭集群========================"
        echo "-----------------关闭HDFS-----------------"
        ssh pc001 "$HADOOP_HOME/sbin/stop-dfs.sh"
        echo "-----------------关闭YARN-----------------"
        ssh pc002 "$HADOOP_HOME/sbin/stop-yarn.sh"
        echo "-----------------关闭历史服务器-----------"
        ssh pc001 "mapred --daemon stop historyserver"
    ;;
    *)
        echo "Error:请输入正确的参数"
        echo "参数 start: 启动集群"
        echo "参数 stop : 关闭集群"
    ;;
    esac
    
  • 文件分发

    # xsync.sh
    #! /bin/bash
    
    # 1.判断参数个数
    if [ $# -lt 1 ]
        then
            echo 'Not Enougth Arugement!'
            exit;
    fi
    
    # 2.遍历集群所有机器
    for host in  pc001 pc002 pc003
    do
        echo "==================$host====================="
        # 3.遍历指定的目录
        for file in $@
        do
            # 4.判断指定文件是否存在
            if [ -e $file ]
                then
                    # 4.1 获取父目录
                    pdir=$(cd -P $(dirname $file);pwd)
                    # 4.2 获取文件名称
                    fname=$(basename $file)
                    # 4.3 对目标host创建对应的目录
                    ssh $host "mkdir -p $pdir"
                    # rsync同步文件
                    rsync -av $pdir/$fname $host:$pdir/$fname
                else
                    echo "The file $file is not exists!"
            fi
        done
    done
    
  • jps集群监控

    # xjpsall.sh
    #! /bin/bash
    
    for host in pc001 pc002 pc003
    do
        echo "============$host=============="
        ssh $host "jps"
    done
    

2.5 配置文件整体一览

2.5.1 core-site.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <!-- 指定NameNode的地址 -->
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://node001:9000</value>
    </property>
    <!-- 指定Hadoop本地数据的存储地址-->
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/opt/software/hadoop-3.2.2/data</value>
    </property>
    <!-- 配置HDFS网页登录使用的静态用户 -->
    <property>
        <name>hadoop.http.staticuser.user</name>
        <value>nuochengze</value>
    </property>

    <property>
        <name>hadoop.proxyuser.nuochengze.groups</name>
        <value>*</value>
    </property>

    <property>
        <name>hadoop.proxyuser.nuochengze.hosts</name>
        <value>*</value>
    </property>

</configuration>

2.5.2 hdfs-site.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>

    <!-- NameNode nn web端访问地址-->
    <property>
        <name>dfs.namenode.http-address</name>
        <value>node001:9870</value>
    </property>
    <!-- SecondaryNameNode 2nn web端访问地址 -->
    <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>node003:9868</value>
    </property>
    <property>
    	<name>dfs.replication</name>
    	<value>3</value>
    </property>

</configuration>

2.5.3 yarn-site.xml

<?xml version="1.0"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->
<configuration>

<!-- Site specific YARN configuration properties -->


    <!-- 指定MR走 shuffle-->
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <!-- 指定 ResourceManager 的地址-->
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>node002</value>
    </property>
    <!-- 环境变量的继承-->
    <property>
        <name>yarn.nodemanager.env-whitelist</name>
        <value>JAVA_HOEM,HADOOP_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
    </property>

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

    <property>
        <!-- 虚拟内存检查,默认打开,修改为关闭 -->
        <name>yarn.nodemanager.vmem-check-enabled</name>
        <value>false</value>
    </property>

    <property>
        <!-- 物理内存检查,默认打开,修改为关闭 -->
        <name>yarn.nodemanager.pmem-check-enabled</name>
        <value>false</value>
    </property>

</configuration>

2.5.4 mapred-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License. See accompanying LICENSE file.
-->

<!-- Put site-specific property overrides in this file. -->

<configuration>
    <!-- 指定MapReduce 程序运行在Yarn上-->
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>mapreduce.application.classpath</name>
        <value>/opt/software/hadoop-3.2.2/etc/hadoop:/opt/software/hadoop-3.2.2/share/hadoop/common/lib/*:/opt/software/hadoop-3.2.2/share/hadoop/common/*:/opt/software/hadoop-3.2.2/share/hadoop/hdfs:/opt/software/hadoop-3.2.2/share/hadoop/hdfs/lib/*:/opt/software/hadoop-3.2.2/share/hadoop/hdfs/*:/opt/software/hadoop-3.2.2/share/hadoop/mapreduce/lib/*:/opt/software/hadoop-3.2.2/share/hadoop/mapreduce/*:/opt/software/hadoop-3.2.2/share/hadoop/yarn:/opt/software/hadoop-3.2.2/share/hadoop/yarn/lib/*:/opt/software/hadoop-3.2.2/share/hadoop/yarn/*</value>
    </property>
    <!-- 历史服务器地址-->
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>node001:10020</value>
    </property>
    <!-- 历史服务器web端地址-->
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>node001:19888</value>
    </property>

    <!-- 以下对标:2GB内存,2核心-->
    <property>
      <name>mapreduce.map.memory.mb</name>
      <value>1024</value>
    </property>
    <property>
      <name>mapreduce.map.java.opts</name>
      <value>-Xmx1024M</value>
    </property>
    <property>
      <name>mapreduce.reduce.memory.mb</name>
      <value>1024</value>
    </property>
    <property>
      <name>mapreduce.reduce.java.opts</name>
      <value>-Xmx1024M</value>
    </property>
</configuration>

2.6 常用端口

端口名称 Hadoop2.x Hadoop3.x
NameNode内部通信端口 8020/9000 8020/9000/9820
NameNode HTTP UI 50070 9870
Yarn查看任务执行端口 8088 8088
历史服务器通信端口 19888 19888

2.7 集群时间同步

如果服务器在公网环境(能连接外网),可以不采用集群时间同步,因为服务器会定期 和公网时间进行校准;

如果服务器在公网环境(能连接外网),可以不采用集群时间同步,因为服务器会定期 和公网时间进行校准;

2.7.1 时间服务器配置

说明:必须是root用户 ! ! !

  1. 查看节点ntpd服务状态和开机自启动状态

    systemctl status ntpd  # 查看节点ntpd服务状态 
    systemctl start ntpd # 启动ntpd
    systemctl is-enabled ntpd # 开机自启动
    
  2. 修改 时间服务器 的 ntp.conf 配置文件

    vim /etc/ntp.conf
    
    # 修改 1(授权 192.168.10.0-192.168.10.255 网段上的所有机器可以从这台机器上查询和同步时间)
    #restrict 192.168.117.0 mask 255.255.255.0 nomodify notrap
    为 restrict 192.168.117.0 mask 255.255.255.0 nomodify notrap
    
    # 修改 2(集群在局域网中,不使用其他互联网上的时间)
    server 0.centos.pool.ntp.org iburst
    server 1.centos.pool.ntp.org iburst
    server 2.centos.pool.ntp.org iburst
    server 3.centos.pool.ntp.org iburst
    为
    #server 0.centos.pool.ntp.org iburst
    #server 1.centos.pool.ntp.org iburst
    #server 2.centos.pool.ntp.org iburst
    #server 3.centos.pool.ntp.org iburst
    
    # 添加 3(当该节点丢失网络连接,依然可以采用本地时间作为时间服务器为集群中的其他节点提供时间同步)
    server 127.127.1.0
    fudge 127.127.1.0 stratum 10
    
  3. 修改 时间服务器 的/etc/sysconfig/ntpd 文件

    vim /etc/sysconfig/ntpd
    
    # 增加内容如下(让硬件时间与系统时间一起同步)
    SYNC_HWCLOCK=yes
    
  4. 重新启动 ntpd 服务及设置 ntpd 服务开机启动

    systemctl start ntpd # 启动ntpd
    systemctl enable ntpd # 开机自启动
    

2.7.2 其他机器配置

  1. 关闭所有节点上 ntp 服务和自启动

    systemctl stop ntpd
    systemctl disable ntpd
    
  2. 在其他机器配置 1 分钟与时间服务器同步一次

    crontab -e
    
    # 编写定时任务如下
    */1 * * * * /usr/sbin/ntpdate hadoop102
    

    crontab使用说明:https://www.cnblogs.com/nuochengze/p/12961967.html

3、HDFS

3.1 HDFS的定义

HDFS(Hadoop Distributed File System),是一个分布式的目录树文件系统,适合用一次写入多次读出的场景。

3.2 HDFS的优缺点

2.1 优点

  • 高容错性

    数据自动保存多个副本,即通过增加副本的形式,提高容错性。

  • 适合处理大数据

    • 数据规模:可达PB级别的数据
    • 文件规模:能够处理百万规模以上的文件数量
  • 可构建在廉价机器上,通过多副本机制,提高可靠性

2.2 缺点

  • 不适合低延时数据访问
  • 无法高效的对大量小文件进行存储
  • 不支持并发写入,文件随机修改
    • 一个文件只能有一个写,不允许多个线程同时写
    • 仅支持数据append(追加),不支持文件的随机修改

3.3 HDFS的组成

  • NameNode(nn)
    1. 名义上的Master,是一个管理者
    2. 管理HDFS的名称空间
    3. 配置副本策略
    4. 管理数据块(Block)映射信息
    5. 处理客户端读写请求
  • DataNode
    1. 名义上的Slave,是一个执行者
    2. 存储实际的数据块
    3. 执行数据块的读/写操作
  • Client
    1. 客户端
    2. 实现文件的切分:文件上传时,Client将文件切分成一个一个的Block,然后进行上传,文件快大小常为128MB
    3. 与NameNode交互,获取文件的位置信息(元数据信息)
    4. 与DataNode交互,读取或者写入数据
    5. Client提供一些命令来管理HDFS,例如对NameNode的格式化
    6. Client提供一些命令来访问HDFS,例如对HDFS增删改查操作
  • SecondaryNameNode
    1. 它承担NameNode的辅助工作,但并不是NameNode的热备份,两者之间存在差异,当NameNode挂掉的时候,它并不能马上替换NameNode并提供服务
    2. 辅助NameNode,分担其工作量,如定期合并Fsimage和Edits,并推送给NameNode
    3. 在紧急情况下,可辅助恢复NameNode

3.4 HDFS的文件块大小

  • 说明
    1. HDFS中的文件在物理上是分块存储的,块(Block)的大小可以通过配置参数(dfs.blocksize)来规定,默认大小在Hadoop2.x/3.x版本中是128M,1.x版本中是64M
    2. 寻址时间,即找到目标Block的时间,通常为10ms左右
    3. 传输时间,寻址时间为传输时间的1%时,则为最佳状态
    4. HDFS的Block大小设置主要取决于磁盘传输速率,普通磁盘的传输速率在100MB/s左右,设置Block为128MB即可,固态硬盘的传输速率在200MB/s~300MB/s之间,设置为256MB较为合适
  • 块的大小不能设置太小,也不能设置太大
    1. HDFS的块设置的太小,会增加寻址时间,程序表现为一直在找块的位置
    2. 如果快设置的太大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间,导致了程序在处理这块数据时,会非常缓慢

3.5 HDFS的shell操作

3.5.1 基本语法

(1)hadoop fs 具体命令
(2)hdfs dfs 具体命令

3.5.2 使用方式

# 1.查看所有的命令
hadoop fs -help

# 2.查看具体命令的用法
hadoop fs --help put

# 3.从本地剪切粘贴到 HDFS
hadoop fs -moveFromLocal dir1 dir2

# 4.从本地文件系统中拷贝文件到 HDFS 路径去
hadoop fs -copyFromLocal dir1 dir2

# 5.等同于 copyFromLocal,生产环境更习惯用 put
hadoop fs -put dir1 dir2

# 6.追加一个文件到已经存在的文件末尾
hadoop fs -appendToFile file1 dir2

# 7.从 HDFS 拷贝到本地
hadoop fs -copyToLocal dir1 dir2

# 8.等同于 copyToLocal,生产环境更习惯用 get
hadoop fs -get dir1 dir2

# 9.-ls: 显示目录信息
hadoop fs -ls dir

# 10.显示文件内容
hadoop fs -cat file

# 11.-/chgrp、-chmod、-chown:Linux 文件系统中的用法一样,修改文件所属权限
hadoop fs -chmod/chgrp/chown file/dir

# 12.创建路径
hadoop fs -mkdir dir

# 13.从 HDFS 的一个路径拷贝到 HDFS 的另一个路径
hadoop fs -cp dir1 dir2

# 14.在 HDFS 目录中移动文件
hadoop fs -mv dir1 dir2

# 15.显示一个文件的末尾 1kb 的数据
hadoop fs -tail file

# 16.删除文件或文件夹
hadoop fs -rm file/dir

# 17.递归删除目录及目录里面内容
hadoop fs -rm -r dir

# 18.统计文件夹的大小信息
hadoop fs -du -s -h  dir #27 表示文件大小;81 表示 27*3 个副本
hadoop fs -du -h  dir # 查看文件夹里的细节

# 19.设置 HDFS 中文件的副本数量
 hadoop fs -setrep 10 file 
 # 这里设置的副本数只是记录在 NameNode 的元数据中,是否真的会有这么多副本,还得看 DataNode 的数量。因为目前只有 3 台设备,最多也就 3 个副本,只有节点数的增加到 10台时,副本数才能达到10

3.6 HDFS 的 API 操作

3.6.1 准备工作

  • 创建Maven项目

    img

  • 修改pom.xml文件,增加依赖

        <dependencies>
            <dependency>
                <groupId>org.apache.hadoop</groupId>
                <artifactId>hadoop-client</artifactId>
                <version>3.1.3</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.7.30</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>RELEASE</version>
                <scope>compile</scope>
            </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
    

3.6.2 HDFS的API操作

package com.nuochengze.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 {
        // 获取文件系统
        URI uri = new URI("hdfs://pc001:8082");
        String user = "nuochengze";
        Configuration configuration = new Configuration();
        fs = FileSystem.get(uri, configuration, user);
    }

    @After
    public void close() throws IOException {
        fs.close();
    }

    @Test
    public void testMkdir() throws Exception {
        // 1.创建目录
        fs.mkdirs(new Path("hdfs://pc001:8082/test1"));
    }

    @Test
    public void testUpLoadFile() throws Exception {
        // 2.上传文件
        /*
           参数1:表示是否删除原数据
           参数2:表示是否允许覆盖
           参数3:原数据路径
           参数4:目标路径
         */
        fs.copyFromLocalFile(false,
                true,
                new Path("/home/nuochengze/learnstation/test/word.txt"),
                new Path("hdfs://pc001:8082/test"));
    }

    @Test
    public void testDownLoadFile() throws Exception {
        // 3.文件下载
        /*
           参数1:表示是否删除原数据
           参数2:指定下载的文件路径
           参数3:指定将文件下载到的路径
           参数4:是否开启文件校验
         */
        fs.copyToLocalFile(false,
                new Path("/test/word.txt"),
                new Path("/home/nuochengze/learnstation/test/word2.txt"),
                true);
    }

    @Test
    public void testRenameFile() throws Exception {
        // 4.文件更名
        fs.rename(new Path("/test/word.txt"),
                new Path("/test/word_rename.txt"));
        // 5. 文件的移动和更名
        fs.rename(new Path("/test/word_rename.txt"), new Path("/word.txt"));
        // 6. 目录更名
        fs.rename(new Path("/test"), new Path("/test_re"));
    }

    @Test
    public void testDeleteFile() throws Exception {
        // 7.删除文件
        fs.delete(new Path("/word.txt"), false);
        // 8.删除目录  参数1:要删除的路径,  参数2:是否递归删除
        // 当删除的目录为非空目录时,参数2必须为true,否则会抛出异常
        fs.delete(new Path("/test_re"), false);
    }

    @Test
    public void testGetFileDetail() throws Exception {
        // 9.获取文件详情 参数1:要查询的文件路径  参数2:是否递归查询
        RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/test/word.txt"),
                true);
        while (listFiles.hasNext()) {
            LocatedFileStatus fileStatus = listFiles.next();
            System.out.println(fileStatus);
            /*
            HdfsLocatedFileStatus{
                path=hdfs://pc001:8082/test/word.txt;   // 文件路径
                isDirectory=false;   // 目录:true 文件:false
                length=71; // 文件大小
                replication=3;  // 副本数量
                blocksize=134217728;  // 块大小
                modification_time=1631934598065;  // 上次修改时间
                access_time=1631934597763;   // 上传时间
                owner=nuochengze;  // 文件所有者
                group=supergroup;  // 文件所属组
                permission=rw-r--r--;  // 文件权限
                isSymlink=false;
                hasAcl=false;
                isEncrypted=false;
                isErasureCoded=false}
            * */
            System.out.println("====" + fileStatus.getPath() + "====");

            // 10.获取块信息
            BlockLocation[] blockLocations = fileStatus.getBlockLocations();
            System.out.println(Arrays.toString(blockLocations));
            /*
             [0,71,pc002,pc001,pc003]
            * */
        }
    }

    @Test
    public void testIsFileOrDirectory() throws Exception {
        // 11.判断是文件夹还是文件
        FileStatus[] listStatus_ = fs.listStatus(new Path("/"));
        for (FileStatus ls : listStatus_) {
            if(ls.isFile()){
                System.out.println("f: " + ls.getPath().getName());
            }else{
                System.out.println("d: " + ls.getPath().getName());
            }
        }
    }

}


3.6.3 参数优先级

参数优先级排序:(1)客户端代码中设置的值 >(2)ClassPath 下的用户自定义配置文 件 >(3)然后是服务器的自定义配置(xxx-site.xml)>(4)服务器的默认配置(xxx-default.xml)

3.7 HDFS的读写流程

3.7.1 HDFS的写数据流程

img

  1. Client通过Distributed FileSystem(分布式文件系统) 向 NameNode 发起请求上传文件的信息,NameNode检查目标文件是否已存在,父目录是否存在,检查权限

  2. NameNode响应给Client可以上传文件

  3. Client发送给NameNode信息,请求上传第一个Block(0~128MB),让NameNode返回DataNode的信息

  4. NameNode返回信息给Client,明确哪些DataNode可以存储数据

    • 节点最近:两个节点到达最近的共同祖先的距离总和

      在HDFS写数据的过程中,NameNode会选择距离待上传数据最近距离的DataNode接收数据

    • 负载均衡

  5. Client创建一个流(FSDataOutputStream ),向DataNode1请求建立Block传输通道,DataNode1则向DataNode2请求建立Block传输通道,DataNode2向DataNode3请求建立Block传输通道,依次类推直到所有DataNode信息传递完成

  6. 当尾部DataNode收到请求建立通道连接的信息后,返回给上级DataNode2信息说明DataNode3应答成功,DataNode2返回给上级DataNode1信息说明DataNode2应答成功,DataNode1返回给上级Client信息说明DataNode3应答成功,于此传输通道建立成功

  7. Client生成一个发送消息的队列,存放Packet,之后从队列中往DataNode1发送第一个Packet,同时Client创建ack队列存放所发送数据的应答信息;当DataNode1收到Packet后,生成一个缓存队列(Bytebuffer)存放接受到的信息,之后DataNode1在磁盘写一份数据,同时将缓冲队列中的Packet分发给下级直至尾部DataNode,即留一份发一份,当尾部收到信息后反馈给上级应答成功的信息;当整个链路完成所有应答,表示该Packet传输完成,此时ack中将该Packet对应的应答信息删除;如果链路未完成整体应答,Client将该Packet对应在ack中的应答信息及Packet数据,一同塞入发送消息的队列,直至完成传输。

    • ack队列,存放所有应答信息
    • Packet,数据传输的基本单位,大小为64K,每一个Packet由512字节的chunk和一个4字节的chunksum校验位,以及数据部分组成
    • 多个Packet,组成一个Block
  8. 数据传输完成

3.7.2 HDFS的读数据流程

img

  1. Client通过DistributedFileSystem,向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址
  2. Client按距离优先,负载优先的顺序,请求读取数据
  3. DataNode开始传输数据给Client(从磁盘里面读取数据输入流,以Packet为单位来做校验)
  4. Client以Packet为单位接收,先在本地缓存,然后写入目标文件
    • 数据读取时,并行读取后将数据拼接,返回给Client

3.8 NameNode和SecondaryNameNode

3.8.1 工作流程

img

  • 图例说明1
    • FsImage文件,备份元数据,只存放计算出的结果
    • Edits文件,存放操作的步骤
    • SecondaryNameNode,专门用于FsImage和Edits的合并
  • 图例说明2
    1. 第一阶段,NameNode启动
      1. 第一次启动NameNode格式化后,创建FsImage和Edits文件。非第一次启动,直接加载编辑日志(edits_inprogress_001)和镜像文件(fsimage)到内存
      2. 客户端(client)对元数据进行增删改请求
      3. NameNode记录操作日志,更新滚动日志,即往Edits文件(edits_inprogress_xxx)中追加数据
      4. NameNode在内存中对元数据进行增删改(通过合并edits_inprogree_xxx和fsimage)
    2. 第二阶段,SecondaryNameNode工作
      1. SecondaryNameNode询问NameNode是否需要CheekPoint,直接带回NameNode是否检查结果的应答
      2. SecondaryNameNode请求执行CheckPoint
      3. NameNode滚动正在写的Edits日志,即将CheckPoint之后的操作日志记录在新创建的edits_inproress_002文件中,同时edits_inprogress_001文件更名为edits_001
      4. 将滚动钱的编辑日志edits_001和镜像文件FsImage拷贝到SecondaryNameNode
      5. 在内存中生成新的fsimage.chkpoint
      6. 拷贝fsimage.chkpoint到NameNode,并将其命名为fsimage,替代掉旧的fsimage

3.8.2 Fsimage和Edits解析

  • Fsimage和Edits的概念

    NameNode被格式化后,在$HADOOP_HONE/data/tmp/dfs/name/current目录中产生如下文件:

    img

    • Fsimage文件:HDFS文件系统元数据的一个永久性的检查点,其中包含HDFS文件系统的所有目录和文件inode的序列化信息
    • Edits文件:存放HDFS文件系统的所有更新操作的路径,文件系统客户端执行的所有写操作首先会被记录到Edits文件中
    • seen_txid文件:保存最后一个edits_inprogress_xxx的数字
    • 每次NameNode启动的时候都会将Fsimage文件读入内存中,加载Edits里面的更新操作,保证内存中的元数据信息是最新的,即,可以将NameNode启动的时候加载的文件,看成是Fsimage和Edits文件的合并
    • 在Fsimage中没有记录块所对应的DataNode,因为在集群启动后,NameNode要求DataNode上报数据块信息,并间隔一段时间后再次上报
  • 查看Fsimage文件和Edits文件

    正常打开Fsimage文件和Edits文件,显示乱码

    • oiv 查看Fsimage文件

      # 基本语法
      hdfs oiv -p 文件类型 -i 镜像文件 -o 转换后文件输出路径
      
      # example:
      hfds oiv -p XML -i $HADOOP_HOME/,,,/fsimage_xxx -o /.../fsimage.xml
      
    • oev查看Edits文件

      # 基本语法
      hdfs oev -p 文件类型 -i 编辑日志 -o 转换后文件输出路径
      
      # example:
      hfds oev -p XML -i $HADOOP_HOME/,,,/edits_xxx -o /.../edits.xml
      

3.8.3 CheckPoint时间设置

  • 通常的情况下,SecondaryNameNode每隔一个小时执行一次

    [hdfs-default.xml]
    
    <property>
    	<name>dfs.namenode.checkpoint.period</name>
        <value>3600s</value>
    </property>
    
  • 一分钟检查一次操作次数,当操作次数达到1百万时,SecondaryNameNode执行一次

    [hdfs-default.xml]
    
    <property>
    	<name>dfs.namenode.checkpoint.txns</name>
        <value>1000000</value>
    </property>
    <property>
    	<name>dfs.namenode.checkpoint.check.period</name>
        <value>60s</value>
    </property>
    

3.9 DataNode工作机制

3.9.1 DataNode工作机制

img

  • 一个数据块在DataNode上以文件形式存储在磁盘上,包括:数据本身,元数据(数据块的长度,数据块的校验和,时间戳等)

  • DataNode启动后向NameNode注册,通过后,周期性(6小时)的向NameNode上报所有的块信息

    [hdfs-default.xml]
    
    <property>
    	<name>dfs.blockreport.intervalMsec</name>
        <value>21600000</value>
        <description>DataNode向NameNode汇报当前解读信息的时间间隔,默认为6小时</description>
    </property>
    
    
    <property>
    	<name>dfs.datanode.directoryscan.interval</name>
        <value>21600000</value>
        <description>DataNode扫描自己节点块信息列表的时间,默认为6小时</description>
    </property>
    
  • 心跳是每3秒一次,心跳返回结果带有NameNode给该DataNode的命令(如复制块数据,或删除块数据)。如果超过10分钟没有收到某个DataNode的心跳,则认为该节点不可用

3.9.2 数据完整性

DataNode节点保证数据完整性的方式:

  1. 当DataNode读取Block的时候,会计算出文件的CheckSum
  2. 如果计算传输过来的CheckSum,与Block创建时值不一样,说明Block已经损坏
  3. 此时Client会读取其他DataNode上的Block,来保证数据传输的完整性
  4. 常见的校验算法crc(32),md5(128),sal1(160)
  5. DataNode在其文件创建后周期验证CheckSum

3.9.3 掉线时限参数设置

img

  1. DataNode进行死亡或者网络故障造成DataNode无法与NameNode通信

  2. NameNode不会立即把该节点判定为死亡,要经过一段时间,这段时间暂称作超时时长

  3. HDFS默认的超时时长为10分钟+30秒

  4. 如果定义超时时长为TimeOut,则超时时长的计算公式为:

    TimeOut = 2 * dfs.namenode.heartbeat.recheck-interval + 10*dfs.heartbeat.interval
    
    • 默认的dfs.namenode.heartbeat.recheck-interval大小为5分钟

    • 默认的dfs.heartbeat.interval为3秒

    • hdfs-site.xml配置文件中dfs.namenode.heartbeat.recheck.interval的单位为毫秒,dfs.heartbeat.interval的单位为秒

      [hdfs-default.xml]
      
      <property>
      	<name>dfs.namenode.heartbeat.recheck-interval</name>
          <value>300000</value>
      </property>
      
      <property>
      	<name>dfs.heartbeat.interval</name>
          <value>3</value>
      </property>
      

4、MapReduce

4.1 MapReduce概述

4.1.1 MapReduce定义

  • MapReduce是一个分布式运算程序的编程框架,是用户开发"基于Hadoop的数据分析应用"的核心框架
  • MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上

4.1.2 MapReduce的优缺点

  • 优点

    1. 易于编程,通过简单的实现一些接口,就可以完成一个分布式程序
    2. 良好的扩展性,通过简单的增加机器可以扩展它的计算能力
    3. 高容错性,能自动调度节点,替换非工作节点,保证高效完成计算任务
  • 缺点

    1. 不擅长实时计算

      MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。

    2. 不擅长流式计算

      流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。 这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。

    3. 不擅长DAG(有向无环图)计算

      多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下, MapReduce 并不是不能做,而是使用后,每个 MapReduce 作业的输出结果都会写入到磁盘, 会造成大量的磁盘 IO,导致性能非常的低下。

4.1.3 MapReduce核心思想

4.1.4 MapReduce进程

一个完整的MapReduce程序在分布式运行时有三类实例进程:

  1. MrAppMaster:负责整个程序的过程调度及状态协调
  2. MapTask:负责Map阶段的整个数据处理流程
  3. ReduceTask:负责Reduce阶段的整个数据处理流程

4.1.5 常用数据序列化类型

Java类型 Hadoop Writable类型
Boolean BooleanWritable
Byte ByteWritable
Int IntWritable
Float FloatWritable
Long LongWritable
Double DoubleWritable
String Text
Map MapWritable
Array ArrayWritable
Null NullWritable

4.1.6 MapReduce编程规范

用户编写的程序分成三个部分:Mapper、Reducer和Driver

  • Mapper阶段

    1. 用户自定义的Mapper要继承自己的父类
    2. Mapper的输入数据时<K,V>对的形式
      • 其中的K,是输入数据的偏移量
      • 其中的V,是该偏移量下的全部数据
    3. Mapper中的业务逻辑写在map()方法中
    4. Mapper的输出数据时<K,V>对的形式
    5. map()方法(MapTask进程)对每一个<K,V>调用一次
  • Reducer阶段

    1. 用户自定义的Reducer要继承自己的父类
    2. Reducer的输入数据类型对应Mapper的输出数据类型,即<K,V>对
    3. Reducer的业务逻辑写在reduce()方法中
    4. ReduceTask进程对每一组相同K的<K,V>组调用一次reduce()方法
  • Driver阶段

    相当于YARN集群的客户端,用于提交整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象

    1. 获取配置信息,获取job对象实例
    2. 指定本程序的jar包所在的本地路径
    3. 关联Mapper/Reducer业务类
    4. 指定Mapper输出数据的<K,V>类型
    5. 指定最终输出的数据的<K,V>类型
    6. 指定job的输入原始文件所在目录
    7. 指定job的输出结果所在目录(不能提前存在,否则会抛出异常)
    8. 提交作业

4.1.7 WordCount案例

4.1.7.1 环境准备

参考 3.6.1

4.1.7.2 编写程序

  • 编写Mapper类

    package 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> {
        // 创建输出的<K,V>
        private Text outK = new Text();
        IntWritable outV = new IntWritable(1); //map阶段不进行聚合,只是标记1
        @Override
        protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
            // 1.获取一行
            String line = value.toString();
            // 2.切割
            String[] words = line.split(" ");
            // 3.循环写出
            for (String word : words) {
                // 封装outK
                outK.set(word);
                // 将<outK,outV>放入context上下文管理器
                context.write(outK,outV);//context.write(Text var1, IntWritable var2)中的变量对应Mapper中的输出泛型
            }
        }
    }
    
  • 编写Reducer类

    package 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> {
        IntWritable outV = new IntWritable();
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable value : values) {
                sum += value.get();
            }
            outV.set(sum);
            context.write(key,outV);
        }
    }
    
    
  • 编写Driver驱动类

    package 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;
    
    public class WordCountDriver {
        public static void main(String[] args) throws Exception{
            // 1. 获取配置信息,获取job对象实例
            Job job = Job.getInstance(new Configuration());
            // 2. 指定本程序的jar包所在的本地路径
            job.setJarByClass(WordCountDriver.class);
            // 3. 关联Mapper/Reducer业务类
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
            // 4. 指定Mapper输出数据的<K,V>类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
            // 5. 指定最终输出的数据的<K,V>类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            // 6. 指定job的输入原始文件所在目录
            FileInputFormat.setInputPaths(job,new Path(args[0]));
            // 7. 指定job的输出结果所在目录(不能提前存在,否则会抛出异常)
            FileOutputFormat.setOutputPath(job,new Path(args[1]));
            // 8. 提交作业
            System.exit(job.waitForCompletion(true)?0:1);
    
        }
    }
    

4.1.7.3 在集群上测试

  • 添加maven打包插件依赖

        <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->Reimport 刷新即可

  • 将程序打包成jar包

  • 修改不带依赖的 jar 包名称为 wordcount.jar,并将jar包拷贝到集群中

  • 启动集群

  • 执行WordCount程序

    hadoop jar ./wordcount.jar MapReduce.WordCount.WordCountDriver /test /output
    

4.2 Hadoop序列化

4.2.1 序列化概述

  • 序列化

    把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁 盘(持久化)和网络传输

  • 反序列化

    将收到的字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换 成内存中的对象

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

具体实现bean对象序列化步骤有7步:

  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. 注意反序列化的顺序和序列化的顺序完全一致

  6. 当想要把结果显示在文件中时,需要重写toString(),可用"\t"分开

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

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

4.2.3 案例

  • 数据

    1       13736230513     192.196.100.1   www.atguigu.com 2481    24681   200
    2       13846544121     192.196.100.2                   264     0       200
    3       13956435636     192.196.100.3                   132     1512    200
    4       13966251146     192.168.100.1                   240     0       404
    5       18271575951     192.168.100.2   www.atguigu.com 1527    2106    200
    6       84188413        192.168.100.3   www.atguigu.com 4116    1432    200
    7       13590439668     192.168.100.4                   1116    954     200
    8       15910133277     192.168.100.5   www.hao123.com  3156    2936    200
    9       13729199489     192.168.100.6                   240     0       200
    10      13630577991     192.168.100.7   www.shouhu.com  6960    690     200
    11      15043685818     192.168.100.8   www.baidu.com   3659    3538    200
    12      15959002129     192.168.100.9   www.atguigu.com 1938    180     500
    13      13560439638     192.168.100.10                  918     4938    200
    14      13470253144     192.168.100.11                  180     180     200
    15      13682846555     192.168.100.12  www.qq.com      1938    2910    200
    16      13992314666     192.168.100.13  www.gaga.com    3008    3720    200
    17      13509468723     192.168.100.14  www.qinghua.com 7335    110349  404
    18      18390173782     192.168.100.15  www.sogou.com   9531    2412    200
    19      13975057813     192.168.100.16  www.baidu.com   11058   48243   200
    20      13768778790     192.168.100.17                  120     120     200
    21      13568436656     192.168.100.18  www.alibaba.com 2481    24681   200
    22      13568436656     192.168.100.19                  1116    954     200
    
  • 编写MapReduce程序

    1. 编写流量统计的Bean对象

      package 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 {
          // 1.定义属性
          private long upFlow;
          private long downFlow;
          private long sumFlow;
      
          // 2.提供无参构造
          public FlowBean(){
      
          }
          // 3.提供三个参数的getter()和setter()方法
          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(long sumFlow) {
              this.sumFlow = sumFlow;
          }
      
          public void setSumFlow(){
              this.sumFlow = this.upFlow+this.downFlow;
          }
      
          //
          @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;
          }
      }
      
    2. 编写Mapper类

      package 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> {
          // 1.声明输出对象
          Text outK = new Text();
          FlowBean outV = new FlowBean();
      
          @Override
          protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
              // 2.获取输入对象
              String lineString = value.toString();
              // 3.按条件切割字符串
              String[] splitStrings = lineString.split("\t");
              // 4.获取想要的数据
              String phone = splitStrings[1];
              String up = splitStrings[splitStrings.length-3];
              String down = splitStrings[splitStrings.length-3];
              // 5.封装数据为FlowBean
              outK.set(phone);
              outV.setUpFlow(Long.parseLong(up));
              outV.setDownFlow(Long.parseLong(down));
              outV.setSumFlow();
      
              // 7.写出数据
              context.write(outK,outV);
      
          }
      }
      
      
    3. 编写Reducer类

      package MapReduce.writable;
      
      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> {
          // 1.创建输出对象
          FlowBean outV = new FlowBean();
      
          @Override
          protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
              // 2.遍历输入对象values值
              long totalUp = 0;
              long totalDown = 0;
              for (FlowBean value : values) {
                  totalUp += value.getUpFlow();
                  totalDown += value.getDownFlow();
              }
              // 3.封装输出对象
              outV.setUpFlow(totalUp);
              outV.setDownFlow(totalDown);
              outV.setSumFlow();
      
              // 4.写出数据
              context.write(key,outV);
          }
      }
      
      
    4. 编写Driver类

      package 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;
      
      public class FlowDriver {
          public static void main(String[] args) throws Exception{
              // 1. 获取配置信息,获取job对象实例
              Job job = Job.getInstance(new Configuration());
              // 2. 指定本程序的jar包所在的本地路径
              job.setJarByClass(FlowDriver.class);
              // 3. 关联Mapper/Reducer业务类
              job.setMapperClass(FlowMapper.class);
              job.setReducerClass(FlowReducer.class);
              // 4. 指定Mapper输出数据的<K,V>类型
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(FlowBean.class);
              // 5. 指定最终输出的数据的<K,V>类型
              job.setOutputKeyCl  ass(Text.class);
              job.setOutputValueClass(FlowBean.class);
              // 6. 指定job的输入原始文件所在目录
              FileInputFormat.setInputPaths(job,new Path("/home/nuochengze/learnstation/test1"));
              // 7. 指定job的输出结果所在目录(不能提前存在,否则会抛出异常)
              FileOutputFormat.setOutputPath(job,new Path("/home/nuochengze/learnstation/out1"));
              // 8. 提交作业
              System.exit(job.waitForCompletion(true)?0:1);
          }
      }
      
      

4.3 MapReduce框架原理

整个阶段分为三部分:MapTask部分、Shuffle部分、ReduceTask部分

4.3.1 InputFormat数据输入

4.3.1.1 切片与MapTask并行度决定机制

  • 说明

    数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据的单位,由磁盘读写速度决定

    数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask

  • 机制

    1. 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
    2. 每一个Split切片分配一个MapTask并行实例处理
    3. 默认情况下,切片大小=BlockSize
    4. 切片时不考虑数据集(一个文件下的多个文件)整体,而是逐个针对每一个文件单独切片

4.3.1.2 Job提交流程源码解析

waitForCompletion(); // Driver最后阶段提交了任务

submit(); // 在waitForCompletion()方法中,通过submit进行任务提交

// submit()细节如下
// 1.建立连接
connect();
	// 1.1 创建提交Job的代理
	new Cluster(getConfiguration());
		// 1.1.1 判断是本地运行环境,还是yarn集群运行环境
		initialize(jobTrackAddr, conf);
            
// 2.提交Job
submitter.submitJobInternal(Job.this,cluster);
// 2.1 创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster,conf);
// 2.2 获取jobid,并创建Job路径
JobID jobId = submitClient.getNewJobId();
// 2.3 如果是在集群中运行,则拷贝jar包到集群
copyAndConfigureFiles(job,submitJobDir);
rUploader.uploadFiles(job,jobSubmitDir);
// 2.4 计算切片,生成切片规划文件job.split
writeSplits(job,submitJobDir);
	maps = writeNewSplits(job,jobSubmitDir);
	input.getSplits(job);
// 2.5 向Stag路径写job相关参数形成XML配置文件
writeConf(conf,submitJobFile);
conf.writeXml(out);
// 2.6 提交Job,返回提交状态,即生成_SUCCESS文件
status = submitClient.submitJob(jobId,submitJobDir.toString(),job.getCredentials());

4.3.1.3 FileInputFormat 切片源码解析

input.getSplits(job); // 切片片段

// 1.程序找到数据存储的目录
// 2.开始遍历处理(规划切片)目录下的每一个文件
// 3.遍历第一个文件 xx.txt
	// 3.1 获取文件大小:fs.size()
	// 3.2 计算切片大小
	computeSplitSize(Math.max(minSize.Math.min(maxSize,blocksize)))  = blocksize = 128M / 32M(程序在本地环境运行);
	// 3.3 默认情况下,切片的大小  等于 blocksize 的大小
	// 3.4 切片时,逐个文件切割,第一个切片:0~128M,第二个切片:128~256M,第三个切片:256M:300M。(在每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
	// 3.5 切片完成后,将切片的信息写到一个切片规划文件中
	// 3.6 整个切片的核心过程在getSplit()方法中完成
	// 3.7 InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等
// 4.提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数

4.3.1.4 FileInputFormat切片机制

  • 切片机制

    1. 按照文件的内容长度进行切片
    2. 切片大小,默认等于Block大小
    3. 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
  • 说明

    • 源码中计算切片大小的公式

      Math.max(minSzie,Math.min(maxSize,blockSize));
      // mapreduce.input.fileinputformat.split.minsize=1 默认值为1
      // mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
      // 因此,在默认情况下,切片的大小等于快大小
      
    • 切片大小设置

      maxszie(切片最大值):参数如果调得比blockSize小,则会让切片变小,等于调小了切片
      minsize(切片最大值):参数如果调得比blockSize大,则可以让切片变得比快大小大
      
    • 获取切片信息API

      String name = inputSplit.getPath().getName(); // 获取切片的文件名称
      FileSplit inputSPlit = (FileSplit) context.getInputSplit(); // 根据文件类型获取切片信息
      

4.3.1.5 FileInputFormat实现类

在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制文件、数据库表等。

针对不同的数据类型,FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等

4.3.1.6 TextInputFormat

  • 定义

    TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录,键是存储该行在整个文件中的起始字节偏移量,LongWritable类型;值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。

  • 示例

    • 一个分片包含了如下 4 条文本记录

      Rich learning form
      Intelligent learning engine
      Learning more convenient
      From the real demand for more close to the enterprise
      
    • 每条记录表示为以下键/值对

      (0,Rich learning form)
      (20,Intelligent learning engine)
      (49,Learning more convenient)
      (74,From the real demand for more close to the enterprise)
      

4.3.1.7 ComBineTextInputFormat

  • 定义

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

  • 切片机制

    生成切片过程包括:虚拟存储过程和切片过程2各部分。

    虚拟存储切片最大值设置:CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

    • 虚拟存储过程

      1. 将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分成一个块
      2. 如果输入文件 大于 设置的最大值且大于两倍,则以最大值切割成一块
      3. 当剩余数据大小超过设置的最大值且不大于最大值2倍时,将文件均分成2个虚拟存储块(防止出现太小切片)

      例如:setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储 文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件

    • 切片过程

      1. 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片
      2. 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片
  • 将CombineTextInputFormat设置为FileInputFormat,替换默认的TextInputFormat

    在Driver类中添加以下代码

    // 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
    job.setInputFormatClass(CombineTextInputFormat.class);
    //虚拟存储切片最大值设置 4m
    CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
    

4.3.2 MapReduce工作流程

4.3.3 Shuffle机制

Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle

4.3.3.1 Partition分区

  • 默认的Partition分区

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

    默认的分区是根据key的hashCode对ReduceTasks个数取模得到的,用户无法控制哪个kye存储到哪个分区

  • 自定义Partition步骤

    1. 自定义类继承Partitioner,重写getPartition()方法

      public class CustomPartitioner extends Partitioner<Text,FlowBean>{
          @Override
          public int getPartition(K key, V value,int numReduceTasks) {
          // 控制部分代码
          return partition;
        }
      }
      
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.Partitioner;
      
      public class CustomPartitioner extends Partitioner<Text,FlowBean> {
          @Override
          public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
              String subPhone = text.toString().substring(0, 3);
      
              int partition;
      
              if ("136".equals(subPhone)){
                  partition = 0;
              }else if("137".equals(subPhone)){
                  partition = 1;
      
              }else{
                  partition = 2;
              }
              return partition;
          }
      }
      
    2. 在Job驱动中,设置自定义Partitioner

      job.setPartitionerClass(CustomPatitioner.class);
      
    3. 自定义Partition后,要根据自定义Patitioner的逻辑设置相应数量的ReduceTask

      job.setNumReduceTasks(5);
      
  • 分区总结

    1. 如果ReduceTask的数量 > getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
    2. 如果 1 < ReduceTask的数量 < getPartition的结果数,则有一部分分区数据无处安放,产生Exception
    3. 如果 ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给了一个ReduceTask,最终也就只会产生一个结果part-r-00000
    4. 分区号必须从零开始,逐一累加

4.3.3.2 排序

MapTask和ReduceTask均会对数据按照key进行排序,该操作属于Hadoop的默认行为。

默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

  • 对于MapTask

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

  • 对于ReduceTask

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

  • 排序分类

    • 部分排序

      MapReduce根据输入记录的键对数据集排序,保证输出的每个文件内部有序

    • 全排序

      最终输出结果只有一个文件,且文件内部有序,实现方式是只设置一个ReduceTask。

    • 辅助排序:(GroupingComapraotr分组)

      在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部 字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序

    • 二次排序

      在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序

  • 自定义排序

    • 原理

      bean 对象做为 key 传输,需要实现 WritableComparable 接口重写 compareTo 方法,就可 以实现排序

          @Override
          public int compareTo(FlowBean o) {
              if(this.sumFlow > o.sumFlow){
                  return -1;
              }else if(this.sumFlow < o.sumFlow){
                  return 1;
              }else{
                  // 可在逻辑体中,嵌套二次排序的规则
                  return 0;
              }
          }
      
    • 示例代码

      // Bean对象
      package MapReduce.WritableComparable;
      
      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> {
          // 1.定义属性
          private long upFlow;
          private long downFlow;
          private long sumFlow;
      
          // 2.提供无参构造
          public FlowBean(){
      
          }
          // 3.提供三个参数的getter()和setter()方法
          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(long sumFlow) {
              this.sumFlow = sumFlow;
          }
      
          public void setSumFlow(){
              this.sumFlow = this.upFlow+this.downFlow;
          }
      
          //
          @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;
              }
          }
      }
      
      
      // Driver对象
      package MapReduce.WritableComparable;
      
      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;
      
      public class FlowDriver {
          public static void main(String[] args) throws Exception{
              // 1. 获取配置信息,获取job对象实例
              Job job = Job.getInstance(new Configuration());
              // 2. 指定本程序的jar包所在的本地路径
              job.setJarByClass(FlowDriver.class);
              // 3. 关联Mapper/Reducer业务类
              job.setMapperClass(FlowMapper.class);
              job.setReducerClass(FlowReducer.class);
              // 4. 指定Mapper输出数据的<K,V>类型
              job.setMapOutputKeyClass(FlowBean.class);
              job.setMapOutputValueClass(Text.class);
              // 5. 指定最终输出的数据的<K,V>类型
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(FlowBean.class);
              // 6. 指定job的输入原始文件所在目录
              FileInputFormat.setInputPaths(job,new Path("D:\\my_project\\learnstation\\test2"));
              // 7. 指定job的输出结果所在目录(不能提前存在,否则会抛出异常)
              FileOutputFormat.setOutputPath(job,new Path("D:\\my_project\\learnstation\\out2"));
              // 8. 提交作业
              System.exit(job.waitForCompletion(true)?0:1);
          }
      }
      
      
      // Mapper对象
      
      package MapReduce.WritableComparable;
      
      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> {
          // 1.声明输出对象
          private FlowBean outK = new FlowBean();
          private Text outV = new Text();
          @Override
          protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
              String[] lines = value.toString().split("\t");
              outK.setUpFlow(Long.parseLong(lines[1]));
              outK.setDownFlow(Long.parseLong(lines[2]));
              outK.setSumFlow();
              outV.set(lines[0]);
              context.write(outK,outV);
          }
      }
      
      
      // Reducer对象
      
      package MapReduce.WritableComparable;
      
      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> {
          @Override
          protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
              for (Text value : values) {
                  context.write(value,key);
              }
          }
      }
      
      
  • 区内排序(WritableComparable +CustomPartitioner)

    先完成分区,之后在分区内进行排序

4.3.3.3 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

    1. 自定义一个Combiner继承Reducer,重写Reduce方法

      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);
       }
      }
      
    2. 在Job驱动类中设置

      job.setCombinerClass(WordCountCombiner.class); // 当Combiner的逻辑和Reducer中的逻辑相似时,可替换为job.setCombinerClass(WordCountReducer.class)
      

4.3.4 OutputFormat数据输出

  • 说明

    OutputFormat是MapReduce输出的基类,所有实现了MapReduce的输出都实现了OutputFormat接口

  • 自定义OutputFormat

    • 自定义OutputFormat步骤

      1. 自定义一个类继承FileOutputFormat

      2. 重写getRecordWriter()方法

        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 {
                // RecordWriter类需要重写,并通过job进行关联
                LogRecordWriter logRecordWriter = new LogRecordWriter(job);
                return logRecordWriter;
            }
        }
        
      3. 创建logRecordWriter对象,继承RecordWriter

        1. 创建带参构造器,将job进行关联,创建对应的输出流
        2. 重写write方法,实现数据分流逻辑
        3. 重写close方法,关闭输出流
        
        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.Job;
        import org.apache.hadoop.mapreduce.RecordWriter;
        import org.apache.hadoop.mapreduce.TaskAttemptContext;
        
        import java.io.IOException;
        
        public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
            // 4.将输出对象声明为属性
            private FSDataOutputStream test_out;
            private FSDataOutputStream other_out;
            // 1.创建带参的构造方法
            public LogRecordWriter(TaskAttemptContext job){
                try {
                    // 2.获取job,创建文件系统对象
                    FileSystem fs = FileSystem.get(job.getConfiguration());
        
                    // 3.用文件系统对象创建两个输出流对应不同的目录
                    test_out = fs.create(new Path("D:\\my_project\\learnstation\\out3\\test.log"));
                    other_out = fs.create(new Path("D:\\my_project\\learnstation\\out3\\other.log"));
        
                } catch (IOException e) {
                    e.printStackTrace();
                }
        
            }
        
            @Override
            public void write(Text key, NullWritable value) throws IOException, InterruptedException {
                // 5.处理输出的key为字符串
                String log = key.toString();
        
                // 6.针对字符串内容进行处理,实现内容分流
                if (log.contains("test")){
                    test_out.writeBytes(log+"\n");
                }else{
                    other_out.writeBytes(log+"\n");
                }
            }
        
            @Override
            public void close(TaskAttemptContext context) throws IOException, InterruptedException {
                // 7.关流
                IOUtils.closeStream(test_out);
                IOUtils.closeStream(other_out);
            }
        }
        
        
      4. 关联OutputFormat类到Driver

        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, InterruptedException, ClassNotFoundException {
                Job job = Job.getInstance(new Configuration());
        
                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);
        
                // 1.设置自定义的OutputFormat
                job.setOutputFormatClass(LogOutputFormat.class);
        
                FileInputFormat.setInputPaths(job,new Path("D:\\my_project\\learnstation\\test3"));
                /* 虽 然 我 们 自 定 义 了 outputformat , 但是因为我们的 outputformat 继承自fileoutputformat,
                而 fileoutputformat 要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录*/
                FileOutputFormat.setOutputPath(job,new Path("D:\\my_project\\learnstation\\out3"));
        
                System.exit(job.waitForCompletion(true)?0:1);
        
            }
        }
        

4.3.5 MapReduce内核源码解析

4.3.5.1 MapTask工作机制

  1. Read阶段

    MapTask通过InputFormat获取的RecordReader,从输入InputSplit中解析出一个个key/value

  2. Map阶段

    该节点主要是将解析出的key/value交给用户编写的map()函数处理,并产生一系列新的key/value

  3. Collect收集阶段

    在用户编写的map()函数中,当数据处理完成后,一般会调用OutputCollecotr.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中

  4. Spill阶段

    溢写阶段,当环形缓冲区到达阈值后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。其将数据写入本地磁盘之前,先对数据进行一次本地排序,并在必要时对数据进行合并,压缩等操作。

    1. 利用快速排序算法对缓存区内的数据进行排序:先按照分区编号partition进行排序,然后按照key进行排序;数据在排序后,以分区为单位聚集在一起,且同一分区内所有数据按照key有序
    2. 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中,如果用户设置了Conbiner,则写入文件之前,对每个分区中的数据进行一次聚集操作
    3. 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中
  5. Merge阶段

    当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index,避免同时打开大量文件和同时读取大量 小文件产生的随机读取带来的开销。

    在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文 件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件

4.3.5.2 ReduceTask工作机制

  1. Copy阶段

    ReduceTask从各个MapTask上远程拷贝一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中

  2. Sort阶段

    在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。由于各个 MapTask 已经实现对自己的处理结果进行了 局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可

  3. Reduce阶段

    reduce()函数将计算结果写入到磁盘中

4.3.5.3 ReduceTask并行度决定机制

  • ReduceTask并行度(个数)

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

    // 默认值是 1,手动设置为 4
    job.setNumReduceTasks(4);
    
  • 注意事项

    1. ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。

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

    3. 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜

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

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

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

      // MapTask.java
      
      @SuppressWarnings("unchecked")
          OldOutputCollector(MapOutputCollector<K,V> collector, JobConf conf) {
            numPartitions = conf.getNumReduceTasks();
            if (numPartitions > 1) {
              partitioner = (Partitioner<K,V>)
                ReflectionUtils.newInstance(conf.getPartitionerClass(), conf);
            } else {
              partitioner = new Partitioner<K,V>() {
                @Override
                public void configure(JobConf job) { }
                @Override
                public int getPartition(K key, V value, int numPartitions) {
                  return numPartitions - 1;
                }
              };
            }
            this.collector = collector;
          }
      

4.3.6 Join的多种应用

  • order.txt文件

    1001	01	1
    1002	02	2
    1003	03	3
    1004	01	4
    1005	02	5
    1006	03	6
    
  • pd.txt文件

    01	小米
    02	华为
    03	格力
    

4.3.6.1 Reduce Join

  • 说明

    • Map端的主要工作

      为来自不同表或文件的key/value对,打标签以区别不同来源的记录,然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出

    • Reduce端的主要工作

      在Reduce端以连接字段作为key的分组已经完成,只需要在每一个分组中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就行了。

  • 总结

    • 缺点

      这种方式中,合并的操作是在 Reduce 阶段完成,Reduce 端的处理压力太大,Map 节点的运算负载则很低,资源利用率不高,且在 Reduce 阶段极易产生数据倾斜。

    • 解决方法

      Map 端实现数据合并

  • 示例

    • ReduceJoinBean.java

      import org.apache.hadoop.io.Writable;
      
      import java.io.DataInput;
      import java.io.DataOutput;
      import java.io.IOException;
      
      public class ReduceJoinBean implements Writable {
      
          // 1.通过两表字段,确定Bean属性
          private String id; //订单id
          private String pid; // 商品id
          private int amount; // 商品数量
          private String pname; // 商品名称
          private String flag; // 标记来源表
      
          // 2.空参构造
          public ReduceJoinBean() {
          }
          
          // 3.重写序列化方法
          @Override
          public void write(DataOutput out) throws IOException {
              out.writeUTF(id);
              out.writeUTF(pid);
              out.writeInt(amount);
              out.writeUTF(pname);
              out.writeUTF(flag);
      
          }
      
          // 4.重写反序列化方法
          @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();
          }
      
          // 5.重写toString方法
          @Override
          public String toString() {
              return id + "\t" + pname + "\t" + amount;
          }
      
          // 6.属性的getter和setter方法
          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;
          }
      }
      
      
    • ReduceJoinDriver.java

      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(ReduceJoinBean.class);
              job.setOutputKeyClass(ReduceJoinBean.class);
              job.setOutputValueClass(NullWritable.class);
      
              FileInputFormat.setInputPaths(job,new Path("D:\\my_project\\learnstation\\test4"));
              FileOutputFormat.setOutputPath(job,new Path("D:\\my_project\\learnstation\\out4"));
      
              System.exit(job.waitForCompletion(true)?0:1);
          }
      }
      
    • ReduceJoinMapper.java

      import org.apache.hadoop.io.LongWritable;
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.InputSplit;
      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, ReduceJoinBean> {
          private String fileName;
          private Text outK = new Text();
          private ReduceJoinBean outV = new ReduceJoinBean();
      
          @Override
          protected void setup(Mapper<LongWritable, Text, Text, ReduceJoinBean>.Context context) throws IOException, InterruptedException {
              // 1.初始化,当文件打开时,执行一次
              InputSplit inputSplit = context.getInputSplit();
              FileSplit split = (FileSplit) inputSplit;
              // 获取文件名称
              fileName = split.getPath().getName();
          }
      
          @Override
          protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, ReduceJoinBean>.Context context) throws IOException, InterruptedException {
              // 2.针对不同的表,进行不同的处理
              String line = value.toString();
              if (fileName.contains("order")) {
                  String[] lines = line.split("\t");
                  outK.set(lines[1]);
                  outV.setId(lines[0]);
                  outV.setPid(lines[1]);
                  outV.setAmount(Integer.parseInt(lines[2]));
                  outV.setPname("");
                  outV.setFlag("order");
      
              } else {
                  String[] lines = line.split("\t");
                  outK.set(lines[0]);
                  outV.setId("");
                  outV.setPid(lines[0]);
                  outV.setAmount(0);
                  outV.setPname(lines[1]);
                  outV.setFlag("pd");
              }
              context.write(outK, outV);
          }
      }
      
      
    • ReduceJoinReducer.java

      package 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, ReduceJoinBean, ReduceJoinBean, NullWritable> {
          @Override
          protected void reduce(Text key, Iterable<ReduceJoinBean> values, Reducer<Text, ReduceJoinBean, ReduceJoinBean, NullWritable>.Context context) throws IOException, InterruptedException  {
              ArrayList<ReduceJoinBean> orderBeans = new ArrayList<ReduceJoinBean>();
              ReduceJoinBean pdBean = new ReduceJoinBean();
      
              for (ReduceJoinBean value : values) {
                  if ("order".equals(value.getFlag())) {
                      ReduceJoinBean tempReduceJoinBean = new ReduceJoinBean();
                      try {
                          BeanUtils.copyProperties(tempReduceJoinBean, value);
                      } catch (IllegalAccessException e) {
                          e.printStackTrace();
                      } catch (InvocationTargetException e) {
                          e.printStackTrace();
                      }
                      orderBeans.add(tempReduceJoinBean);
                  } else {
                      try {
                          BeanUtils.copyProperties(pdBean, value);
                      } catch (IllegalAccessException | InvocationTargetException e) {
                          e.printStackTrace();
                      }
                  }
              }
      
              for (ReduceJoinBean orderBean : orderBeans) {
                  orderBean.setPname(pdBean.getPname());
                  context.write(orderBean, NullWritable.get());
              }
      
          }
      }
      
      

4.3..6.2 Map Join

  • 使用场景

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

  • 优点

    在 Reduce 端处理过多的表,非常容易产生数据倾斜,因此在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数据的压力,尽可能的减少数据倾斜

  • 步骤

    1. 在Mapper的setup阶段,将文件读取到缓存集合中

    2. 在Driver驱动类中加载缓存

      //缓存普通文件到 Task 运行节点。
      job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
      //如果是集群运行,需要设置 HDFS 路径
      job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
      
  • 示例

    • MapJoinDriver.java

      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 {
              Job job = Job.getInstance(new Configuration());
      
              job.setJarByClass(MapJoinDriver.class);
              job.setMapperClass(MapJoinMapper.class);
      
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(NullWritable.class);
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(NullWritable.class);
      
              // 加载缓存数据,需要具体到文件,否则会有报错
              job.addCacheFile(new URI("file:///D:/my_project/learnstation/cache/pd.txt"));
              // Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
              job.setNumReduceTasks(0);
      
              FileInputFormat.setInputPaths(job,new Path("D:\\my_project\\learnstation\\test5"));
              FileOutputFormat.setOutputPath(job,new Path("D:\\my_project\\learnstation\\out5"));
      
              System.exit(job.waitForCompletion(true)?0:1);
          }
      }
      
    • MapJoinMapper.java

      import com.ctc.wstx.util.StringUtil;
      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> {
          // 7.创建集合,存储从缓冲文件中读取出来的数据
          private HashMap<String, String> pdMap = new HashMap<String, String>();
      
          private Text outK = new Text();
      
      
          @Override
          protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
              // 1.获取缓存的文件(小文件),并把文件内容封装到集合
              URI[] cacheFiles = context.getCacheFiles();
      
              // 2.获取文件系统
              FileSystem fs = FileSystem.get(context.getConfiguration());
              // 3.打开缓存文件
              FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
      
              // 4.将数据加载到流
              BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
      
              // 5.对流数据进行处理
              String line;
              while(StringUtils.isNotEmpty(line = bufferedReader.readLine())){
                  // 6.切割读取出来的数据
                  String[] splits = line.split("\t");
      
                  // 8.将数据存储到集合
                  pdMap.put(splits[0],splits[1]);
              }
      
              // 9.关闭缓冲流
              IOUtils.closeStream(bufferedReader);
      
          }
      
          @Override
          protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
              // 10.处理大文件
              String[] splits = value.toString().split("\t");
      
              // 11.获取pname
              String pname = pdMap.get(splits[1]);
      
              // 12.封装数据
              outK.set(splits[0]+"\t"+pname+"\t"+splits[2]);
      
              context.write(outK,NullWritable.get());
      
          }
      }
      
      

4.3.7 数据清洗(ETL)

  • 定义

    ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取 (Extract)、转换(Transform)、加载(Load)至目的端的过程。

    在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户 要求的数据。清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序

4.4 Hadoop数据压缩

4.4.1 概述

  • 压缩的优缺点

    压缩的优点:减少磁盘IO,减少磁盘存储空间

    压缩的缺点:增加CPU开销

  • 压缩原则

    • 运算密集型的job,少用压缩
    • IO密集型的job,多用压缩

4.4.2 MR支持的压缩编码

  • 压缩算法对比介绍

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

    压缩算法 原始文件大小 压缩文件大小 压缩速度 解压速度
    gzip 8.3GB 1.8GB 17.5MB/s 58MB/s
    bzip2 8.3GB 1.1GB 2.4MB/s 9.5MB/s
    LZO 8.3GB 2.9GB 49.3MB/s 74.6MB/s

4.4.3 压缩方式选择

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

  • 压缩方式的优缺点

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

    压缩可以在 MapReduce 作用的任意阶段启用

4.4.4 压缩参数配置

  • 为了支持多种压缩/解压缩算法,Hadoop 引入了编码/解码器

    压缩格式 对应的编码/解码器
    DEFLATE org.apache.hadoop.io.compress.DefaultCodec
    gzip org.apache.hadoop.io.compress.GzipCodec
    bzip2 org.apache.hadoop.io.compress.BZip2Codec
    LZO com.hadoop.compression.lzo.LzopCodec
    Snappy com.hadoop.compression.lzo.LzopCodec
  • 要在 Hadoop 中启用压缩,可以配置如下参数

    参数 默认值 阶段 建议
    io.compression.codecs (在 core-site.xml 中配置) 无,这个需要在命令行输入 hadoop checknative 查看 输入压缩 Hadoop 使用文件扩展 名判断是否支持某种 编解码器
    mapreduce.map.output.compr ess(在 mapred-site.xml 中 配置) false mapper 输出 这个参数设为 true 启 用压缩
    mapreduce.map.output.compr ess.codec(在 mapred-site.xml 中配置) org.apache.hadoop.io.com press.DefaultCodec mapper 输出 企业多使用 LZO 或 Snappy 编解码器在此 阶段压缩数据
    mapreduce.output.fileoutpu tformat.compress(在 mapred-site.xml 中配置) false reducer 输出 这个参数设为 true 启用压缩
    mapreduce.output.fileoutpu tformat.compress.codec(在 mapred-site.xml 中配置) org.apache.hadoop.io.com press.DefaultCodec reducer 输出 使用标准工具或者编 解码器,如 gzip 和 bzip2

4.4.5 案例

  • Map输出端采用压缩

    • 修改Driver

      import java.io.IOException;
      
      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.io.compress.GzipCodec;
      import org.apache.hadoop.mapreduce.Job;
      import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
      import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
      
      public class WordCountDriver {
          public static void main(String[] args) throws IOException,
                  ClassNotFoundException, InterruptedException {
              Configuration conf = new Configuration();
      // 开启 map 端输出压缩
              conf.setBoolean("mapreduce.map.output.compress", true);
      // 设置 map 端输出压缩方式
              conf.setClass("mapreduce.map.output.compress.codec",
                      BZip2Codec.class, CompressionCodec.class);
              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]));
              boolean result = job.waitForCompletion(true);
              System.exit(result ? 0 : 1);
          }
      }
      
      
    • Mapper和Reducer保持不变

  • Reduce 输出端采用压缩

    • 修改Driver

      import java.io.IOException;
      
      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.DefaultCodec;
      import org.apache.hadoop.io.compress.GzipCodec;
      import org.apache.hadoop.io.compress.Lz4Codec;
      import org.apache.hadoop.io.compress.SnappyCodec;
      import org.apache.hadoop.mapreduce.Job;
      import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
      import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
      
      public class WordCountDriver {
          public static void main(String[] args) throws IOException,
                  ClassNotFoundException, InterruptedException {
              Configuration conf = new Configuration();
              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]));
              // 设置 reduce 端输出压缩开启
              FileOutputFormat.setCompressOutput(job, true);
              // 设置压缩的方式
              FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
              // FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class); 
              // FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
      
              boolean result = job.waitForCompletion(true);
              System.exit(result ? 0 : 1);
          }
      }
      
    • Mapper和Reducer保持不变

5、Yarn

  • 定义

    Yarn 是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式 的操作系统平台,而 MapReduce 等运算程序则相当于运行于操作系统之上的应用程序

5.1 Yarn基础架构

YARN 主要由 ResourceManager、NodeManager、ApplicationMaster 和 Container 等组件 构成

5.2 Yarn工作机制

5.3 作业提交流程

  • 说明
    1. YARN 中的任务将其进度和状态(包括 counter)返回给应用管理器, 客户端每秒(通过 mapreduce.client.progressmonitor.pollinterval 设置)向应用管理器请求进度更新, 展示给用户
    2. 除了向应用管理器请求作业进度外, 客户端每 5 秒都会通过调用 waitForCompletion()来 检查作业是否完成。时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业 完成之后, 应用管理器和 Container 会清理工作状态。作业的信息会被作业历史服务器存储 以备之后用户核查

5.4 Yarn调度器和调度算法

Hadoop 作业调度器主要有三种:FIFO、容量(Capacity Scheduler)和公平(Fair Scheduler)

yarn-default.xml 文件

<property>
 <description>The class to use as the resource scheduler.</description>
 <name>yarn.resourcemanager.scheduler.class</name>
<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
</property>


5.5 Yarn常用命令

5.5.1 yarn application 查看任务

  • 列出所有的Application

    yarn application -list
    
  • 根据 Application 状态过滤:yarn application -list -appStates (所有状态:ALL、NEW、 NEW_SAVING、SUBMITTED、ACCEPTED、RUNNING、FINISHED、FAILED、KILLED)

    yarn application -list -appStates FINISHED
    
  • Kill掉Application

    yarn application -kill <Application-Id>
    

5.5.2 yarn logs查看日志

  • 查看Application日志

    yarn logs -applicationId <Application-Id>
    
  • 查看Container日志

    yarn logs -applicationId <Application-Id> -containerId <Container-Id>
    

5.5.3 yarn applicationattempt 查看尝试运行的任务

  • 列出所有 Application 尝试的列表

    yarn applicationattempt -list <Application-Id>
    
  • 打印 ApplicationAttemp 状态

    yarn applicationattempt -status <ApplicationAttemptId>
    

5.5.4 yarn container查看容器

  • 列出所有Container

    yarn container -list <ApplicationAttemptId>
    
  • 打印Container状态

    yarn container -status <ContainerId>
    

    说明:只有在任务跑的途中才能看到 container 的状态

5.5.5 yarn node查看节点状态

  • 列出所有节点:

    yarn node -list -all
    

5.5.6 yarn rmadmin 更新配置

  • 加载队列配置

    yarn rmadmin -refreshQueues
    

5.5.7 yarn queue 查看队列

  • 打印队列信息

    yarn queue -status <QueueName>
    

5.6 Yarn生产环境核心参数

5.6.1 Yarn生产环境核心参数配置

  • yarn-site.xml配置参数

    如果集群的硬件资源不一致,要每个 NodeManager 单独配置

    <property>
        <!-- 选择调度器,默认容量 -->
        <name>yarn.resourcemanager.scheduler.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value>
    </property>
    
    <property>
        <!-- ResourceManager 处理调度器请求的线程数量,默认 50;如果提交的任务数大于 50,可以增加该值,但是不能超过 3 台 * 4 线程 = 12 线程(去除其他应用程序实际不能超过 8) -->
        <name>yarn.resourcemanager.scheduler.client.thread-count</name>
        <value>8</value>
    </property>
    
    <property>
        <!-- 是否让 yarn 自动检测硬件进行配置,默认是 false,如果该节点有很多其他应用程序,建议手动配置。如果该节点没有其他应用程序,可以采用自动 -->
        <name>yarn.nodemanager.resource.detect-hardware-capabilities</name>
        <value>false</value>
    </property>
    
    <property>
        <!-- 是否将虚拟核数当作 CPU 核数,默认是 false,采用物理 CPU 核数 -->
        <name>yarn.nodemanager.resource.count-logical-processors-as-cores</name>
        <value>false</value>
    </property>
    
    <property>
        <!-- 虚拟核数和物理核数乘数,默认是 1.0 -->
        <name>yarn.nodemanager.resource.pcores-vcores-multiplier</name>
        <value>1.0</value>
    </property>
    
    <property>
        <!-- NodeManager 使用内存数,默认 8G,修改为 4G 内存 -->
        <name>yarn.nodemanager.resource.memory-mb</name>
        <value>4096</value>
    </property>
    
    
    <property>
        <!-- nodemanager 的 CPU 核数,不按照硬件环境自动设定时默认是 8 个,修改为 4 个 -->
        <name>yarn.nodemanager.resource.cpu-vcores</name>
        <value>4</value>
    </property>
    
    <property>
        <!-- 容器最小内存,默认 1G -->
        <name>yarn.scheduler.minimum-allocation-mb</name>
        <value>1024</value>
    </property>
    
    <property>
        <!-- 容器最大内存,默认 8G,修改为 2G -->
        <name>yarn.scheduler.maximum-allocation-mb</name>
        <value>2048</value>
    </property>
    
    <property>
        <!-- 容器最小 CPU 核数,默认 1 个 -->
        <name>yarn.scheduler.minimum-allocation-vcores</name>
        <value>1</value>
    </property>
    
    <property>
        <!-- 容器最大 CPU 核数,默认 4 个,修改为 2 个 -->
        <name>yarn.scheduler.maximum-allocation-vcores</name>
        <value>2</value>
    </property>
    
    
    <property>
        <!-- 虚拟内存检查,默认打开,修改为关闭 -->
        <name>yarn.nodemanager.vmem-check-enabled</name>
        <value>false</value>
    </property>
    
    <property>
        <!-- 虚拟内存和物理内存设置比例,默认 2.1 -->
        <name>yarn.nodemanager.vmem-pmem-ratio</name>
        <value>2.1</value>
    </property>
    

5.6.2 配置多队列的容量调度器

  1. 在 capacity-scheduler.xml 中修改配置如下:

    <property>
        <!-- 指定多队列,增加 hive 队列 -->
        <name>yarn.scheduler.capacity.root.queues</name>
        <value>default,hive</value>
    </property>
            
    <property>
        <!-- 降低 default 队列资源额定容量为 40%,默认 100% -->
        <name>yarn.scheduler.capacity.root.default.capacity</name>
        <value>40</value>
    </property>
            
    <property>
        <!-- 降低 default 队列资源最大容量为 60%,默认 100% -->
        <name>yarn.scheduler.capacity.root.default.maximum-capacity</name>
        <value>60</value>
    </property>
    
  2. 为新加队列添加必要属性

    
    <property>
        <!-- 指定 hive 队列的资源额定容量 -->
        <name>yarn.scheduler.capacity.root.hive.capacity</name>
        <value>60</value>
    </property>
    
    <property>
    <!-- 指定 hive 队列的资源最大容量 -->
    <name>yarn.scheduler.capacity.root.hive.maximum-capacity</name>
    <value>80</value>
    </property>
    
    <property>
        <!-- 用户最多可以使用队列多少资源,1 表示100% -->
        <name>yarn.scheduler.capacity.root.hive.user-limit-factor</name>
        <value>1</value>
    </property>
    
    <property>
        <!-- 启动 hive 队列 -->
        <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>
    
    <property>
        <!-- 任务的超时时间设置:yarn application -appId appId -updateLifetime Timeout
               参考资料: https://blog.cloudera.com/enforcing-application-lifetime-slas-yarn/ -->
        <!-- 如果 application 指定了超时时间,则提交到该队列的 application 能够指定的最大超时
        时间不能超过该值。
        -->
        <name>yarn.scheduler.capacity.root.hive.maximum-application-lifetime</name>
        <value>-1</value>
    </property>
    
    <property>
        <!-- 如果 application 没指定超时时间,则用 default-application-lifetime 作为默认
        值 -->
        <name>yarn.scheduler.capacity.root.hive.default-application-lifetime</name>
        <value>-1</value>
    </property>
    
  3. 重启Yarn或执行队列刷新命令

    yarn rmadmin -refreshQueues
    
  4. 向Hive队列提交任务

    hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar wordcount -D 
    mapreduce.job.queuename=hive /input /output
    

    说明: -D 表示运行时改变参数值

  5. 打 jar 包的方式

    默认的任务提交都是提交到 default 队列的。如果希望向其他队列提交任务,需要在 Driver 中声明

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

    容量调度器,支持任务优先级的配置,在资源紧张时,优先级高的任务将优先获取资源。 默认情况,Yarn 将所有任务的优先级限制为 0,若想使用任务的优先级功能,须开放该限制

    • 修改yarn-site.xml文件

      <property>
       <name>yarn.cluster.max-application-priority</name>
       <value>5</value>
      </property>
      
    • 提交任务

      hadoop jar /opt/module/hadoop3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.3.jar pi -D mapreduce.job.priority=5 5 2000000
      
    • 也可以通过以下命令修改正在执行的任务的优先级

      yarn application -appID <ApplicationID> -updatePriority 优先级
          
      示例:
      yarn application -appID application_1611133087930_0009 -updatePriority 5
      

5.6.3 配置多队列的公平调度器

  • 说明

  • 配置

    1. 修改yarn-site.xml文件,加入以下参数

      <property>
          <name>yarn.resourcemanager.scheduler.class</name>
          <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</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 nuochengze 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>
          <!-- 增加一个队列 nuochengze -->
          <queue name="nuochengze" 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>
      

5.6.4 Tool接口

  • 说明

    yarn jar YarnDemo.jar WordCountDriver -D mapreduce.job.queuename=root.test /input /output1
    

    自己打包生成的wordCount.jar包在集群中通过命令行运行时,结果报错,误认 -D mapreduce.job.queuename=root.tes为第一个参数,不能动态传参,实现Tool接口即可解决

  • 案例

    1. 创建类 WordCount 并实现 Tool 接口

      package 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);
              }
          }
      }
      
      
    2. 新建 WordCountDriver

      package 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);
          }
      }
      
    3. 提交到集群

      yarn jar YarnDemo.jar WordCountDriver wordcount /input /output
      

      注意此时提交的 3 个参数,第一个用于生成特定的 Tool,第二个和第三个为输入输出目 录。此时如果我们希望加入设置参数,可以在 wordcount 后面添加参数

      yarn jar YarnDemo.jar WordCountDriver wordcount -D mapreduce.job.queuename=root.test /input /output1
      

6、生产调优

6.1 HDFS-核心参数

6.1.1 NameNode内存生产配置

  • 查看节点Node的内存占用情况

    jmap -heap <进程号>
    
  • 参考经验

    Hardware Requirements | 6.x | Cloudera Documentation

  • Hadoop2.x,配置NameNode内存

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

    HADOOP_NAMENODE_OPTS=-Xmx3072m
    
  • Hadoop3.x,配置NameNode和DataNode内存

    export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS -Xmx1024m"
    export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS -Xmx1024m"
    

6.1.2 NameNode心跳并发配置

  • 说明

    NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作,其默认值为10

  • 自定义工作线程池数量

    企业经验:dfs.namenode.handler.count=20 ×𝑙𝑜𝑔_e^Cluster Size,比如集群规模(DataNode 台 数)为 3 台时,此参数设置为 21。

    编辑hdfs-site.xml

    <property>
     <!-- NameNode工作线程池数量-->
     <name>dfs.namenode.handler.count</name>
     <value>21</value>
    </property>
    

6.1.3 开启回收站配置

开启回收站功能,可以将删除的文件在不超时的情况下,恢复原数据,起到防止误删除、 备份等作用

  • 回收站工作机制

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

    修改core-site.xml

    <property>
     <!-- 配置垃圾回收时间为 1 分钟-->
     <name>fs.trash.interval</name>
     <value>1</value>
    </property>
    
  • 说明

    • 通过网页上直接删除的文件不会走回收站

    • 通过程序删除的文件,默认不会走回收站,需要调用moveToTrash()才进入回收站

      Trash trash = new Trash(conf);
      trash.moveToTrash(path);
      
    • 在命令行利用 hadoop fs -rm 命令删除的文件才会走回收站

    • 恢复回收站数据的方式:从回收站路径,将文件移动回源目录

6.2 HDFS-集群压测

HDFS 的读写性能主要受网络磁盘影响比较大

6.2.1 测试 HDFS 写性能

  • 写测试底层原理

  • 测试内容:向 HDFS 集群写 5 个 128M 的文件

    hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -write -nrFiles 5 -fileSize 128MB
    
    • 运行结果

    • nrFiles n 为生成 mapTask 的数量,生产环境一般可通过 hadoop103:8088 查看 CPU 核数,设置为(CPU 核数 - 1)

6.2.2 测试HDFS写性能

  • 测试内容:向 HDFS 集群写 5 个 128M 的文件

    hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -read -nrFiles 5 -fileSize 128MB
    
  • 运行结果

6.3 HDFS-多目录

6.3.1 NameNode多目录配置

NameNode 的本地目录可以配置成多个,且每个目录存放内容相同,增加了可靠性

  • 修改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>
    
    • 修改完NameNode参数后,需要删除三台节点的 data 和 logs 中所有数据后,格式化处理

      hdfs namenode -format
      

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

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

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可 以执行磁盘数据均衡命令。(Hadoop3.x 新特性)

  1. 生成均衡计划

    hdfs diskbalancer -plan <hostname>
    
  2. 执行均衡计划

    hdfs diskbalancer -execute <hostname.plan.json>
    
  3. 查看当前均衡任务的执行情况

    hdfs diskbalancer -query <hostname>
    
  4. 取消均衡任务

    hdfs diskbalancer -cancel <hostname.plan.json>
    

6.4 HDFS-集群扩容及缩容

6.4.1 添加白名单

  • 白名单

    • 表示在白名单的主机 IP 地址可以,用来存储数据。

    • 不在白名单中,仍然可以和集群通信,只是数据不再存储在该节点中

  • 步骤

    1. 在NameNode节点的$HADOOP_HOME/etc/hadoop目录中分别创建whitelist和blacklist文件,往其中添加目标host

    2. 在hdfs-site.xml配置文件中增加dfs.hosts配置参数

      <!-- 白名单 -->
      <property>
       <name>dfs.hosts</name>
       <value>$HADOOP_HOME/etc/hadoop/whitelist</value>
      </property>
      <!-- 黑名单 -->
      <property>
       <name>dfs.hosts.exclude</name>
       <value>$HADOOP_HOME/etc/hadoop/blacklist</value>
      </property>
      
    3. 将文件分发给集群节点

    4. 第一次添加黑白名单必须重启集群,之后只需要刷新NameNode节点即可

      hdfs dfsadmin -refreshNodes 
      

6.4.2 服役新服务器

  1. 直接启动DataNode,即可关联到集群

    hdfs --daemon start datanode
    yarn --daemon start nodemanager
    
  2. 在白名单中增加新服役的服务器

  3. 刷新节点

    hdfs dfsadmin -refreshNodes
    

6.4.3 服务器间数据均衡

由于本地性原则,本地节点通常比远端节点的数据量多,时间过久会造成数据不均衡

  • 开启数据均衡命令

    $HADOOP_HOME/sbin/start-balancer.sh -threshold 10
    

    对于参数 10,代表的是集群中各个节点的磁盘空间利用率相差不超过 10%,可根据实 际情况进行调整

  • 停止数据均衡命令

    $HADOOP_HOME/sbin/stop-balancer.sh
    

6.4.4 黑名单退役服务器

  • 黑名单

    在黑名单的主机 IP 地址不可以,用来存储数据

  • 步骤

    1. 编辑$HADOOP_HOME/etc/hadoop 目录下的 blacklist 文件,添加待退役节点host

    2. 在 hdfs-site.xml 配置文件中增加 dfs.hosts 配置参数

      <!-- 黑名单 -->
      <property>
       <name>dfs.hosts.exclude</name>
       <value>$HADOOP_HOME/etc/hadoop/blacklist</value>
      </property>
      
      
    3. 第一次添加黑名单必须重启集群,不是第一次,只需要刷新 NameNode 节点即可

      hdfs dfsadmin -refreshNodes
      
    4. 检查 Web 浏览器,退役节点的状态为 decommission in progress(退役中),说明数据 节点正在复制块到其他节点

    5. 等待退役节点状态为 decommissioned(所有块已经复制完成),停止该节点及节点资源 管理器。注意:如果副本数是 3,服役的节点小于等于 3,是不能退役成功的,需要修改 副本数后才能退役

    6. 如果数据不均衡,可以用命令实现集群的再平衡

      $HADOOP_HOME/sbin/start-balancer.sh -threshold 10
      

6.5 HDFS-存储优化

6.5.1 纠删码

  • 纠删码原理

    HDFS 默认情况下,一个文件有 3 个副本,这样提高了数据的可靠性,但也带来了 2 倍 的冗余开销。Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约 50%左右的存储空间,但是回损耗计算资源。

  • 纠删码操作相关命令

    hdfs ec
    
  • 查看当前支持的纠删码策略

    hdfs ec -listPolicies
    

    • RS-3-2-1024k:使用 RS 编码,每 3 个数据单元,生成 2 个校验单元,共 5 个单元,也 就是说:这 5 个单元中,只要有任意的 3 个单元存在(不管是数据单元还是校验单元,只要总数=3),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576,即最小单位为1MB
    • RS-10-4-1024k:使用 RS 编码,每 10 个数据单元(cell),生成 4 个校验单元,共 14 个单元,也就是说:这 14 个单元中,只要有任意的 10 个单元存在(不管是数据单元还是校 验单元,只要总数=10),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=1048576
    • RS-6-3-1024k:使用 RS 编码,每 6 个数据单元,生成 3 个校验单元,共 9 个单元,也 就是说:这 9 个单元中,只要有任意的 6 个单元存在(不管是数据单元还是校验单元,只要 总数=6),就可以得到原始数据。每个单元的大小是 1024k=1024*1024=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 策略的支持,如要使用别的策略需要提前启用

      1. 开启对 RS-3-2-1024k 策略的支持

        hdfs ec -enablePolicy -policy RS-3-2-1024K
        
      2. 在 HDFS 创建目录,并设置 RS-3-2-1024k 策略

        hdfs dfs -mkdir /input
        hdfs ec -setPolicy -path /input -policy RS-3-2-1024K
        
      3. 数据节点存放的数据可正常显示,校验单元节点的数据显示乱码

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

异构存储主要解决,不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题

6.5.2.1 异构存储shell操作

  • 查看可用存储策略

    hdfs storagepolicies -listPolicies
    

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

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

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

    hdfs storagepolicies -unsetStoragePolicy -path xxx
    
  • 查看文件块的分布

    hdfs fsck /input -files -blocks -locations
    
  • 查看集群节点

    hadoop dfsadmin -report
    

6.5.2.2 案例测试

  1. 测试环境

    服务器规模:4台

    集群配置:副本数为2,创建好带有存储类型的目录

    集群规划:

    节点 存储类型分配
    pc001 RAM_DISK,SSD
    pc002 SSD,DISK
    pc003 DISK,RAM_DISK
    pc004 ARCHIVE
  2. 配置节点信息

    1. 在pc001节点的 hdfs-site.xml 添加如下信息

      <property>
      	<name>dfs.replication</name>
      	<value>2</value>
      </property>
      <property>
      	<name>dfs.storage.policy.enabled</name>
      	<value>true</value>
      </property>
      <property>
      	<name>dfs.datanode.data.dir</name>
      	<value>[SSD]file:///opt/software/hadoop-3.1.3/hdfsdata/ssd,[RAM_DISK]file:///opt/software/hadoop-3.1.3/hdfsdata/ram_disk</value>
      </property>
      
    2. 在pc002节点的 hdfs-site.xml 添加如下信息

      <property>
      	<name>dfs.replication</name>
      	<value>2</value>
      </property>
      <property>
      	<name>dfs.storage.policy.enabled</name>
      	<value>true</value>
      </property>
      <property>
      	<name>dfs.datanode.data.dir</name>
      	<value>[SSD]file:///opt/software/hadoop-3.1.3/hdfsdata/ssd,[DISK]file:///opt/software/hadoop-3.1.3/hdfsdata/disk</value>
      </property>
      
    3. 在pc003节点的 hdfs-site.xml 添加如下信息

      <property>
      	<name>dfs.replication</name>
      	<value>2</value>
      </property>
      <property>
      	<name>dfs.storage.policy.enabled</name>
      	<value>true</value>
      </property>
      <property>
      	<name>dfs.datanode.data.dir</name>
      	<value>[RAM_DISK]file:///opt/software/hadoop-3.1.3/hdfsdata/ram_disk,[DISK]file:///opt/software/hadoop-3.1.3/hdfsdata/disk</value>
      </property>
      
    4. 在pc004节点的 hdfs-site.xml 添加如下信息

      <property>
      	<name>dfs.replication</name>
      	<value>1</value>
      </property>
      <property>
      	<name>dfs.storage.policy.enabled</name>
      	<value>true</value>
      </property>
      <property>
      	<name>dfs.datanode.data.dir</name>
      	<value>[ARCHIVE]file:///opt/software/hadoop-3.1.3/hdfsdata/archive</value>
      </property>
      
    5. 启动集群,创建文件目录,上传文件

      xhadoop start
      hdfs dfs -mkdir /input
      hdfs dfs -put $HADOOP_HOME/LICENSE.txt /input
      
  3. HOT存储策略

    未设置存储策略,所有文件块都存储在 DISK 下。所以,默认存储策略为 HOT

    • 查看上传的文件块分布

      hdfs fsck /input -files -blocks -locations
      

    • 获取该目录的存储策略

      hdfs storagepolicies -getStoragePolicy -path /input
      

  4. WARM 存储策略测试

    • 修改存储策略

      hdfs storagepolicies -setStoragePolicy -path /input -policy WARM
      

    • 获取该目录的存储策略

      hdfs storagepolicies -getStoragePolicy -path /input
      

    • 改变存储策略后,文件快不会自动归档,需要手动处理

      hdfs mover /input
      
    • 再次查看文件块分布

      hdfs fsck /input -files -blocks -locations
      

  5. COLD 策略测试

    当我们将目录设置为 COLD 并且我们未配置 ARCHIVE 存储目录的情况下,不可以向该目录直接上传文件,会报出异常

    hdfs storagepolicies -setStoragePolicy -path /input -policy COLD
    hdfs mover /input
    hdfs fsck /input -files -blocks -locations
    
  6. ONE_SSD 策略测试

    hdfs storagepolicies -setStoragePolicy -path /input -policy ONE_SSD
    hdfs mover /input
    hdfs fsck /input -files -blocks -locations
    
  7. ALL_SSD 策略测试

    hdfs storagepolicies -setStoragePolicy -path /input -policy ALL_SSD 
    hdfs mover /input
    hdfs fsck /input -files -blocks -locations
    
  8. LAZY_PERSIST 策略测试

    hdfs storagepolicies -setStoragePolicy -path /input -policy LAZY_PERSIST 
    hdfs mover /input
    hdfs fsck /input -files -blocks -locations
    

    发现所有的文件块都是存储在 DISK,按照理论一个副本存储在 RAM_DISK, 其他副本存储在 DISK 中,这是因为,我们还需要配置“dfs.datanode.max.locked.memory”, “dfs.block.size”参数。

    出现存储策略为 LAZY_PERSIST 时,文件块副本都存储在 DISK 上的原因有如下两 点:

    1. 当客户端所在的 DataNode 节点没有 RAM_DISK 时,则会写入客户端所在的 DataNode 节点的 DISK 磁盘,其余副本会写入其他节点的 DISK 磁盘
    2. 当客户端所在的 DataNode 有 RAM_DISK,但“dfs.datanode.max.locked.memory” 参数值未设置或者设置过小(小于“dfs.block.size”参数值)时,则会写入客户端所在的 DataNode 节点的 DISK 磁盘,其余副本会写入其他节点的 DISK 磁盘(虚拟机的“max locked memory”为 64KB,所以,如果参数配置过大,还会报 出错误)

6.6 故障排除

6.6.1 集群安全模式

  • 安全模式

    文件系统只接受读数据请求,而不接受删除、修改等变更请求

  • 进入安全模式场景

    • NameNode 在加载镜像文件和编辑日志期间处于安全模式
    • NameNode 再接收 DataNode 注册时,处于安全模式
  • 退出安全模式条件

    • dfs.namenode.safemode.min.datanodes:最小可用 datanode 数量,默认 0
    • dfs.namenode.safemode.threshold-pct:副本数达到最小要求的 block 占系统总 block 数的 百分比,默认 0.999f。(只允许丢一个块)
    • dfs.namenode.safemode.extension:稳定时间,默认值 30000 毫秒,即 30 秒
  • 基本语法

    集群处于安全模式,不能执行重要操作(写操作)。集群启动完成30秒后,自动退出安全模 式

    (1)bin/hdfs dfsadmin -safemode get (功能描述:查看安全模式状态)
    (2)bin/hdfs dfsadmin -safemode enter (功能描述:进入安全模式状态)
    (3)bin/hdfs dfsadmin -safemode leave(功能描述:离开安全模式状态)
    (4)bin/hdfs dfsadmin -safemode wait (功能描述:等待安全模式状态)
    

6.6.2 小文件归档

  • HDFS 存储小文件弊端

    每个文件均按块存储,每个块的元数据存储在 NameNode 的内存中,因此 HDFS 存储 小文件会非常低效。因为大量的小文件会耗尽 NameNode 中的大部分内存。但注意,存储小文件所需要的磁盘容量和数据块的大小无关

    HDFS 上每个文件都要在 NameNode 上创建对应的元数据,这个元数据的大小约为 150byte,这样当小文件比较多的时候,就会产生很多的元数据文件,一方面会大量占用 NameNode 的内存空间,另一方面就是元数据文件过多,使得寻址索引速度变慢

    小文件过多,在进行 MR 计算时,会生成过多切片,需要启动过多的 MapTask。每个 MapTask 处理的数据量小,导致 MapTask 的处理时间比启动时间还小,白白消耗资源。

  • 解决存储小文件办法之一(存储方向)

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

    1. 把/test 目录里面的所有文件归档成一个叫 test.har 的归档文件,并把归档后文件存储 到/ 路径下

      hadoop archive -archiveName test.har -p /test /
      
    2. 查看归档

      hadoop fs -ls har:///test.har
      

    3. 查看归档里面的文件

      hadoop fs -ls har:///test.har/word.txt
      

    4. 解归档文件

      hadoop fs -cp har:///test.har/* /output
      
      • 输出目录需要存在
  • 解决存储小文件办法之一(计算方向)

    CombineTextInputFormat 用于将多个小文件在切片过程中生成一个单独的切片或者少 量的切片

  • 解决存储小文件办法之一:开启 uber 模式,实现 JVM 重用(计算方向)

    默认情况下,每个 Task 任务都需要启动一个 JVM 来运行,如果 Task 任务计算的数据 量很小,我们可以让同一个 Job 的多个 Task 运行在一个 JVM 中,不必为每个 Task 都开启 一个 JVM

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

6.7 MapReduce优化经验

6.7.1 MapReduce跑的慢的原因

  • 计算机性能

    CPU、内存、磁盘、网络

  • I/O 操作优化

    • 数据倾斜
    • Map 运行时间太长,导致 Reduce 等待过久
    • 小文件过多

6.7.2 MapReduce常用调优参数

  • 针对Map阶段和Shuffle阶段

  • 针对Reduce阶段

6.7.3 MapReduce数据倾斜问题

  • 数据倾斜现象

    数据频率倾斜——某一个区域的数据量要远远大于其他区域

    数据大小倾斜——部分记录的大小远远大于平均值

  • 减少数据倾斜的方法

    1. 首先检查是否空值过多造成的数据倾斜

      可以直接过滤掉空值;如果想保留空值,就自定义分区,将空值加随机数打散。最后再二次聚合

    2. 能在 map 阶段提前处理,最好先在 Map 阶段处理。如:Combiner、MapJoin

    3. 设置多个 reduce 个数

6.8 场景案例

6.8.1 需求

  1. 需求

    从 1G 数据中,统计每个单词出现次数。服务器 3 台,每台配置 4G 内存, 4 核 CPU,4 线程

  2. 需求分析

    1G / 128m = 8 个 MapTask;1 个 ReduceTask;1 个 mrAppMaster

    平均每个节点运行 10 个 / 3 台 ≈ 3 个任务(4 3 3)

6.8.2 HDFS 参数调优

  • 修改:hadoop-env.sh

    export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS -Xmx1024m"
    export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS -Xmx1024m"
    
  • 修改 hdfs-site.xml

    <!-- NameNode 有一个工作线程池,默认值是 10 -->
    <property>
     <name>dfs.namenode.handler.count</name>
     <value>21</value>
    </property>
    
  • 修改 core-site.xml

    <!-- 配置垃圾回收时间为 60 分钟 -->
    <property>
     <name>fs.trash.interval</name>
    <value>60</value>
    </property>
    

6.8.3 MapReduce调优

  • 修改 mapred-site.xml

    <!-- 环形缓冲区大小,默认 100m -->
    <property>
     	<name>mapreduce.task.io.sort.mb</name>
     	<value>100</value>
    </property>
    
    <!-- 环形缓冲区溢写阈值,默认 0.8 -->
    <property>
     	<name>mapreduce.map.sort.spill.percent</name>
     	<value>0.80</value>
    </property>
    
    <!-- merge 合并次数,默认 10 个 -->
    <property>
     	<name>mapreduce.task.io.sort.factor</name>
     	<value>10</value>
    </property>
    
    <!-- maptask 内存,默认 1g; maptask 堆内存大小默认和该值大小一致mapreduce.map.java.opts -->
    <property>
     	<name>mapreduce.map.memory.mb</name>
     	<value>-1</value>
    </property>
    
    <!-- matask 的 CPU 核数,默认 1 个 -->
    <property>
     	<name>mapreduce.map.cpu.vcores</name>
     	<value>1</value>
    </property>
    
    <!-- matask 异常重试次数,默认 4 次 -->
    <property>
     	<name>mapreduce.map.maxattempts</name>
     	<value>4</value>
    </property>
    
    <!-- 每个 Reduce 去 Map 中拉取数据的并行数。默认值是 5 -->
    <property>
        <name>mapreduce.reduce.shuffle.parallelcopies</name>
     	<value>5</value>
    </property>
    
    <!-- Buffer 大小占 Reduce 可用内存的比例,默认值 0.7 -->
    <property>
     	<name>mapreduce.reduce.shuffle.input.buffer.percent</name>
     	<value>0.70</value>
    </property>
    
    <!-- Buffer 中的数据达到多少比例开始写入磁盘,默认值 0.66。 -->
    <property>
     	<name>mapreduce.reduce.shuffle.merge.percent</name>
     	<value>0.66</value>
    </property>
    
    <!-- reducetask 内存,默认 1g;reducetask 堆内存大小默认和该值大小一致
    mapreduce.reduce.java.opts -->
    <property>
     	<name>mapreduce.reduce.memory.mb</name>
     	<value>-1</value>
    </property>
    
    <!-- reducetask 的 CPU 核数,默认 1 个 -->
    <property>
     	<name>mapreduce.reduce.cpu.vcores</name>
     	<value>2</value>
    </property>
    
    <!-- reducetask 失败重试次数,默认 4 次 -->
    <property>
     	<name>mapreduce.reduce.maxattempts</name>
     	<value>4</value>
    </property>
    
    <!-- 当 MapTask 完成的比例达到该值后才会为 ReduceTask 申请资源。默认是 0.05
    -->
    <property>
     	<name>mapreduce.job.reduce.slowstart.completedmaps</name>
     	<value>0.05</value>
    </property>
    
    <!-- 如果程序在规定的默认 10 分钟内没有读到数据,将强制超时退出 -->
    <property>
     	<name>mapreduce.task.timeout</name>
     	<value>600000</value>
    </property>
    

6.8.4 Yarn参数调优

  • yarn-site.xml 配置参数

    <!-- 选择调度器,默认容量 -->
    <property>
    	<name>yarn.resourcemanager.scheduler.class</name>
    	<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capaci
    ty.CapacityScheduler</value>
    </property>
    
    <!-- ResourceManager 处理调度器请求的线程数量,默认 50;如果提交的任务数大于 50,可以
    增加该值,但是不能超过 3 台 * 4 线程 = 12 线程(去除其他应用程序实际不能超过 8) -->
    <property>
    	<name>yarn.resourcemanager.scheduler.client.thread-count</name>
    	<value>8</value>
    </property>
    
    <!-- 是否让 yarn 自动检测硬件进行配置,默认是 false,如果该节点有很多其他应用程序,建议
    手动配置。如果该节点没有其他应用程序,可以采用自动 -->
    <property>
    	<name>yarn.nodemanager.resource.detect-hardware-capabilities</name>
    	<value>false</value>
    </property>
    
    <!-- 是否将虚拟核数当作 CPU 核数,默认是 false,采用物理 CPU 核数 -->
    <property>
    	<name>yarn.nodemanager.resource.count-logical-processors-ascores</name>
    	<value>false</value>
    </property>
    
    <!-- 虚拟核数和物理核数乘数,默认是 1.0 -->
    <property>
    	<name>yarn.nodemanager.resource.pcores-vcores-multiplier</name>
    	<value>1.0</value>
    </property>
    
    <!-- NodeManager 使用内存数,默认 8G,修改为 4G 内存 -->
    <property>
        <name>yarn.nodemanager.resource.memory-mb</name>
    	<value>4096</value>
    </property>
    
    <!-- nodemanager 的 CPU 核数,不按照硬件环境自动设定时默认是 8 个,修改为 4 个 -->
    <property>
    	<name>yarn.nodemanager.resource.cpu-vcores</name>
    	<value>4</value>
    </property>
    
    <!-- 容器最小内存,默认 1G -->
    <property>
    	<name>yarn.scheduler.minimum-allocation-mb</name>
    	<value>1024</value>
    </property>
    
    <!-- 容器最大内存,默认 8G,修改为 2G -->
    <property>
    	<name>yarn.scheduler.maximum-allocation-mb</name>
    	<value>2048</value>
    </property>
    
    <!-- 容器最小 CPU 核数,默认 1 个 -->
    <property>
    	<name>yarn.scheduler.minimum-allocation-vcores</name>
    	<value>1</value>
    </property>
    
    <!-- 容器最大 CPU 核数,默认 4 个,修改为 2 个 -->
    <property>
    	<name>yarn.scheduler.maximum-allocation-vcores</name>
    	<value>2</value>
    </property>
    
    <!-- 虚拟内存检查,默认打开,修改为关闭 -->
    <property>
    	<name>yarn.nodemanager.vmem-check-enabled</name>
    	<value>false</value>
    </property>
    
    <!-- 虚拟内存和物理内存设置比例,默认 2.1 -->
    <property>
    	<name>yarn.nodemanager.vmem-pmem-ratio</name>
    	<value>2.1</value>
    </property>
    
posted @ 2021-09-25 12:32  Norni  阅读(270)  评论(0编辑  收藏  举报