Hadoop

1.hadoop的发展历史

image-20200414133443007

  • Apache Lucene是一个文本搜索系统库

  • Apache Nutch作为前者的一部分

  • Hadoop最早起源于Nutch。Nutch的设计目标是构建一个大型的全网搜索引擎,包括网页抓取、索引、查询等功能,但随着抓取网页数量的增加,遇到了严重的可扩展性问题——如何解决数十亿网页的存储和索引问题。

  • 2003年、2004年谷歌发表的两篇论文为该问题提供了可行的解决方案。

    ——分布式文件系统(GFS),可用于处理海量网页的存储

    ——分布式计算框架MAPREDUCE,可用于处理海量网页的索引计算问题。

  • Nutch的开发人员2004年、2005年分别完成了相应的开源实现HDFS和MAPREDUCE,并从Nutch中剥离成为独立项目HADOOP,到2008年1月,HADOOP成为Apache顶级项目(同年,cloudera公司成立),迎来了它的快速发展期。

  • 狭义上来说,hadoop就是单独指代hadoop这个软件

  • 广义上来说,hadoop指代大数据的一个生态圈,包括很多其他的软件

clip_image004

2. hadoop的版本介绍

  • 0.x系列版本:hadoop当中最早的一个开源版本,在此基础上演变而来的1.x以及2.x的版本

  • 1.x版本系列:hadoop版本当中的第二代开源版本,主要修复0.x版本的一些bug等

  • 2.x版本系列:架构产生重大变化,引入了yarn平台等许多新特性,也是现在生产环境当中使用最多的版本

  • 3.x版本系列:在2.x版本的基础上,引入了一些hdfs的新特性等,且已经发型了稳定版本,未来公司的使用趋势

3. hadoop生产环境版本选择

  • Hadoop三大发行版本:Apache、Cloudera、Hortonworks。

    • Apache版本最原始(最基础)的版本,对于入门学习最好。
    • Cloudera在大型互联网企业中用的较多。
    • Hortonworks文档较好。
  • Apache Hadoop

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

    下载地址:https://archive.apache.org/dist/hadoop/common/

  • Cloudera Hadoop

    官网地址:https://www.cloudera.com/downloads/cdh/5-10-0.html

    下载地址:http://archive.cloudera.com/cdh5/cdh/5/

    • 2008年成立的Cloudera是最早将Hadoop商用的公司,为合作伙伴提供Hadoop的商用解决方案,主要是包括支持、咨询服务、培训。
    • 2009年Hadoop的创始人Doug Cutting也加盟Cloudera公司。Cloudera产品主要为CDH,Cloudera Manager,Cloudera Support
    • CDH是Cloudera的Hadoop发行版,完全开源,比Apache Hadoop在兼容性,安全性,稳定性上有所增强。
    • Cloudera Manager是集群的软件分发及管理监控平台,可以在几个小时内部署好一个Hadoop集群,并对集群的节点及服务进行实时监控。Cloudera Support即是对Hadoop的技术支持。
    • Cloudera的标价为每年每个节点4000美元。
    • Cloudera开发并贡献了可实时处理大数据的Impala项目。
  • Hortonworks Hadoop

    官网地址:https://hortonworks.com/products/data-center/hdp/

    下载地址:https://hortonworks.com/downloads/#data-platform

    • 现cloudera与hortonworks已合并。
    • 2011年成立的Hortonworks是雅虎与硅谷风投公司Benchmark Capital合资组建。
    • 公司成立之初就吸纳了大约25名至30名专门研究Hadoop的雅虎工程师,上述工程师均在2005年开始协助雅虎开发Hadoop,贡献了Hadoop80%的代码。
    • 雅虎工程副总裁、雅虎Hadoop开发团队负责人Eric Baldeschwieler出任Hortonworks的首席执行官。
    • Hortonworks的主打产品是Hortonworks Data Platform(HDP),也同样是100%开源的产品,HDP除常见的项目外还包括了Ambari,一款开源的安装和管理系统。
    • HCatalog,一个元数据管理系统,HCatalog现已集成到Facebook开源的Hive中。Hortonworks的Stinger开创性的极大的优化了Hive项目。Hortonworks为入门提供了一个非常好的,易于使用的沙盒。
    • Hortonworks开发了很多增强特性并提交至核心主干,这使得Apache Hadoop能够在包括Window Server和Windows Azure在内的Microsoft Windows平台上本地运行。定价以集群为基础,每10个节点每年为12500美元。

    注意:Hortonworks已经与Cloudera公司合并

4. hadoop的架构模块介绍

image-20200414134203318

  • Hadoop由三个模块组成:分布式存储HDFS、分布式计算MapReduce、资源调度引擎Yarn

image-20200414134230170

  • 关键词

    • 分布式
    • 主从架构
  • HDFS模块:

    • namenode:主节点,主要负责HDFS集群的管理以及元数据信息管理

    • datanode:从节点,主要负责存储用户数据

    • secondaryNameNode:辅助namenode管理元数据信息,以及元数据信息的冷备份

  • Yarn模块:

    • ResourceManager:主节点,主要负责资源分配
    • NodeManager:从节点,主要负责执行任务

5. hdfs功能详解

5.1. hdfs的架构详细剖析

1). 分块存储&机架感知&3副本

  • 分块存储

    • 保存文件到HDFS时,会先默认按128M的大小对文件进行切分成block块
    • 数据以block块的形式存在HDFS文件系统中
      • 在hadoop1当中,文件的block块默认大小是64M
      • hadoop2、3当中,文件的block块大小默认是128M,block块的大小可以通过hdfs-site.xml当中的配置文件进行指定
    <property>
        <name>dfs.blocksize</name>
        <value>块大小 以字节为单位</value><!-- 只写数值就可以 -->
    </property>
    
    • hdfs-default.xml参考默认属性

    • 例如:

      如果有一个文件大小为1KB,也是要占用一个block块,但是实际占用磁盘空间还是1KB大小

      类似于有一个水桶可以装128斤的水,但是我只装了1斤的水,那么我的水桶里面水的重量就是1斤,而不是128斤

    • block元数据:每个block块的元数据大小大概为150字节

  • 3副本存储

    • 为了保证block块的安全性,也就是数据的安全性,在hadoop2当中,采用文件默认保存三个副本,我们可以更改副本数以提高数据的安全性
    • 在hdfs-site.xml当中修改以下配置属性,即可更改文件的副本数
    <property>
          <name>dfs.replication</name>
          <value>3</value>
    </property>

2). 抽象成数据块的好处

  1. 文件可能大于集群中任意一个磁盘
    10T*3/128 = xxx块 10T 文件方式存—–>多个block块,这些block块属于一个文件

  2. 使用块抽象而不是文件可以简化存储子系统

    hdfs将所有的文件全部抽象成为block块来进行存储,不管文件大小,全部一视同仁都是以block块的形式进行存储,方便我们的分布式文件系统对文件的管理

  3. 块非常适合用于数据备份;进而提供数据容错能力和可用性

3). HDFS架构

image-20200416160250256

  • HDFS集群包括,NameNode和DataNode以及Secondary Namenode。

    • NameNode负责管理整个文件系统的元数据,包括hdfs目录树、每个文件有哪些块、每个块存储在哪些datanode
    • DataNode 负责管理用户的文件数据块,每一个数据块都可以在多个datanode上存储多个副本。
    • Secondary NameNode用来监控HDFS状态的辅助后台程序,每隔一段时间获取HDFS元数据的快照。最主要作用是辅助namenode管理元数据信息
  • NameNode与Datanode的总结概述

image-20200416160339310

4). 扩展

  1. 块缓存
  • 通常DataNode从磁盘中读取块,但对于访问频繁的文件,其对应的块可能被显示的缓存在DataNode的内存中,以堆外块缓存的形式存在。

  • 默认情况下,一个块仅缓存在一个DataNode的内存中,当然可以针对每个文件配置DataNode的数量。作业调度器通过在缓存块的DataNode上运行任务,可以利用块缓存的优势提高读操作的性能。

    例如:
    连接(join)操作中使用的一个小的查询表就是块缓存的一个很好的候选。
    用户或应用通过在缓存池中增加一个cache directive来告诉namenode需要缓存哪些文件及存多久。缓存池(cache pool)是一个拥有管理缓存权限和资源使用的管理性分组

  1. hdfs的文件权限验证
  • hdfs的文件权限机制与linux系统的文件权限机制类似

    r:read w:write x:execute 权限x对于文件表示忽略,对于文件夹表示是否有权限访问其内容

    如果linux系统用户zhangsan使用hadoop命令创建一个文件,那么这个文件在HDFS当中的owner就是zhangsan

    HDFS文件权限的目的,防止好人做错事,而不是阻止坏人做坏事。HDFS相信你告诉我你是谁,你就是谁

    hdfs 权限-》kerberos、ranger、sentry来做

5.2. hdfs的优缺点

5.2.1. hdfs的优点

(1) 高容错性

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

​ 2) 某一个副本丢失以后,它可以自动恢复,这是由 HDFS 内部机制实现的,我们不必关心。

(2) 适合批处理

​ 1) 它是通过移动计算而不是移动数据。

​ 2) 它会把数据位置暴露给计算框架。

(3) 适合大数据处理

​ 1) 数据规模:能够处理数据规模达到 GB、TB、甚至PB级别的数据。

​ 2) 文件规模:能够处理百万规模以上的文件数量,数量相当之大。

​ 3) 节点规模:能够处理10K节点的规模。

(4) 流式数据访问

​ 1) 一次写入,多次读取,不能随机修改,只能追加。

​ 2) 它能保证数据的一致性。

(5) 可构建在廉价机器上

​ 1) 它通过多副本机制,提高可靠性。

​ 2) 它提供了容错和恢复机制。比如某一个副本丢失,可以通过其它副本来恢复。

5.2.2. hdfs的缺点

(1) 不适合低延时数据访问

​ 1) 比如毫秒级的来存储数据,这是不行的,它做不到。

​ 2) 它适合高吞吐率的场景,就是在某一时间内写入大量的数据。但是它在低延时的情况 下是不行的,比如毫秒级以内读取数据,这样它是很难做到的。

(2) 无法高效的对大量小文件进行存储

​ 1) 存储大量小文件的话,它会占用 NameNode大量的内存来存储文件、目录和块信息。这样是不可取的,因为NameNode的内存总是有限的。

​ 2) 小文件存储的寻道时间会超过读取时间,它违反了HDFS的设计目标。 改进策略

(3) 并发写入、文件随机修改

​ 1) 一个文件只能有一个写,不允许多个线程同时写。

​ 2) 仅支持数据 append(追加),不支持文件的随机修改。

5.3. hdfs的shell命令操作

  • HDFS命令有两种风格,均可使用,效果相同:
    • hadoop fs开头的
    • hdfs dfs开头的
  1. 如何查看hdfs或hadoop子命令的帮助信息,如ls子命令
hdfs dfs -help ls
hadoop fs -help ls #两个命令等价
  1. 查看hdfs文件系统中指定目录的文件列表。对比linux命令ls
hdfs dfs -ls /
hadoop fs -ls /
hdfs dfs -ls -R /
  1. 在hdfs文件系统中创建文件
hdfs dfs -touchz /edits.txt
hdfs dfs -ls /
  1. 向HDFS文件中追加内容
hadoop fs -appendToFile edit1.xml /edits.txt #将本地磁盘当前目录的edit1.xml内容追加到HDFS根目录 的edits.txt文件
  1. 查看HDFS文件内容
hdfs dfs -cat /edits.txt
hdfs dfs -text /edits.txt
  1. 从本地路径上传文件至HDFS
#用法:hdfs dfs -put /本地路径 /hdfs路径
hdfs dfs -put /linux本地磁盘文件 /hdfs路径文件
hdfs dfs -copyFromLocal /linux本地磁盘文件 /hdfs路径文件  #跟put作用一样
hdfs dfs -moveFromLocal /linux本地磁盘文件 /hdfs路径文件  #跟put作用一样,只不过,源文件被拷贝成功后,会被删除
  1. 在hdfs文件系统中下载文件
hdfs dfs -get /hdfs路径 /本地路径hdfs dfs -copyToLocal /hdfs路径 /本地路径  #根get作用一样
  1. 在hdfs文件系统中创建目录
hdfs dfs -mkdir /shell
  1. 在hdfs文件系统中删除文件
hdfs dfs -rm /edits.txt将文件彻底删除(被删除文件不放到hdfs的垃圾桶里)how?hdfs dfs -rm -skipTrash /xcall
  1. 在hdfs文件系统中修改文件名称(也可以用来移动文件到目录)
hdfs dfs -mv /xcall.sh /call.shhdfs dfs -mv /call.sh /shell
  1. 在hdfs中拷贝文件到目录
hdfs dfs -cp /xrsync.sh /shell
  1. 递归删除目录
hdfs dfs -rm -r /shell
  1. 列出本地文件的内容(默认是hdfs文件系统)
hdfs dfs -ls file:///home/hadoop/
  1. 查找文件
# linux find命令find . -name 'edit*'# HDFS find命令hadoop fs -find / -name part-r-00000 # 在HDFS根目录中,查找part-r-00000文件
  1. 总结
  • 输入hadoop fs 或hdfs dfs,回车,查看所有的HDFS命令

  • 许多命令与linux命令有很大的相似性,学会举一反三

  • 有用的help,如查看ls命令的使用说明:hadoop fs -help ls

  • 绝大多数的大数据框架的命令,也有类似的help信息

5.4. hdfs安全模式

  • 安全模式是HDFS所处的一种特殊状态
    • 文件系统只接受读请求
    • 不接受写请求,如删除、修改等变更请求。
  • 在NameNode主节点启动时,HDFS首先进入安全模式
    • DataNode在启动的时候会向namenode汇报可用的block等状态,当整个系统达到安全标准时,HDFS自动离开安全模式。
    • 如果HDFS处于安全模式下,则文件block不能进行任何的副本复制操作,因此达到最小的副本数量要求是基于datanode启动时的状态来判定的
    • 启动时不会再做任何复制(从而达到最小副本数量要求)
    • hdfs集群刚启动的时候,默认30S钟的时间是处于安全期的,只有过了30S之后,集群脱离了安全模式,然后才可以对集群进行操作
  • 何时退出安全模式
    • namenode知道集群共多少个block(不考虑副本),假设值是total;
    • namenode启动后,会上报block report,namenode开始累加统计满足最小副本数(默认1)的block个数,假设是num
    • 当num/total > 99.9%时,退出安全模式

image-20200925151612455

[hadoop@node01 hadoop]$ hdfs dfsadmin -safemode 
Usage: hdfs dfsadmin [-safemode enter | leave | get | wait]

5.5. hdfs的java API开发

第一步:windows中的hadoop环境配置

  • 修改hosts文件

    192.168.52.100 node01.kaikeba.com node01
    192.168.52.110 node02.kaikeba.com node02
    192.168.52.120 node03.kaikeba.com node03
    
  • 配置hadoop环境(因为windows涉及到了跨平台,大数据集群在linux操作系统中)

    • 下载hadoop-3.1.4.tar.gz 镜像链接,解压到一个没有中文、没有空格的目录下,如D:/hadoop-3.1.4
    • 下载winutils/hadoop-3.1.2 下载地址,将bin目录的内容拷贝到D:/hadoop-3.1.4的bin目录下,并将bin目录下的hadoop.dll文件拷贝到C:\Windows\System32
    • 配置hadoop的环境变量,HADOOP_HOME=D:/hadoop-3.1.4,将%HADOOP_HOME%\bin、%HADOOP_HOME%\sbin加入path中
    • 将hadoop集群的以下5个配置文件core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml、workers,拷贝到windows下hadoop的%HADOOP_HOME%\etc\hadoop目录下
    • cmd中运行hadoop,验证环境是否配置成功。若出现找不到指定的批处理标签,则需要将``%HADOOP_HOME%\bin`目录下所有cmd文件用notepad++打开,进行文档格式转换(编辑>文档格式转换>转为Windows(CRLF))
    • 虚拟机中hdfs集群启动前提下,cmd中运行hdfs dfs -ls /,可以查询出hdfs集群根目录的内容

第二步:创建maven工程并导入jar包

1).创建maven工程后

  • maven会自动的去本地仓库查看时候有所需的jar包
  • 如果没有的话,默认去中央仓库,将jar包下载到本地;
  • 以后如果再次使用此jar时,就直接使用本地仓库的jar即可
  • 此过程是maven自动完成的

2).仓库

3).settings.xml文件

  • maven的配置文件settings.xml在maven的conf目录中

  • 声明文件规范

    <?xml version="1.0" encoding="UTF-8"?>
    <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
    http://maven.apache.org/xsd/settings-1.0.0.xsd">
    
  • 本地仓库路径
    <localRepository>/path/to/local/repo</localRepository>

  • Maven是否需要和用户交互以获得输入。如果Maven需要和用户交互以获得输入,则设置成true,反之则应为false。默认为true

    <interactiveMode>true</interactiveMode>

  • 表示Maven是否需要在离线模式下运行。如果构建系统需要在离线模式下运行,则为true,默认为false。当由于网络设置原因或者安全因素,构建服务器不能连接远程仓库的时候,该配置就十分有用

    <offline>false</offline>

  • 当插件的组织Id(groupId)没有显式提供时,供搜寻插件组织Id(groupId)的列表。该元素包含一个pluginGroup元素列表,每个子元素包含了一个组织Id(groupId)。

    pluginGroups

  • 为仓库列表配置的下载镜像列表

    mirrors

4).pom文件

  • 4.0.0 项目的模板版本

  • com.guiji 可以用这个来标识公司

  • HadoopTest 可以标识工程的作用

  • jar 指定打包类型

  • 1.0-SNAPSHOT 制定项目版本

  • ... 项目的依赖关系

  • 构建项目的信息

  • 总项目/ pom.xml 总项目的pom配置文件

  • 子项目1/ pom.xml 子项目1的pom文件

  • 子项目2/ pom.xml 子项目2的pom文件

<properties>
    <hadoop.version>3.1.4</hadoop.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>${hadoop.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-common</artifactId>
        <version>${hadoop.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs</artifactId>
        <version>${hadoop.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-mapreduce-client-core</artifactId>
        <version>${hadoop.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/junit/junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <!--   <verbal>true</verbal>-->
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.4.3</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <minimizeJar>true</minimizeJar>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

5).maven简单的命令

mvn clean 
mvn clean package

如果依赖自动下载有问题,需要自己手动去仓库下载jar包,然后使用如下命令添加依赖包进仓库,要用 mvn install这个命令将jar包打进仓库

mvn install:install-file -Dfile=D:\junit-3.8.2.jar -DgroupId=junit -DartifactId=junit -Dversion=3.8.2 -Dpackaging=jar

6).IDEA的常用快捷键

  • Ctrl+Alt+v :补全返回值
  • Ctrl+E:显示最近编辑的文件列表
  • Ctrl+F12:显示当前文件的结构
  • Ctrl+P:显示方法的参数信息
  • Ctrl+Alt+T:可以将代码包在一块内,例如try/catch
  • Ctrl+H:显示类结构图
  • Alt+回车:导入包自动修正
  • Ctrl+/:单行注释
  • Ctrl+Shift+/:块注释
  • 方法或者类注释:输入/**
  • 双shift:类源文件查询

第三步:开发hdfs的javaAPI操作

api文档

编程时,注意导入正确的包

小技巧:让IDEA自动导包

image-20200925162638215

1).获取文件系统

  • 获取org.apache.hadoop.fs类下的FileSystem,通过两下shift调出查询框,查询FileSystem类文件

  • FileSystem类是一个抽象类,无法直接用new创建,只能通过继承使用它的子类或内置方法(get)获取

  • FileSystem.get有三种重载方法

    • get(Configuration conf):传入Configuration对象,获取fs.defaultFS值,没有则为默认的"file:///",即本地文件。需要传入前在Configuration中配置fs.defaultFS

    • get(URI uri, Configuration conf):直接传入URI

    • get(final URI uri, final Configuration conf,final String user) :指定user,防止好人办坏事,但是无法防止坏人办好事

FileSystem fs = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration(), "geliang");

2).创建文件夹

//简化版
@Test
public void mkDirOnHDFS() throws IOException {
    //配置项
    Configuration configuration = new Configuration();
    //设置要连接的hdfs集群
    configuration.set("fs.defaultFS", "hdfs://node01:8020");
    //获得文件系统
    FileSystem fileSystem = FileSystem.get(configuration);
    //调用方法创建目录;若目录已经存在,则创建失败,返回false
    boolean mkdirs = fileSystem.mkdirs(new Path("hdfs://node01:8020/geliang/dir1"));
    //释放资源
    fileSystem.close();
}

//指定目录所属用户
@Test
public void mkDirOnHDFS2() throws IOException, URISyntaxException, InterruptedException {
    //配置项
    Configuration configuration = new Configuration();
    //获得文件系统
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), configuration, "geliang");
    //调用方法创建目录
    boolean mkdirs = fileSystem.mkdirs(new Path("hdfs://node01:8020/geliang/dir1"));
    //释放资源
    fileSystem.close();
}

//创建目录时,指定目录权限
@Test
public void mkDirOnHDFS3() throws IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS", "hdfs://node01:8020");

    FileSystem fileSystem = FileSystem.get(configuration);
    FsPermission fsPermission = new FsPermission(FsAction.ALL, FsAction.READ, FsAction.READ);
    boolean mkdirs = fileSystem.mkdirs(new Path("hdfs://node01:8020/geliang/dir3"), fsPermission);
    if (mkdirs) {
        System.out.println("目录创建成功");
    }

    fileSystem.close();
}

3).文件上传

@Test
public void uploadFile2HDFS() throws IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS", "hdfs://node01:8020");
    FileSystem fileSystem = FileSystem.get(configuration);
    fileSystem.copyFromLocalFile(new Path("file:///E:\\hello.txt"),new Path("hdfs://node01:8020/geliang"));
    fileSystem.close();
}

4).文件下载

@Test
public void downloadFileFromHDFS() throws IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS", "hdfs://node01:8020");
    FileSystem fileSystem = FileSystem.get(configuration);
    fileSystem.copyToLocalFile(false,
                               new Path("hdfs://node01:8020/geliang/hello.txt"),
                               new Path("file:///E:\\hello.txt"),
                               true);
    /*copyToLocalFile四个参数,
        delSrc:是否删除源文件
        src: 源文件目录
        dst: 目标文件目录
        useRawLocalFileSystem: 是否将RawLocalFileSystem用作本地文件系统,RawLocalFileSystem是非crc文件系统,它不会在本地创建任何crc校验文件
        */
    fileSystem.close();
}

5).文件删除

@Test
void rmFileOnHDFS() throws IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS", "hdfs://node01:8020");
    FileSystem fs = FileSystem.get(configuration);
    boolean delete = fs.delete(new Path("hdfs://node01:8020/geliang"), true);
    System.out.println("删除" + (delete?"成功!":"失败!"));
    // delete(Path f, boolean recursive)
    // recursive:是否递归删除
    // deleteOnExit(Path f),标记一个路径,当文件系统关闭后删除
    fs.close();
}

6).文件重命名

@Test
void renameOnHDFS() throws IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS", "hdfs://node01:8020");
    FileSystem fs = FileSystem.get(configuration);
    boolean rename = fs.rename(
        new Path("hdfs://node01:8020/geliang/hello.txt"),
        new Path("hdfs://node01:8020/geliang/hello2.txt"));
    System.out.println("重命名" + (rename?"成功!":"失败!"));
    fs.close();
}

7).查看hdfs文件详细信息

@Test
public void viewFileInfo() throws IOException, InterruptedException, URISyntaxException {
    // 1获取文件系统
    Configuration configuration = new Configuration();
    FileSystem fs = FileSystem.get(new URI("hdfs://node01:8020"), configuration);

    // 2 获取文件详情
    RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("hdfs://node01:8020/geliang/"), true);

    while (listFiles.hasNext()) {
        LocatedFileStatus status = listFiles.next();
        // 输出详情
        // 文件名称
        System.out.println(status.getPath().getName());
        // 长度
        System.out.println(status.getLen());
        // 权限
        System.out.println(status.getPermission());
        // 分组
        System.out.println(status.getGroup());
        // 获取存储的块信息
        BlockLocation[] blockLocations = status.getBlockLocations();

        for (BlockLocation blockLocation : blockLocations) {
            // 获取块存储的主机节点
            String[] hosts = blockLocation.getHosts();
            for (String host : hosts) {
                System.out.println(host);
            }
        }
    }
    // 3 关闭资源
    fs.close();
}

8).IO流操作hdfs文件

  1. 通过io流进行数据上传操作
@Test
void putFileToHDFS() throws IOException {
    // 1.获取hdfs文件系统
    FileSystem fs = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration(), "geliang");
    // 2.获取本地文件输入流
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("E:\\hello.txt")));
    // 3.通过hdfs文件系统创建输出流
    BufferedOutputStream bos = new BufferedOutputStream(fs.create(new Path("hdfs://node01:8020/geliang/hello.txt")));
    // 4.IOUtils.copy实现流拷贝
    IOUtils.copy(bis, bos);
    // 5.关闭资源
    IOUtils.closeQuietly(bis);
    IOUtils.closeQuietly(bos);
    fs.close();
}

image-20200925161954948

  1. 通过IO流从hdfs上面下载文件

    @Testvoid getFileFromHDFS() throws IOException, URISyntaxException, InterruptedException {    // 1.获取hdfs文件系统    FileSystem fs = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration(), "geliang");    // 2.fs.open获取hdfs文件输入流    BufferedInputStream bis = new BufferedInputStream(fs.open(new Path("hdfs://node01:8020/geliang/hello.txt")));    // 3.获取本地文件输出流    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("E:\\hello.txt")));    // 4.IOUtils.copy实现流拷贝    IOUtils.copy(bis, bos);    // 5.关闭资源    IOUtils.closeQuietly(bis);    IOUtils.closeQuietly(bos);    fs.close();}
    
  2. hdfs的小文件合并

/**
  * 构建小文件
  */
@Test
void createSmallFile() throws URISyntaxException, IOException, InterruptedException {
    File dir = new File("E:\\smallfile");
    for (int i = 0; i < 100; i++) {
        FileWriter fw = new FileWriter(new File(dir, "smallfile" + i + ".txt"));
        fw.write("这是" + "smallfile" + i);
        fw.close();
    }
}


/**
  * 小文件合并:读取所有本地小文件,写入到hdfs的大文件里面去。(相当于linux中用cat合并文件)
  */
@Test
void mergeFile() throws URISyntaxException, IOException, InterruptedException {
    // 1.获取hdfs文件
    FileSystem fs = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration(), "geliang");
    // 2.fs.create获取hdfs大文件输出流
    BufferedOutputStream bos = new BufferedOutputStream(fs.create(new Path("hdfs://node01:8020/geliang/bigfile.txt")));
    // 3.遍历小文件,获取输入流,通过IOUtils.copy依次拷贝到大文件,关闭输入流
    File dir = new File("E:\\smallfile");
    File[] files = dir.listFiles();
    if (files != null) {
        for (File file : files) {
            System.out.println(file.getName());
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
            IOUtils.copy(bis, bos);
            IOUtils.closeQuietly(bis);
        }
    }
    // 4.关闭资源
    IOUtils.closeQuietly(bos);
    fs.close();
}

6.NameNode和SecondaryNameNode功能剖析

6.1. namenode与secondaryName解析

  • NameNode主要负责集群当中的元数据信息管理,而且元数据信息需要经常随机访问,因为元数据信息必须高效的检索
    • 元数据信息保存在哪里能够快速检索呢?
    • 如何保证元数据的持久安全呢?
  • 为了保证元数据信息的快速检索,那么我们就必须将元数据存放在内存当中,因为在内存当中元数据信息能够最快速的检索,那么随着元数据信息的增多(每个block块大概占用150字节的元数据信息),内存的消耗也会越来越多。
  • 如果所有的元数据信息都存放内存,服务器断电,内存当中所有数据都消失,为了保证元数据的安全持久,元数据信息必须做可靠的持久化,在hadoop当中为了持久化存储元数据信息,将所有的元数据信息保存在了FSImage文件当中,那么FSImage随着时间推移,必然越来越膨胀,FSImage的操作变得越来越难,为了解决元数据信息的增删改,hadoop当中还引入了元数据操作日志edits文件,edits文件记录了客户端操作元数据的信息,随着时间的推移,edits信息也会越来越大,为了解决edits文件膨胀的问题,hadoop当中引入了secondaryNamenode来专门做fsimage与edits文件的合并。

image-20200925164802493

  1. namenode工作机制

    (1)第一次启动namenode格式化后,创建fsimage和edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。

    (2)客户端对元数据进行增删改的请求

    (3)namenode记录操作日志,更新滚动日志。

    (4)namenode在内存中对数据进行增删改查

  2. Secondary NameNode工作

    (1)Secondary NameNode询问namenode是否需要checkpoint。直接带回namenode是否检查结果。

​ (2)Secondary NameNode请求执行checkpoint。

​ (3)namenode滚动正在写的edits日志

​ (4)将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode

​ (5)Secondary NameNode加载编辑日志和镜像文件到内存,并合并。

​ (6)生成新的镜像文件fsimage.chkpoint

​ (7) 拷贝fsimage.chkpoint到namenode

​ (8)namenode将fsimage.chkpoint重新命名成fsimage

属性 解释
dfs.namenode.checkpoint.period 3600秒(即1小时) The number of seconds between two periodic checkpoints.
dfs.namenode.checkpoint.txns 1000000 The Secondary NameNode or CheckpointNode will create a checkpoint of the namespace every 'dfs.namenode.checkpoint.txns' transactions, regardless of whether 'dfs.namenode.checkpoint.period' has expired.
dfs.namenode.checkpoint.check.period 60(1分钟) The SecondaryNameNode and CheckpointNode will poll the NameNode every 'dfs.namenode.checkpoint.check.period' seconds to query the number of uncheckpointed transactions.

6.2. FSImage与edits详解

  • 所有的元数据信息都保存在了FsImage与Eidts文件当中,这两个文件就记录了所有的数据的元数据信息,元数据信息的保存目录配置在了hdfs-site.xml当中
    <!-- namenode保存fsimage的路径 -->
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///kkb/install/hadoop-3.1.4/hadoopDatas/namenodeDatas</value>
    </property>
    <!-- namenode保存editslog的目录 -->
    <property>
        <name>dfs.namenode.edits.dir</name>
        <value>file:///kkb/install/hadoop-3.1.4/hadoopDatas/dfs/nn/edits</value>
    </property>
  • 客户端对hdfs进行写文件时会首先被记录在edits文件中

    edits修改时元数据也会更新。

    每次hdfs更新时edits先更新后,客户端才会看到最新信息。

    fsimage:是namenode中关于元数据的镜像,一般称为检查点。

    一般开始时对namenode的操作都放在edits中,为什么不放在fsimage中呢?

    因为fsimage是namenode的完整的镜像,内容很大,如果每次都加载到内存的话生成树状拓扑结构,这是非常耗内存和CPU。

    fsimage内容包含了namenode管理下的所有datanode中文件及文件block及block所在的datanode的元数据信息。随着edits内容增大,就需要在一定时间点和fsimage合并。

6.3. FSimage文件当中的文件信息查看

cd  /kkb/install/hadoop-3.1.4/hadoopDatas/namenodeDatas/current
hdfs oiv    #查看帮助信息
hdfs oiv -i fsimage_0000000000000000864 -p XML -o /home/hadoop/fsimage1.xml

6.4. edits当中的文件信息查看

cd /kkb/install/hadoop-3.1.4/hadoopDatas/dfs/nn/edits/current
hdfs oev     #查看帮助信息
hdfs oev -i edits_0000000000000000865-0000000000000000866 -o /home/hadoop/myedit.xml -p XML

6.5. namenode元数据信息多目录配置

  • 为了保证元数据的安全性

    • 我们一般都是先确定好我们的磁盘挂载目录,将元数据的磁盘做RAID1 namenode的本地目录可以配置成多个,且每个目录存放内容相同,增加了可靠性。
    • 多个目录间逗号分隔
  • 具体配置如下:

    hdfs-site.xml

<property>
   <name>dfs.namenode.name.dir</name>
   <value>file:///kkb/install/hadoop-3.1.4/hadoopDatas/namenodeDatas,file:///path/to/another/</value>
</property>

7. hdfs的读写流程

7.1. hdfs的写入流程

image-20201031103803685

文件上传流程如下:

  • 创建文件:
    ①HDFS client向HDFS写入数据,先调用DistributedFileSystem.create()
    ②RPC调用namenode的create(),会在HDFS目录树中指定的路径,添加新文件;并将操作记录在edits.log中
    namenode.create()方法执行完后,返回一个FSDataOutputStream,它是DFSOutputStream的包装类

  • 建立数据流管道pipeline
    ③client调用DFSOutputStream.write()写数据(先写第一个块的数据,暂时叫blk1)
    ④DFSOutputStream通过RPC调用namenode的addBlock,向namenode申请一个空的数据块block
    ⑤addBlock返回LocatedBlock对象;此对象中包含了当前blk要存储在哪三个datanode的信息,比如dn1、dn2、dn3
    ⑥客户端,根据位置信息,建立数据流管道(图中蓝色线条)

  • 向数据流管道写当前块的数据
    ⑦写数据时,先将数据写入一个检验块chunk中,写满512字节后,对此chunk计算校验和checksum值(4字节)
    ⑧然后将chunk及对应校验和写入packet中,一个packet是64KB
    ⑨随着源源不断的带校验和的chunk写入packet,当packet写满后,将packet写入dataqueue数据队列中
    ⑩packet从队列中取出,沿pipeline发送到dn1,再从dn1发送到dn2,再从dn2发送到dn3
    ⑪同时,此packet会保存一份到一个确认队列ack queue中
    ⑫packet到达最后一个datanode即dn3后,做校验,将校验结果逆着pipeline方向回传到客户端,具体是校验结果从dn3传到dn2,dn2也会做校验,校验结果再
    传到dn1,dn1也做校验;结果再传回客户端
    ⑬客户端根据校验结果,如果“成功”,则将将保存在ack queue中的packet删除;如果失败,则将packet取出,重新放回到data queue末尾,等待再次沿pipeline发送
    ⑭如此,将block中的一个数据一个个packet发送出去;当此block发送完毕,
    即dn1、dn2、dn3都接受了blk1的完整的副本,那么三个dn分别RPC调用namenode的blockReceivedAndDeleted(),
    namenode会更新内存中block与datanode的对应关系(比如dn1上多了一个blk1副本)

  • 关闭dn1、dn2、dn3构建的pipeline;且文件还有下一个块时,再从④开始;直到文件全部数据写完
    ⑮最终,调用DFSOutputStream的close()
    ⑯客户端调用namenode的complete(),告知namenode文件传输完成

  • 容错

image-20200509101759635

  • 假设说当前构建的pipeline是dn1、dn2、dn3构成的
    当传输数据的过程中,dn2挂了或通信不畅了,则当前pipeline中断
    HDFS会如何做?

  • 先将ack queue中的所有packet全部放回到data queue中
    客户端RPC调用namenode的updateBlockForPipeline(),为当前block(假设是blk1)生成新的版本比如ts1(本质是时间戳)
    故障dn2会从pipeline中删除
    DFSOutputStream再RPC调用namenode的getAdditionalDatanode(),让namenode分配新的datanode,比如是dn4
    输出流将原dn1、dn3与新的dn4组成新的管道,他们上边的blk1版本设置为新版本ts1
    由于新添加的dn4上没有blk1的数据,客户端告知dn1或dn3,将其上的blk1的数据拷贝到dn4上
    新的数据管道建立好后,DFSOutputStream调用updatePipeline()更新namenode元数据
    至此,pipeline恢复,客户端按正常的写入流程,完成文件的上传

  • 故障datanode重启后,namenode发现它上边的block的blk1的时间戳是老的,会让datanode将blk1删除掉

7.2. hdfs的读取流程

image-20200509101636214

  • 1、client端读取HDFS文件,client调用文件系统对象DistributedFileSystem的open方法
  • 2、返回FSDataInputStream对象(对DFSInputStream的包装)
  • 3、构造DFSInputStream对象时,调用namenode的getBlockLocations方法,获得file的开始若干block(如blk1, blk2, blk3, blk4)的存储datanode(以下简称dn)列表;针对每个block的dn列表,会根据网络拓扑做排序,离client近的排在前;
  • 4、调用DFSInputStream的read方法,先读取blk1的数据,与client最近的datanode建立连接,读取数据
  • 5、读取完后,关闭与dn建立的流
  • 6、读取下一个block,如blk2的数据(重复步骤4、5、6)
  • 7、这一批block读取完后,再读取下一批block的数据(重复3、4、5、6、7)
  • 8、完成文件数据读取后,调用FSDataInputStream的close方法

容错

  • 情况一:读取block过程中,client与datanode通信中断

    • client与存储此block的第二个datandoe建立连接,读取数据
    • 记录此有问题的datanode,不会再从它上读取数据
  • 情况二:client读取block,发现block数据有问题

    • client读取block数据时,同时会读取到block的校验和,若client针对读取过来的block数据,计算检验和,其值与读取过来的校验和不一样,说明block数据损坏
    • client从存储此block副本的其它datanode上读取block数据(也会计算校验和)
    • 同时,client会告知namenode此情况;

8. datanode工作机制以及数据存储

1

  • HDFS分布式文件系统也是一个主从架构,主节点是我们的namenode,负责管理整个集群以及维护集群的元数据信息

  • 从节点datanode,主要负责文件数据存储

2

8.1. datanode工作机制

1)一个数据块在datanode上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。

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

3)心跳是每3秒一次,心跳返回结果带有namenode给该datanode的命令如复制块数据到另一台机器,或删除某个数据块。如果超过10分钟没有收到某个datanode的心跳,则认为该节点不可用。

4)集群运行中可以安全加入和退出一些机器

8.2. 数据完整性

1)当DataNode读取block的时候,它会计算checksum。

2)如果计算后的checksum,与block创建时值不一样,说明block已经损坏。

3)client读取其他DataNode上的block。

4)datanode在其文件创建后周期验证checksum。

8.3. 掉线时限参数设置

  • datanode进程死亡或者网络故障造成datanode无法与namenode通信,namenode不会立即把该节点判定为死亡,要经过一段时间,这段时间暂称作超时时长。HDFS默认的超时时长为10分钟+30秒。如果定义超时时间为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 配置文件中的heartbeat.recheck.interval的单位为毫秒,dfs.heartbeat.interval的单位为秒。

<property>
    <name>dfs.namenode.heartbeat.recheck-interval</name>
    <value>300000</value>
</property>
<property>
    <name> dfs.heartbeat.interval </name>
    <value>3</value>
</property>

8.4. DataNode的目录结构

  • 和namenode不同的是,datanode的存储目录是初始阶段自动创建的,不需要额外格式化。

  • /kkb/install/hadoop-3.1.4/hadoopDatas/datanodeDatas/current这个目录下查看版本号

[root@node01 current]# cat VERSION 
#Thu Mar 14 07:58:46 CST 2019
storageID=DS-47bcc6d5-c9b7-4c88-9cc8-6154b8a2bf39
clusterID=CID-dac2e9fa-65d2-4963-a7b5-bb4d0280d3f4
cTime=0
datanodeUuid=c44514a0-9ed6-4642-b3a8-5af79f03d7a4
storageType=DATA_NODE
layoutVersion=-56

具体解释

(1)storageID:存储id号

(2)clusterID集群id,全局唯一

(3)cTime属性标记了datanode存储系统的创建时间,对于刚刚格式化的存储系统,这个属性为0;但是在文件系统升级之后,该值会更新到新的时间戳。

(4)datanodeUuid:datanode的唯一识别码

(5)storageType:存储类型

(6)layoutVersion是一个负整数。通常只有HDFS增加新特性时才会更新这个版本号。

8.5. Datanode多目录配置

  • datanode也可以配置成多个目录,每个目录存储的数据不一样。即:数据不是副本。具体配置如下:
cd /kkb/install/hadoop-3.1.4/etc/hadoop
vim hdfs-site.xml
<!--  定义dataNode数据存储的节点位置,实际工作中,一般先确定磁盘的挂载目录,然后多个目录用,进行分割  -->
<property>
   <name>dfs.datanode.data.dir</name>
   <value>file:///kkb/install/hadoop-3.1.4/hadoopDatas/datanodeDatas</value>
</property>

9. hdfs的小文件治理

9.1. 有没有问题

· NameNode存储着文件系统的元数据,每个文件、目录、块大概有150字节的元数据;

· 因此文件数量的限制也由NN内存大小决定,如果小文件过多则会造成NN的压力过大

· 且HDFS能存储的数据总量也会变小

9.2. HAR文件方案

· 本质启动mr程序,所以需要启动yarn

3

用法:archive -archiveName <NAME>.har -p <parent path> [-r <replication factor>]<src>* <dest>

4

5

第一步:创建归档文件

注意:归档文件一定要保证yarn集群启动

cd /kkb/install/hadoop-3.1.4
bin/hadoop archive  -archiveName myhar.har -p /user/hadoop /user

第二步:查看归档文件内容

hdfs dfs -ls -R /user/myhar.har
hdfs dfs -ls -R har:///user/myhar.har

第三步:解压归档文件

hdfs dfs -mkdir -p /user/har
hdfs dfs -cp har:///user/myhar.har/* /user/har/

注意:原小文件还在,打成har包后,可以删除小文件

9.3. Sequence Files方案

Image201907101934

  • SequenceFile文件,主要由一条条record记录组成;

  • 具体结构(如上图):

    • 一个SequenceFile首先有一个4字节的header(文件版本号)
    • 接着是若干record记录
    • 每个record是键值对形式的;键值类型是可序列化类型,如IntWritable、Text
    • 记录间会随机的插入一些同步点sync marker,用于方便定位到记录边界
  • SequenceFile文件可以作为小文件的存储容器;

    • 每条record保存一个小文件的内容
    • 小文件名作为当前record的键;
    • 小文件的内容作为当前record的值;
    • 如10000个100KB的小文件,可以编写程序将这些文件放到一个SequenceFile文件。
  • 一个SequenceFile是可分割的,所以MapReduce可将文件切分成块,每一块独立操作。

  • 不像HAR,SequenceFile支持压缩。记录的结构取决于是否启动压缩

    • 支持两类压缩:

      • 不压缩NONE,如上图
      • 压缩RECORD,如上图
      • 压缩BLOCK,如下图,①一次性压缩多条记录;②每一个新块Block开始处都需要插入同步点
    • 在大多数情况下,以block(注意:指的是SequenceFile中的block)为单位进行压缩是最好的选择

    • 因为一个block包含多条记录,利用record间的相似性进行压缩,压缩效率更高

    • 把已有的数据转存为SequenceFile比较慢。比起先写小文件,再将小文件写入SequenceFile,一个更好的选择是直接将数据写入一个SequenceFile文件,省去小文件作为中间媒介.

Image201907101935

  • 向SequenceFile写入数据
package com.kkb.hdfs.sequencefile;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.FileSystem;import org.apache.hadoop.fs.Path;import org.apache.hadoop.io.IOUtils;import org.apache.hadoop.io.IntWritable;import org.apache.hadoop.io.SequenceFile;import org.apache.hadoop.io.Text;import java.io.IOException;import java.net.URI;public class WriteSequenceFile {    //模拟数据源    private static final String[] DATA = {            "The Apache Hadoop software library is a framework that allows for the distributed processing of large data sets across clusters of computers using simple programming models.",            "It is designed to scale up from single servers to thousands of machines, each offering local computation and storage.",            "Rather than rely on hardware to deliver high-availability, the library itself is designed to detect and handle failures at the application layer",            "o delivering a highly-available service on top of a cluster of computers, each of which may be prone to failures.",            "Hadoop Common: The common utilities that support the other Hadoop modules."    };    public static void main(String[] args) throws IOException {        //输出路径:要生成的SequenceFile文件名        String uri = "hdfs://node01:8020/SequenceFile.txt";        Configuration conf = new Configuration();        FileSystem fs = FileSystem.get(URI.create(uri), conf);        //向HDFS上的此SequenceFile文件写数据        Path path = new Path(uri);        //因为SequenceFile每个record是键值对的        //指定key类型        IntWritable key = new IntWritable();        //指定value类型        Text value = new Text();        //创建向SequenceFile文件写入数据时的一些选项        //要写入的SequenceFile的路径        SequenceFile.Writer.Option pathOption       = SequenceFile.Writer.file(path);        //record的key类型选项        SequenceFile.Writer.Option keyOption        = SequenceFile.Writer.keyClass(IntWritable.class);        //record的value类型选项        SequenceFile.Writer.Option valueOption      = SequenceFile.Writer.valueClass(Text. );        //SequenceFile压缩方式:NONE | RECORD | BLOCK三选一        //方案一:RECORD、不指定压缩算法        SequenceFile.Writer.Option compressOption   = SequenceFile.Writer.compression(SequenceFile.CompressionType.RECORD);        SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressOption);        //方案二:BLOCK、不指定压缩算法        //       SequenceFile.Writer.Option compressOption   = SequenceFile.Writer.compression(SequenceFile.CompressionType.BLOCK);        //       SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressOption);        //方案三:使用BLOCK、压缩算法BZip2Codec;压缩耗时间        //再加压缩算法        //       BZip2Codec codec = new BZip2Codec();        //       codec.setConf(conf);        //       SequenceFile.Writer.Option compressAlgorithm = SequenceFile.Writer.compression(SequenceFile.CompressionType.RECORD, codec);        //       //创建写数据的Writer实例        //       SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressAlgorithm);        for (int i = 0; i < 10000; i++) {            //分别设置key、value值            key.set(100 - i);            //从数组DATA中取一个字符串,作为value的值            value.set(DATA[i % DATA.length]);            System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key, value);            //在SequenceFile末尾追加内容            writer.append(key, value);        }        //关闭流        IOUtils.closeStream(writer);    }}
  • 命令查看SequenceFile内容
hadoop fs -text /writeSequenceFile
  • 读取SequenceFile文件
package com.kkb.hdfs.sequencefile;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.Path;import org.apache.hadoop.io.IOUtils;import org.apache.hadoop.io.SequenceFile;import org.apache.hadoop.io.Writable;import org.apache.hadoop.util.ReflectionUtils;import java.io.IOException;public class ReadSequenceFile {    public static void main(String[] args) throws IOException {        //要读的SequenceFile        String uri = "hdfs://node01:8020/SequenceFile.txt";        Configuration conf = new Configuration();        Path path = new Path(uri);        //Reader对象        SequenceFile.Reader reader = null;        try {            //读取SequenceFile的Reader的路径选项            SequenceFile.Reader.Option pathOption = SequenceFile.Reader.file(path);            //实例化Reader对象            reader = new SequenceFile.Reader(conf, pathOption);            //根据反射,求出key类型            Writable key = (Writable)                    ReflectionUtils.newInstance(reader.getKeyClass(), conf);            //根据反射,求出value类型            Writable value = (Writable)                    ReflectionUtils.newInstance(reader.getValueClass(), conf);            long position = reader.getPosition();            System.out.println(position);            while (reader.next(key, value)) {                // Returns true if the previous call to next passed a sync mark                String syncSeen = reader.syncSeen() ? "*" : "";                System.out.printf("[%s%s]\t%s\t%s\n", position, syncSeen, key, value);                // 下一条record的开始位置                position = reader.getPosition();            }        } finally {            IOUtils.closeStream(reader);        }    }}

10. hdfs的其他功能介绍

10.1. 多个集群之间的数据拷贝

  • 在我们实际工作当中,极有可能会遇到将测试集群的数据拷贝到生产环境集群,或者将生产环境集群的数据拷贝到测试集群,那么就需要我们在多个集群之间进行数据的远程拷贝,hadoop自带也有命令可以帮我们实现这个功能
  • 1、本地文件拷贝scp
cd /kkb/soft
scp -r jdk-8u141-linux-x64.tar.gz hadoop@node02:/kkb/soft
  • 2、集群之间的数据拷贝distcp
cd /kkb/install/hadoop-2.6.0-cdh5.14.2/
bin/hadoop distcp hdfs://node01:8020/jdk-8u141-linux-x64.tar.gz hdfs://cluster2:8020/

10.2. hdfs快照snapShot管理

  • 快照顾名思义,就是相当于对我们的hdfs文件系统做一个备份,我们可以通过快照对我们指定的文件夹设置备份,但是添加快照之后,并不会立即复制所有文件,而是指向同一个文件。当写入发生时,才会产生新文件

1). 快照使用基本语法

 1、开启指定目录的快照功能
 hdfs dfsadmin -allowSnapshot 路径 
 
 2、禁用指定目录的快照功能(默认就是禁用状态)
 hdfs dfsadmin -disallowSnapshot 路径
 
 3、给某个路径创建快照snapshot
 hdfs dfs -createSnapshot 路径
 
 4、指定快照名称进行创建快照snapshot
 hdfs dfs -createSanpshot 路径 名称    
 
 5、给快照重新命名
 hdfs dfs -renameSnapshot 路径 旧名称 新名称
 
 6、列出当前用户所有可快照目录
 hdfs lsSnapshottableDir  
 
 7、比较两个快照的目录不同之处
 hdfs snapshotDiff 路径1 路径2
 
 8、删除快照snapshot
 hdfs dfs -deleteSnapshot <path> <snapshotName> 

2). 快照操作实际案例

1、开启与禁用指定目录的快照

[hadoop@node01 ~]hdfs dfsadmin -allowSnapshot /user
 Allowing snaphot on /user succeeded
 
 [hadoop@node01 ~]# hdfs dfsadmin -disallowSnapshot /user
 Disallowing snaphot on /user succeeded

2、对指定目录创建快照

注意:创建快照之前,先要允许该目录创建快照

[hadoop@node01 ~]# hdfs dfsadmin -allowSnapshot /user
 Allowing snaphot on /user succeeded

[hadoop@node01 ~]# hdfs dfs -createSnapshot /user  
 Created snapshot /user/.snapshot/s20190317-210906.549

通过web浏览器访问快照

http://node01:9870/explorer.html#/user/.snapshot/s20190317-210906.549

3、指定名称创建快照

[hadoop@node01 ~]# hdfs dfs -createSnapshot /user mysnap1 Created snapshot /user/.snapshot/mysnap1

4、重命名快照

hdfs  dfs -renameSnapshot /user mysnap1 mysnap2

5、列出当前用户所有可以快照的目录

hdfs lsSnapshottableDir

6、比较两个快照不同之处

hdfs dfs -createSnapshot /user snap1hdfs dfs -createSnapshot /user snap2hdfs snapshotDiff /user snap1 snap2

7、删除快照

hdfs dfs -deleteSnapshot /user snap1

10.3. hdfs回收站

  • 任何一个文件系统,基本上都会有垃圾桶机制,也就是删除的文件,不会直接彻底清掉,我们一把都是将文件放置到垃圾桶当中去,过一段时间之后,自动清空垃圾桶当中的文件,这样对于文件的安全删除比较有保证,避免我们一些误操作,导致误删除文件或者数据

1、回收站配置两个参数

 默认值fs.trash.interval=0,0表示禁用回收站,可以设置删除文件的存活时间。
 默认值fs.trash.checkpoint.interval=0,检查回收站的间隔时间。
 要求fs.trash.checkpoint.interval <= fs.trash.interval。

2、启用回收站

修改所有服务器的core-site.xml配置文件

<!-- 开启hdfs的垃圾桶机制,删除掉的数据可以从垃圾桶中回收,单位分钟 -->
<property>
	<name>fs.trash.interval</name>
	<value>10080</value>
</property>

3、查看回收站

​ 回收站在集群的/user/hadoop/.Trash/ 这个路径下

4、通过javaAPI删除的数据,不会进入回收站,需要调用moveToTrash()才会进入回收站

Trash trash = New Trash(conf);
trash.moveToTrash(path);

5、恢复回收站数据

hdfs dfs -mv trashFileDir  hdfsdirtrashFileDir :回收站的文件路径hdfsdir  :将文件移动到hdfs的哪个路径下

6、清空回收站

hdfs dfs -expunge

11.mapreduce

11.1 mapreduce的定义

  • MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。

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

11.2. mapreduce的核心思想

  • MapReduce思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。
  • 即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
  • Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
  • Reduce负责“合”,即对map阶段的结果进行全局汇总。
  • 这两个阶段合起来正是MapReduce思想的体现。
  • 还有一个比较形象的语言解释MapReduce:  
    • 例子一:我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就越快。
    • 然后把所有人的统计数加在一起。这就是“Reduce”。
    • 例子二:电影黑客帝国当中,特工”(Agents),Smith(史密斯)对付救世主Neo打不过怎么办?一个人打不过,就复制十个出来,十个不行就复制一百个

11.3. MapReduce编程模型

  • MapReduce是采用一种分而治之的思想设计出来的分布式计算框架

  • 那什么是分而治之呢?

    • 比如一复杂、计算量大、耗时长的的任务,暂且称为“大任务”;
    • 此时使用单台服务器无法计算或较短时间内计算出结果时,可将此大任务切分成一个个小的任务,小任务分别在不同的服务器上并行的执行;
    • 最终再汇总每个小任务的结果
  • MapReduce由两个阶段组成:

    • Map阶段(切分成一个个小的任务)
    • Reduce阶段(汇总小任务的结果)

image-20200421151347545

1). Map阶段

  • map阶段有一个关键的map()函数;

  • 此函数的输入是键值对

  • 输出是一系列键值对,输出写入本地磁盘。

2). Reduce阶段

  • reduce阶段有一个关键的函数reduce()函数

  • 此函数的输入也是键值对(即map的输出(kv对))

  • 输出也是一系列键值对,结果最终写入HDFS

3). Map&Reduce

image-20200421151527669

11.4. mapreduce编程指导思想

  • mapReduce编程模型的总结:

  • MapReduce的开发一共有八个步骤其中map阶段分为2个步骤,shuffle阶段4个步骤,reduce阶段分为2个步骤

1). Map阶段2个步骤

  • 第一步:设置inputFormat类,将数据切分成key,value对,输入到第二步

  • 第二步:自定义map逻辑,处理我们第一步的输入kv对数据,然后转换成新的key,value对进行输出

2). shuffle阶段4个步骤

  • 第三步:对上一步输出的key,value对进行分区。(相同key的kv对属于同一分区)

  • 第四步:对每个分区的数据按照key进行排序

  • 第五步:对分区中的数据进行规约(combine操作),降低数据的网络拷贝(可选步骤)

  • 第六步:对排序后的kv对数据进行分组;分组的过程中,key相同的kv对为一组;将同一组的kv对的所有value放到一个集合当中(每组数据调用一次reduce方法)

3). reduce阶段2个步骤

  • 第七步:对多个map的任务进行合并,排序,写reduce函数自己的逻辑,对输入的key,value对进行处理,转换成新的key,value对进行输出

  • 第八步:设置将输出的key,value对数据保存到文件中

11.5. hadoop当中常用的数据类型

hadoop没有沿用java当中基本的数据类型,而是自己进行封装了一套数据类型,其自己封装的类型与java的类型对应如下:

Java类型 Hadoop Writable类型
Boolean BooleanWritable
Byte ByteWritable
Int IntWritable
Float FloatWritable
Long LongWritable
Double DoubleWritable
String Text
Map MapWritable
Array ArrayWritable
byte[] BytesWritable

11.6. mapreduce编程入门案例之单词计数统计实现

  • 需求:现有数据格式如下,每一行数据之间都是使用逗号进行分割,求取每个单词出现的次数
hello,hello
world,world
hadoop,hadoop
hello,world
hello,flume
hadoop,hive
hive,kafka
flume,storm
hive,oozie

第一步:创建maven工程并导入以下jar包

    <properties>
        <hadoop.version>3.1.4</hadoop.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>${hadoop.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>${hadoop.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-core</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <!--   <verbal>true</verbal>-->
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <minimizeJar>true</minimizeJar>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

第二步:定义mapper类

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;

public class WordCountMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
    /**
     * 覆写map方法
     * @param key 行偏移量,没啥用
     * @param value 单行字符串
     * @param context 输出内容对象
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] strings = value.toString().split(",");
        for (String string : strings) {
            context.write(new Text(string), new IntWritable(1));
        }
    }
}

第三步:定义reducer类

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
    /**
     * 覆写reduce方法
     * @param key 单词文本
     * @param values 同一个key下单词出现的次数组成的迭代器Iterable
     * @param context 输出内容对象
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int result = 0;
        for (IntWritable value : values) {
            result += value.get();
        }
        context.write(key, new IntWritable(result));
    }
}

第四步:组装main程序

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
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.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;


public class WordCount extends Configured implements Tool {

    @Override
    public int run(String[] args) throws Exception {
        /**
         * 定义k1 v1 k2 v2,
         * k1:行偏移量(当前行行首所处文本的偏移量, Long->LongWritable类型);v1:当前行的字符串(String->Text)
         * k2: 单词字符串(String->Text), v2:某一行中单词出现的次数(Int->IntWritable)
         * k3: reduce汇总后输出单词字符串(String->Text), v3:reduce汇总后单词出现的次数(Int->IntWritable)
         * map泛型:k1,v1,k2,v2类型<LongWritable,Text,Text,IntWritable>
         * reduce泛型:k2,v2,k3,v3类型<Text,IntWritable,Text,IntWritable>
         *
         * 1.读取文本文件,解析出key,value对,k1  v1
         * 2.自定义map逻辑,接收k1 v1转换成k2, v2
         * 2-6步(shuffle过程):分区,排序,规约,分组都省略。
         * 7.自定义reduce逻辑,接受k2,v2转换成k3,v3
         * 8.输出k3,v3到文本文件
         */

        // 获取job对象
        Configuration conf = super.getConf();
        Job job = Job.getInstance(conf, "wordCount");

        // 定义输入路径和输出路径
//        Path inputPath = new Path("file:///E:\\1.txt");
        Path inputPath = new Path(args[0]);
//        Path outputPath = new Path("file:///E:\\out");
        Path outputPath = new Path(args[1]);

        // 输出路径存在会报错,需要提前判断,存在则删除
        FileSystem fs = FileSystem.get(conf);
        if(fs.exists(outputPath)){
            fs.delete(outputPath, true);
        }

        // 打包到hdfs需要指定入口class文件
        job.setJarByClass(WordCount.class);

        // 1.读取文本文件,解析成 k1:行偏移量  v1:一行文本内容
        // TextInputFormat继承自FileInputFormat<LongWritable, Text>,已经实现了文本按行解析
        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job, inputPath);

        // 2.自定义map逻辑,接受k1,v1转换成为新的k2,v2输出
        job.setMapperClass(WordCountMapper.class);
        // 设置MapperClass的输出k2,v2类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 3-6步(shuffle过程):分区,排序,规约,分组都省略。实现将map输出的k2,v2分组,同一个k2下的所有v2组合成一个Iterable,传入reduce

        // 7.自定义reduce逻辑,输入来自mapper的输出k2,v2,逻辑框架默认写好了
        job.setReducerClass(WordCountReduce.class);
        // 设置ReducerClass的输出k3,v3类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 8.输出k3  v3 进行保存
        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job, outputPath);

        // 设置reduceTask的个数
        job.setNumReduceTasks(3);

        // 提交job
        boolean b = job.waitForCompletion(true);
        return b ? 0 : 1;
    }

    public static void main(String[] args) throws Exception {
        Configuration configuration = new Configuration();
        // 提交run方法之后,得到一个程序的退出状态码
        int run = ToolRunner.run(configuration, new WordCount(), args);
        // 根据我们程序的退出状态码,退出整个进程
        System.exit(run);
    }
}
  • 本地运行
  • 集群运行
    • 打包:mvn clean,跳过test,mvn package
    • 上传:取target目录下original-TestMapReduce-1.0-SNAPSHOT.jar(不包含依赖项),拷贝到服务器
    • 执行:yarn jar original-TestMapReduce-1.0-SNAPSHOT.jar WordCount hdfs://node01:8020/geliang/1.txt hdfs://node01:8020/geliang/out

11.7. Map Task数量及切片机制

1). MapTask个数

map个数

  • 在运行我们的MapReduce程序的时候,我们可以清晰的看到会有多个mapTask的运行
    • 那么maptask的个数究竟与什么有关
    • 是不是maptask越多越好,或者说是不是maptask的个数越少越好呢???
    • 我们可以通过MapReduce的源码进行查看mapTask的个数究竟是如何决定的
  • 在MapReduce当中,每个mapTask处理一个切片split的数据量,注意切片与block块的概念很像,但是block块是HDFS当中存储数据的单位,切片split是MapReduce当中每个MapTask处理数据量的单位。
  • MapTask并行度决定机制
    • 数据块:Block是HDFS物理上把数据分成一块一块。
    • 数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储
    • 查看FileInputFormat的源码,里面getSplits的方法便是获取所有的切片,其中有个方法便是获取切片大小

split计算

  • 切片大小的计算公式:
Math.max(minSize, Math.min(maxSize, blockSize));   mapreduce.input.fileinputformat.split.minsize=1 默认值为1  mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue  blockSize为128M 
  • 由以上计算公式可以推算出split切片的大小刚好与block块相等

  • 那么hdfs上面如果有以下两个文件,文件大小分别为300M和10M,那么会启动多少个MapTask???

    1、输入文件两个

file1.txt    300Mfile2.txt    10M

​ 2、经过FileInputFormat的切片机制运算后,形成的切片信息如下:

file1.txt.split1-- 0~128file1.txt.split2-- 128~256file1.txt.split3-- 256~300file2.txt.split1-- 0~10M

​ 一共就会有四个切片,与我们block块的个数刚好相等

  • 如果有1000个小文件,每个小文件是1kb-100MB之间,那么我们启动1000个MapTask是否合适,该如何合理的控制MapTask的个数???

2). 如何控制mapTask的个数

  • 如果需要控制maptask的个数,我们只需要调整maxSize和minsize这两个值,那么切片的大小就会改变,切片大小改变之后,mapTask的个数就会改变

  • maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。

  • minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。

11.8. 自定义InputFormat

inputformat

  • mapreduce框架当中已经给我们提供了很多的文件输入类,用于处理文件数据的输入,如果以上提供的文件数据类还不够用的话,我们也可以通过自定义InputFormat来实现文件数据的输入

  • 需求:现在有大量的小文件,我们通过自定义InputFormat实现将小文件全部读取,然后输出成为一个SequenceFile格式的大文件,进行文件的合并

1).自定义InputFormat

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import java.io.IOException;

public class MyInputFormat extends FileInputFormat<NullWritable, BytesWritable> {

    @Override
    public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        MyRecordReader myRecordReader = new MyRecordReader();
        myRecordReader.initialize(split, context);
        return myRecordReader;
    }

    /**
     * 注意这个方法,决定我们的文件是否可以切分,如果不可切分,直接返回false
     * 到时候读取一个文件的数据的时候,一次性将此文件全部内容都读取出来
     *
     * @param context
     * @param filename
     * @return
     */
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        return false;
    }
}

2).自定义RecordReader读取数据

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

//RecordReader读取分片的数据
public class MyRecordReader extends RecordReader<NullWritable, BytesWritable> {
    //要读取的分片
    private FileSplit fileSplit;
    private Configuration configuration;
    //当前的value值
    private BytesWritable bytesWritable;

    //标记一下分片有没有被读取;默认是false
    private boolean flag = false;

    //初始化方法
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        this.fileSplit = (FileSplit) split;
        this.configuration = context.getConfiguration();
        this.bytesWritable = new BytesWritable();
    }

    /**
     * RecordReader读取分片时,先判断是否有下一个kv对,根据flag判断;
     * 如果有,则一次性的将文件内容全部读出
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if (!flag) {
            long length = fileSplit.getLength();
            byte[] splitContent = new byte[(int) length];
            //读取分片内容
            Path path = fileSplit.getPath();
            FileSystem fileSystem = path.getFileSystem(configuration);
            FSDataInputStream inputStream = fileSystem.open(path);

            //split内容写入splitContent
            IOUtils.readFully(inputStream, splitContent, 0, (int) length);
            //当前value值
            bytesWritable.set(splitContent, 0, (int) length);
            flag = true;

            IOUtils.closeStream(inputStream);
            //fileSystem.close();
            
            return true;
        }
        return false;
    }

    /**
     * 获取当前键值对的键
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public NullWritable getCurrentKey() throws IOException, InterruptedException {
        return NullWritable.get();
    }

    /**
     * 获取当前键值对的值
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return bytesWritable;
    }

    /**
     * 读取分片的进度
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return flag ? 1.0f : 0.0f;
    }

    //释放资源
    @Override
    public void close() throws IOException {

    }
}

3).自定义mapper类

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

public class MyMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {
    /**
     * @param key
     * @param value   小文件的全部内容
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
        //文件名
        FileSplit inputSplit = (FileSplit) context.getInputSplit();
        String name = inputSplit.getPath().getName();
        context.write(new Text(name), value);
    }
}

4).定义main方法

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

public class MyInputFormatMain extends Configured implements Tool {
    @Override
    public int run(String[] args) throws Exception {
        Job job = Job.getInstance(super.getConf(), "mergeSmallFile");
        //如果要集群运行,需要加
        job.setJarByClass(MyInputFormatMain.class);

        job.setInputFormatClass(MyInputFormat.class);
        MyInputFormat.addInputPath(job, new Path(args[0]));

        job.setMapperClass(MyMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);

        //没有reduce。但是要设置reduce的输出的k3   value3 的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);

        //将我们的文件输出成为SequenceFile这种格式
        job.setOutputFormatClass(SequenceFileOutputFormat.class);
        SequenceFileOutputFormat.setOutputPath(job, new Path(args[1]));

        boolean b = job.waitForCompletion(true);
        return b ? 0 : 1;
    }

    public static void main(String[] args) throws Exception {
        int run = ToolRunner.run(new Configuration(), new MyInputFormatMain(), args);
        System.exit(run);
    }

}

11.9. mapreduce的partitioner详解

  • 在mapreduce执行当中,有一个默认的步骤就是partition分区;
    • 分区主要的作用就是默认将key相同的kv对数据发送到同一个分区中;
    • 在mapreduce当中有一个抽象类叫做Partitioner,默认使用的实现类是HashPartitioner,我们可以通过HashPartitioner的源码,查看到分区的逻辑如下
  • 我们MR编程的第三步就是分区;这一步中决定了map生成的每个kv对,被分配到哪个分区里
    • 那么这是如何做到的呢?
    • 要实现此功能,涉及到了分区器的概念;

1). 默认分区器HashPartitioner

  • MR框架有个默认的分区器HashPartitioner

image-20200423173136033

  • 我们能观察到:

    • HashPartitioner实现了Partitioner接口
    • 它实现了getPartition()方法
      • 此方法中对k取hash值
      • 再与MAX_VALUE按位与(确保结果是正数)
      • 结果再模上reduce任务的个数
    • 所以,能得出结论,相同的key会落入同一个分区中

wordcount_partitioner

2). 自定义分区器

  • 实际生产中,有时需要自定义分区的逻辑,让key落入我们想让它落入的分区

  • 此时就需要自定义分区器

  • 如何实现?

  • 参考默认分区器HashPartitioner

    • 自定义的分区器类,如CustomPartitioner
      • 实现接口Partitioner
      • 实现getPartition方法;此方法中定义分区的逻辑
    • main方法
      • 将自定义的分区器逻辑添加进来job.setPartitionerClass(CustomPartitioner.class)
      • 设置对应的reduce任务个数job.setNumReduceTasks(3)
  • 现有一份关于手机的流量数据,样本数据如下

    image-20200424105628141

  • 数据格式说明

    asfdsadfsdfsf

  • 需求:使用mr,实现将不同的手机号的数据划分到6个不同的文件里面去,具体划分规则如下

135开头的手机号分到一个文件里面去,136开头的手机号分到一个文件里面去,137开头的手机号分到一个文件里面去,138开头的手机号分到一个文件里面去,139开头的手机号分到一个文件里面去,其他开头的手机号分到一个文件里面去
  • 根据mr编程8步,需要实现的代码有:
    • 一、针对输入数据,设计JavaBean
    • 二、自定义的Mapper逻辑(第二步)
    • 三、自定义的分区类(第三步)
    • 四、自定义的Reducer逻辑(第七步)
    • 五、main程序入口
  • 代码实现
  • 一、针对数据文件,设计JavaBean;作为map输出的value
import org.apache.hadoop.io.Writable;import java.io.DataInput;import java.io.DataOutput;import java.io.IOException;//序列化与反序列化public class FlowBean implements Writable {    //上行包个数    private Integer upPackNum;    //下行包个数    private Integer downPackNum;    //上行总流量    private Integer upPayLoad;    //下行总流量    private Integer downPayLoad;    //反序列话的时候要用到    public FlowBean() {    }    @Override    public void write(DataOutput out) throws IOException {        //调用序列化方法时,要用与类型匹配的write方法        //记住序列化的顺序        out.writeInt(upPackNum);        out.writeInt(downPackNum);        out.writeInt(upPayLoad);        out.writeInt(downPayLoad);    }    @Override    public void readFields(DataInput in) throws IOException {        //发序列话的顺序要与序列化保持一直        //使用的方法类型要匹配        this.upPackNum = in.readInt();        this.downPackNum = in.readInt();        this.upPayLoad = in.readInt();        this.downPayLoad = in.readInt();    }    public Integer getUpPackNum() {        return upPackNum;    }    public Integer getDownPackNum() {        return downPackNum;    }    public Integer getUpPayLoad() {        return upPayLoad;    }    public Integer getDownPayLoad() {        return downPayLoad;    }    public void setUpPackNum(Integer upPackNum) {        this.upPackNum = upPackNum;    }    public void setDownPackNum(Integer downPackNum) {        this.downPackNum = downPackNum;    }    public void setUpPayLoad(Integer upPayLoad) {        this.upPayLoad = upPayLoad;    }    public void setDownPayLoad(Integer downPayLoad) {        this.downPayLoad = downPayLoad;    }    @Override    public String toString() {        return "FlowBean{" +                "upPackNum=" + upPackNum +                ", downPackNum=" + downPackNum +                ", upPayLoad=" + upPayLoad +                ", downPayLoad=" + downPayLoad +                '}';    }}
  • 二、自定义Mapper类
import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Mapper;import java.io.IOException;public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {    private FlowBean flowBean;    private Text text;    @Override    protected void setup(Context context) throws IOException, InterruptedException {        flowBean = new FlowBean();        text = new Text();    }    @Override    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {        String[] split = value.toString().split("\t");        String phoneNum = split[1];        //上行包个数        String upPackNum = split[6];        //下行包个数        String downPackNum = split[7];        //上行总流量        String upPayLoad = split[8];        //下行总流量        String downPayLoad = split[9];        text.set(phoneNum);        flowBean.setUpPackNum(Integer.parseInt(upPackNum));        flowBean.setDownPackNum(Integer.parseInt(downPackNum));        flowBean.setUpPayLoad(Integer.parseInt(upPayLoad));        flowBean.setDownPayLoad(Integer.parseInt(downPayLoad));        context.write(text, flowBean);    }}
  • 三、自定义分区
import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Partitioner;public class PartitionOwn extends Partitioner<Text, FlowBean> {    @Override    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {        String phoenNum = text.toString();        if (null != phoenNum && !phoenNum.equals("")) {            if (phoenNum.startsWith("135")) {                return 0;            } else if (phoenNum.startsWith("136")) {                return 1;            } else if (phoenNum.startsWith("137")) {                return 2;            } else if (phoenNum.startsWith("138")) {                return 3;            } else if (phoenNum.startsWith("139")) {                return 4;            } else {                return 5;            }        } else {            return 5;        }    }}
  • 自定义Reducer
import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Reducer;import java.io.IOException;public class FlowReducer extends Reducer<Text, FlowBean, Text, Text> {    @Override    protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {        //上行包个数        int upPackNum = 0;        //下行包个数        int downPackNum = 0;        //上行总流量        int upPayLoad = 0;        //下行总流量        int downPayLoad = 0;        for (FlowBean value : values) {            upPackNum += value.getUpPackNum();            downPackNum += value.getDownPackNum();            upPayLoad += value.getUpPayLoad();            downPayLoad += value.getDownPayLoad();        }        context.write(key, new Text(upPackNum + "\t" + downPackNum + "\t" + upPayLoad + "\t" + downPayLoad));    }}
  • main入口
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
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.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

public class FlowMain extends Configured implements Tool {
    @Override
    public int run(String[] args) throws Exception {
        //获取job对象
        Job job = Job.getInstance(super.getConf(), FlowMain.class.getSimpleName());
        //如果程序打包运行必须要设置这一句
        job.setJarByClass(FlowMain.class);

        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job, new Path(args[0]));

        job.setMapperClass(FlowMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        //设置使用的分区器
        job.setPartitionerClass(PartitionOwn.class);
        //reduce task个数
        job.setNumReduceTasks(Integer.parseInt(args[2]));

        job.setReducerClass(FlowReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);

        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job, new Path(args[1]));

        boolean b = job.waitForCompletion(true);
        return b ? 0 : 1;
    }


    public static void main(String[] args) throws Exception {
        Configuration configuration = new Configuration();
        configuration.set("mapreduce.framework.name", "local");
        configuration.set("yarn.resourcemanager.hostname", "local");

        int run = ToolRunner.run(configuration, new FlowMain(), args);
        System.exit(run);
    }

}
  • 注意:对于我们自定义分区的案例,必须打成jar包上传到集群上面去运行,因为我们本地已经没法通过多线程模拟本地程序运行了,将我们的数据上传到hdfs上面去,然后通过 hadoop jar提交到集群上面去运行,观察我们分区的个数与reduceTask个数的关系

  • 思考:如果手动指定6个分区,reduceTask个数设置为3个会出现什么情况

    如果手动指定6个分区,reduceTask个数设置为9个会出现什么情况

11.10. mapreduce当中的排序

1). 可排序的Key

  • 排序是MapReduce框架中最重要的操作之一。

    • MapTask和ReduceTask均会对数据按照key2进行排序(需要对谁排序,就将其定义为key2)。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。

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

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

  • 对于ReduceTask,它从每个执行完成的MapTask上远程拷贝相应的数据文件

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

2). 排序的种类

  • 1、部分排序

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

  • 2、全排序

    最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构

  • 3、辅助排序

    在Reduce端对key进行分组。应用于:在接收的key为bean对象时,如果key(bean对象)的一个或几个字段相同(全部字段比较不相同),那么这些kv对作为一组,调用一次reduce方法,可以采用分组排序

  • 4、二次排序

    • 二次排序:mr编程中,需要先按输入数据的某一列a排序,如果相同,再按另外一列b排序;比如接下来的例子

    • mr自带的类型作为key无法满足需求,往往需要自定义JavaBean作为map输出的key

    • JavaBean中,使用compareTo方法指定排序规则。

3). 二次排序

  • 数据:样本数据如下;

    每条数据有5个字段,分别是手机号、上行包总个数、下行包总个数、上行总流量、下行总流量

    image-20200424111707892

  • 需求先对下行包总个数升序排序;若相等,再按上行总流量进行降序排序

  • 根据mr编程8步,需要实现的代码有:

    • 一、针对输入数据及二次排序规则,设计JavaBean
    • 二、自定义的Mapper逻辑(第二步)
    • 三、自定义的Reducer逻辑(第七步)
    • 四、main程序入口
  • 代码实现:

  • 一、定义javaBean对象,用于封装数据及定义排序规则

import org.apache.hadoop.io.WritableComparable;import java.io.DataInput;import java.io.DataOutput;import java.io.IOException;//bean要能够可序列化且可比较,所以需要实现接口WritableComparablepublic class FlowSortBean implements WritableComparable<FlowSortBean> {    private String phone;    //上行包个数    private Integer upPackNum;    //下行包个数    private Integer downPackNum;    //上行总流量    private Integer upPayLoad;    //下行总流量    private Integer downPayLoad;    //用于比较两个FlowSortBean对象    @Override    public int compareTo(FlowSortBean o) {        //升序        int i = this.downPackNum.compareTo(o.downPackNum);        if (i == 0) {            //降序            i = -this.upPayLoad.compareTo(o.upPayLoad);        }        return i;    }    //序列化    @Override    public void write(DataOutput out) throws IOException {        out.writeUTF(phone);        out.writeInt(upPackNum);        out.writeInt(downPackNum);        out.writeInt(upPayLoad);        out.writeInt(downPayLoad);    }    //反序列化    @Override    public void readFields(DataInput in) throws IOException {        this.phone = in.readUTF();        this.upPackNum = in.readInt();        this.downPackNum = in.readInt();        this.upPayLoad = in.readInt();        this.downPayLoad = in.readInt();    }    @Override    public String toString() {        return phone + "\t" + upPackNum + "\t" + downPackNum + "\t" + upPayLoad + "\t" + downPayLoad;    }    //setter、getter方法    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }    public Integer getUpPackNum() {        return upPackNum;    }    public void setUpPackNum(Integer upPackNum) {        this.upPackNum = upPackNum;    }    public Integer getDownPackNum() {        return downPackNum;    }    public void setDownPackNum(Integer downPackNum) {        this.downPackNum = downPackNum;    }    public Integer getUpPayLoad() {        return upPayLoad;    }    public void setUpPayLoad(Integer upPayLoad) {        this.upPayLoad = upPayLoad;    }    public Integer getDownPayLoad() {        return downPayLoad;    }    public void setDownPayLoad(Integer downPayLoad) {        this.downPayLoad = downPayLoad;    }}
  • 二、自定义mapper类
import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.NullWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Mapper;import java.io.IOException;public class FlowSortMapper extends Mapper<LongWritable, Text, FlowSortBean, NullWritable> {    private FlowSortBean flowSortBean;    //初始化    @Override    protected void setup(Context context) throws IOException, InterruptedException {        flowSortBean = new FlowSortBean();    }    @Override    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {        /**         * 手机号	上行包	下行包	上行总流量	下行总流量         * 13480253104	3	3	180	180         */        String[] split = value.toString().split("\t");        flowSortBean.setPhone(split[0]);        flowSortBean.setUpPackNum(Integer.parseInt(split[1]));        flowSortBean.setDownPackNum(Integer.parseInt(split[2]));        flowSortBean.setUpPayLoad(Integer.parseInt(split[3]));        flowSortBean.setDownPayLoad(Integer.parseInt(split[4]));        context.write(flowSortBean, NullWritable.get());    }}
  • 三、自定义reducer类
import org.apache.hadoop.io.NullWritable;import org.apache.hadoop.mapreduce.Reducer;import java.io.IOException;public class FlowSortReducer extends Reducer<FlowSortBean, NullWritable, FlowSortBean, NullWritable> {    @Override    protected void reduce(FlowSortBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {        //经过排序后的数据,直接输出即可        context.write(key, NullWritable.get());    }}
  • 四、main程序入口
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

public class FlowSortMain extends Configured implements Tool {
    @Override
    public int run(String[] args) throws Exception {
        //获取job对象
        Job job = Job.getInstance(super.getConf(), "flowSort");
        //如果程序打包运行必须要设置这一句
        job.setJarByClass(FlowSortMain.class);

        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job,new Path(args[0]));

        job.setMapperClass(FlowSortMapper.class);
        job.setMapOutputKeyClass(FlowSortBean.class);
        job.setMapOutputValueClass(NullWritable.class);

        job.setReducerClass(FlowSortReducer.class);
        job.setOutputKeyClass(FlowSortBean.class);
        job.setOutputValueClass(NullWritable.class);

        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job,new Path(args[1]));

        boolean b = job.waitForCompletion(true);

        return b?0:1;
    }


    public static void main(String[] args) throws Exception {
        Configuration configuration = new Configuration();
        int run = ToolRunner.run(configuration, new FlowSortMain(), args);
        System.exit(run);
    }

}

11.11. mapreduce中的combiner

1).combiner基本介绍

  • combiner类本质也是reduce聚合,combiner类继承Reducer父类

  • combine是运行在map端的,对map task的结果做聚合;而reduce是将来自不同的map task的数据做聚合

  • 作用:

    • combine可以减少map task落盘及向reduce task传输的数据量
  • 是否可以做map端combine:

    • 并非所有的mapreduce job都适用combine,无论适不适用combine,都不能对最终的结果造成影响;比如下边求平均值的例子,就不适用适用combine
    Mapper
    3 5 7 ->(3+5+7)/3=5 
    2 6 ->(2+6)/2=4
    
    Reducer
    (3+5+7+2+6)/5=23/5    不等于    (5+4)/2=9/2
    

2).需求:

  • 对于我们前面的wordCount单词计数统计,我们加上Combiner过程,实现map端的数据进行汇总之后,再发送到reduce端,减少数据的网络拷贝

  • 自定义combiner类

    其实直接使用词频统计中的reducer类作为combine类即可

  • 在main方法中加入

 job.setCombinerClass(MyReducer.class);
  • 运行程序,观察控制台有combiner和没有combiner的异同

image-20210526095603898

11.12. mapreduce中的GroupingComparator分组详解

  • 关键类GroupingComparator
    • 是mapreduce当中reduce端决定哪些数据作为一组,调用一次reduce的逻辑
    • 默认是key相同的kv对,作为同一组;每组调用一次reduce方法;
    • 可以自定义GroupingComparator,实现自定义的分组逻辑

1). 自定义WritableComparator类

  • (1)继承WritableComparator
  • (2)重写compare()方法
@Override
public int compare(WritableComparable a, WritableComparable b) {
        // 比较的业务逻辑
        return result;
}
  • (3)创建一个构造将比较对象的类传给父类
protected OrderGroupingComparator() {
        super(OrderBean.class, true);
}

2). 需求:

  • 现在有订单数据如下
订单id 商品id 成交金额
Order_0000001 Pdt_01 222.8
Order_0000001 Pdt_05 25.8
Order_0000002 Pdt_03 322.8
Order_0000002 Pdt_04 522.4
Order_0000002 Pdt_05 822.4
Order_0000003 Pdt_01 222.8
  • 现在需要求取每个订单当中金额最大的商品信息

  • 根据mr编程8步,需要实现的代码有:

    • 一、针对输入数据及相同订单按金额降序排序,设计JavaBean
    • 二、自定义的Mapper逻辑(第二步)
    • 三、自定义分区器,相同订单分到同一区(第三步)
    • 四、自定义分区内排序(在JavaBean中已完成)(第四步)
    • 五、自定义分组,相同订单的为同一组(第六步)
    • 六、自定义的Reducer逻辑(第七步)
    • 七、main程序入口

3). 自定义OrderBean对象

import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class OrderBean implements WritableComparable<OrderBean> {
    private String orderId;
    private Double price;

    /**
     * key间的比较规则
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(OrderBean o) {
        //注意:如果是不同的订单之间,金额不需要排序,没有可比性
        int orderIdCompare = this.orderId.compareTo(o.orderId);
        if (orderIdCompare == 0) {
            //比较金额,按照金额进行倒序排序
            int priceCompare = this.price.compareTo(o.price);
            return -priceCompare;
        } else {
            //如果订单号不同,没有可比性,直接返回订单号的升序排序即可
            return orderIdCompare;
        }
    }

    /**
     * 序列化方法
     *
     * @param out
     * @throws IOException
     */
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(orderId);
        out.writeDouble(price);
    }

    /**
     * 反序列化方法
     *
     * @param in
     * @throws IOException
     */
    @Override
    public void readFields(DataInput in) throws IOException {
        this.orderId = in.readUTF();
        this.price = in.readDouble();
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return orderId + "\t" + price;
    }
}

4). 自定义mapper类:

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

public class GroupMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {

    /**
     * Order_0000001	Pdt_01	222.8
     * Order_0000001	Pdt_05	25.8
     * Order_0000002	Pdt_03	322.8
     * Order_0000002	Pdt_04	522.4
     * Order_0000002	Pdt_05	822.4
     * Order_0000003	Pdt_01	222.8
     * Order_0000003	Pdt_03	322.8
     * Order_0000003	Pdt_04	522.4
     *
     * @param key
     * @param value
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] fields = value.toString().split("\t");

        OrderBean orderBean = new OrderBean();
        orderBean.setOrderId(fields[0]);
        orderBean.setPrice(Double.valueOf(fields[2]));

        //输出orderBean
        context.write(orderBean, NullWritable.get());
    }
}

5). 自定义分区类:

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;

public class GroupPartitioner extends Partitioner<OrderBean, NullWritable> {
    @Override
    public int getPartition(OrderBean orderBean, NullWritable nullWritable, int numPartitions) {
        //将每个订单的所有的记录,传入到一个reduce当中
        return orderBean.getOrderId().hashCode() % numPartitions;
    }
}

6) 自定义分组类:

import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

public class MyGroup extends WritableComparator {
    public MyGroup() {
        //分组类:要对OrderBean类型的k进行分组
        super(OrderBean.class, true);
    }

    @Override
    public int compare(WritableComparable a, WritableComparable b) {
        OrderBean a1 = (OrderBean) a;
        OrderBean b1 = (OrderBean) b;
        //需要将同一订单的kv作为一组
        return a1.getOrderId().compareTo(b1.getOrderId());
    }
}

7). 自定义reduce类

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class GroupReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {

    /**
     * Order_0000002	Pdt_03	322.8
     * Order_0000002	Pdt_04	522.4
     * Order_0000002	Pdt_05	822.4
     * => 这一组中有3个kv
     * 并且是排序的
     * Order_0000002	Pdt_05	822.4
     * Order_0000002	Pdt_04	522.4
     * Order_0000002	Pdt_03	322.8
     *
     * @param key
     * @param values
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        //Order_0000002	Pdt_05	822.4 获得了当前订单中进而最高的商品
        context.write(key, NullWritable.get());
    }
}

8). 定义程序入口类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

/**
 * 分组求top 1
 */
public class GroupMain extends Configured implements Tool {
    @Override
    public int run(String[] args) throws Exception {
        //获取job对象
        Job job = Job.getInstance(super.getConf(), "group");
        job.setJarByClass(GroupMain.class);

        //第一步:读取文件,解析成为key,value对
        job.setInputFormatClass(TextInputFormat.class);
        TextInputFormat.addInputPath(job, new Path(args[0]));

        //第二步:自定义map逻辑
        job.setMapperClass(GroupMapper.class);
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);

        //第三步:分区
        job.setPartitionerClass(GroupPartitioner.class);

        //第四步:排序  已经做了

        //第五步:规约  combiner  省掉

        //第六步:分组   自定义分组逻辑
        job.setGroupingComparatorClass(MyGroup.class);

        //第七步:设置reduce逻辑
        job.setReducerClass(GroupReducer.class);
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);

        //第八步:设置输出路径
        job.setOutputFormatClass(TextOutputFormat.class);
        TextOutputFormat.setOutputPath(job, new Path(args[1]));

        //如果设置reduce任务数为多个,必须打包到集群运行
        //job.setNumReduceTasks(3);

        boolean b = job.waitForCompletion(true);
        return b ? 0 : 1;
    }

    public static void main(String[] args) throws Exception {
        int run = ToolRunner.run(new Configuration(), new GroupMain(), args);
        System.exit(run);
    }
}

11.13. map task工作机制

  • (1)Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value。

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

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

  • (4)Spill阶段:即“溢写”,当环形缓冲区满80%后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。

  • 溢写阶段详情:

    • 步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。

    • 步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。

    • 步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括,在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。

    • (5)合并阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。

    • 当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。

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

    • 让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

11.14. reduce task工作机制

1). reduce流程

  • (1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。

  • (2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。

  • (3)Sort阶段:当所有map task的分区数据全部拷贝完,按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。

  • (4)Reduce阶段:reduce()函数将计算结果写到HDFS上。

2). 设置ReduceTask并行度(个数)

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

    // 默认值是1,手动设置为4

    job.setNumReduceTasks(4);

3). 实验:测试ReduceTask多少合适

  • (1)实验环境:1个Master节点,16个Slave节点:CPU:8GHZ,内存: 2G

  • (2)实验结论:

  • 表4-3 改变ReduceTask (数据量为1GB)

MapTask =16
ReduceTask 1 5 10 15 16 20 25 30 45 60
总时间 892 146 110 92 88 100 128 101 145 104

11.15. mapreduce完整流程

1). map简图

map简图

2). reduce简图

reduce简图

3). mapreduce简略步骤

  • 第一步:读取文件,解析成为key,value对

  • 第二步:自定义map逻辑接受k1,v1,转换成为新的k2,v2输出;写入环形缓冲区

  • 第三步:分区:写入环形缓冲区的过程,会给每个kv加上分区Partition index。(同一分区的数据,将来会被发送到同一个reduce里面去)

  • 第四步:排序:当缓冲区使用80%,开始溢写文件

    • 先按partition进行排序,相同分区的数据汇聚到一起;
    • 然后,每个分区中的数据,再按key进行排序
  • 第五步:combiner。调优过程,对数据进行map阶段的合并(注意:并非所有mr都适合combine)

  • 第六步:将环形缓冲区的数据进行溢写到本地磁盘小文件

  • 第七步:归并排序,对本地磁盘溢写小文件进行归并排序

  • 第八步:等待reduceTask启动线程来进行拉取数据

  • 第九步:reduceTask启动线程,从各map task拉取属于自己分区的数据

  • 第十步:从mapTask拉取回来的数据继续进行归并排序

  • 第十一步:进行groupingComparator分组操作

  • 第十二步:调用reduce逻辑,写出数据

  • 第十三步:通过outputFormat进行数据输出,写到文件,一个reduceTask对应一个结果文件

12.yarn

12.1. 什么是YARN

a19a61bc-9378-3e38-944a-899a09f37908-1588057804444

  • Apache Hadoop YARN(Yet Another Resource Negotiator)是Hadoop的子项目,为分离Hadoop2.0资源管理和计算组件而引入
  • YRAN具有足够的通用性,可以支持其它的分布式计算模式

99b59921-9a97-3199-8c39-d3b77dfdceaf-1588057804445

12.2. YARN架构剖析

  • 类似HDFS,YARN也是经典的主从(master/slave)架构
    • YARN服务由一个ResourceManager(RM)和多个NodeManager(NM)构成

    • ResourceManager为主节点(master)

    • NodeManager为从节点(slave)

12.2.1 ResourceManager

  • ResourceManager是YARN中主的角色
  • RM是一个全局的资源管理器,集群只有一个active的对外提供服务
    • 负责整个系统的资源管理和分配
    • 包括处理客户端请求
    • 启动/监控 ApplicationMaster
    • 监控 NodeManager、资源的分配与调度
  • 它主要由两个组件构成:
    • 调度器(Scheduler)
    • 应用程序管理器(Applications Manager,ASM)
  • 调度器Scheduler
    • 调度器根据队列、容量等限制条件(如每个队列分配一定的资源,最多执行一定数量的作业等),将系统中的资源分配给各个正在运行的应用程序。
    • 需要注意的是,该调度器是一个“纯调度器”
      • 它不从事任何与具体应用程序相关的工作,比如不负责监控或者跟踪应用的执行状态等,也不负责重新启动因应用执行失败或者硬件故障而产生的失败任务,这些均交由应用程序相关的ApplicationMaster完成。
      • 调度器仅根据各个应用程序的资源需求进行资源分配,而资源分配单位用一个抽象概念“资源容器”(Resource Container,简称Container)表示,Container是一个动态资源分配单位,它将内存、CPU、磁盘、网络等资源封装在一起,从而限定每个任务使用的资源量。
  • 应用程序管理器Applications Manager,ASM
    • 应用程序管理器主要负责管理整个系统中所有应用程序
    • 接收job的提交请求
    • 为应用分配第一个 Container 来运行 ApplicationMaster
      • 包括应用程序提交
      • 与调度器scheduler协商资源以启动 ApplicationMaster
      • 监控 ApplicationMaster 运行状态并在失败时重新启动它等

12.2.2 NodeManager

20190103113256851

  • NodeManager 是YARN中的 slave角色

  • NodeManager :

    • 当一个节点启动时,它会向 ResourceManager 进行注册并告知 ResourceManager 自己有多少资源可用。
    • 每个计算节点,运行一个NodeManager进程,通过心跳(每秒 yarn.resourcemanager.nodemanagers.heartbeat-interval-ms )上报节点的资源状态(磁盘,内存,cpu等使用信息)
  • 功能:

    • 接收及处理来自 ResourceManager 的命令请求,分配 Container 给应用的某个任务;
    • NodeManager 监控本节点上的资源使用情况和各个 Container 的运行状态(cpu和内存等资源)
    • 负责监控并报告 Container 使用信息给 ResourceManager。
    • 定时地向RM汇报以确保整个集群平稳运行,RM 通过收集每个 NodeManager 的报告信息来追踪整个集群健康状态的,而 NodeManager 负责监控自身的健康状态;
    • 处理来自 ApplicationMaster 的请求;
    • 管理着所在节点每个 Container 的生命周期;
  • 管理每个节点上的日志;

    • 在运行期,通过 NodeManager 和 ResourceManager 协同工作,这些信息会不断被更新并保障整个集群发挥出最佳状态。
    • NodeManager 只负责管理自身的 Container,它并不知道运行在它上面应用的信息。负责管理应用信息的组件是 ApplicationMaster

12.2.3 Container

  • Container 是 YARN 中的资源抽象
    • YARN以Container为单位分配资源
    • 它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等
    • 当 AM 向 RM 申请资源时,RM 为 AM 返回的资源便是用 Container 表示的
  • YARN 会为每个任务分配一个 Container,且该任务只能使用该 Container 中指定数量的资源。
  • Container 和集群NodeManager节点的关系是:
    • 一个NodeManager节点可运行多个 Container
    • 但一个 Container 不会跨节点。
    • 任何一个 job 或 application 必须运行在一个或多个 Container 中
    • 在 Yarn 框架中,ResourceManager 只负责告诉 ApplicationMaster 哪些 Containers 可以用
    • ApplicationMaster 还需要去找 NodeManager 请求分配具体的 Container。
  • 需要注意的是
    • Container 是一个动态资源划分单位,是根据应用程序的需求动态生成的
    • 目前为止,YARN 仅支持 CPU 和内存两种资源,且使用了轻量级资源隔离机制 Cgroups 进行资源隔离。
  • 功能:
    • 对task环境的抽象;

    • 描述一系列信息;

    • 任务运行资源的集合(cpu、内存、io等);

    • 任务运行环境

12.2.4 ApplicationMaster

  • 功能:

    • 获得数据分片;
    • 为应用程序申请资源并进一步分配给内部任务(TASK);
    • 任务监控与容错;
    • 负责协调来自ResourceManager的资源,并通过NodeManager监视容器的执行和资源使用情况。
  • ApplicationMaster 与 ResourceManager 之间的通信

    • 是整个 Yarn 应用从提交到运行的最核心部分,是 Yarn 对整个集群进行动态资源管理的根本步骤
    • application master周期性的向resourcemanager发送心跳,让rm确认appmaster的健康
    • Yarn 的动态性,就是来源于多个Application 的 ApplicationMaster 动态地和 ResourceManager 进行沟通,不断地申请、释放、再申请、再释放资源的过程。

12.2.5 JobHistoryServer

作业历史服务:记录在yarn中调度的作业历史运行情况,可以通过历史任务日志服务器来查看hadoop的历史任务,出现错误都应该第一时间来查看日志

配置历史服务jobhistoryserver

1).修改mapred-site.xml

  • node01执行以下命令修改mapred-site.xml
cd /kkb/install/hadoop-3.1.4/etc/hadoop
vim mapred-site.xml
  • 增加如下内容
<property>
    <name>mapreduce.jobhistory.address</name>
    <value>node01:10020</value>
</property>

<property>
    <name>mapreduce.jobhistory.webapp.address</name>
    <value>node01:19888</value>
</property>

<property>
  <name>yarn.app.mapreduce.am.env</name>
  <value>HADOOP_MAPRED_HOME=/kkb/install/hadoop-3.1.4</value>
</property>
<property>
  <name>mapreduce.map.env</name>
  <value>HADOOP_MAPRED_HOME=/kkb/install/hadoop-3.1.4</value>
</property>
<property>
  <name>mapreduce.reduce.env</name>
  <value>HADOOP_MAPRED_HOME=/kkb/install/hadoop-3.1.4</value>
</property>



注意:如果已经存在以上两项配置,那么就不需要再进行配置了

2).修改yarn-site.xml

vim yarn-site.xml
  • 增加内容
<!-- 开启日志聚合功能,将application运行时,每个container的日志聚合到一起,保存到文件系统,一般是HDFS -->
<property>
    <name>yarn.log-aggregation-enable</name>
    <value>true</value>
</property>
<!-- 多长时间删除一次聚合产生的日志 -->
<property>
    <name>yarn.log-aggregation.retain-seconds</name>
    <value>2592000</value><!--30 day-->
</property>
<!-- 保留用户日志多少秒。只有日志聚合功能没有开启时yarn.log-aggregation-enable,才有效;现已开启 
<property>
    <name>yarn.nodemanager.log.retain-seconds</name>
    <value>604800</value>
</property>
-->
<!-- 指定聚合产生的日志的压缩算法 -->
<property>
    <name>yarn.nodemanager.log-aggregation.compression-type</name>
    <value>gz</value>
</property>
<!--  nodemanager本地文件存储目录  -->
<property>
    <name>yarn.nodemanager.local-dirs</name>
    <value>/kkb/install/hadoop-3.1.4/hadoopDatas/yarn/local</value>
</property>
<!--  resourceManager  保存完成任务的最大个数  -->
<property>
    <name>yarn.resourcemanager.max-completed-applications</name>
    <value>1000</value>
</property>
<property>
    <name>yarn.log.server.url</name>
    <value>http://node01:19888/jobhistory/logs</value>
</property>

3).同步到其他节点

  • node01服务器执行以下命令,将修改后的文件同步发送到其他服务器上面去
cd /kkb/install/hadoop-3.1.4/etc/hadoop
scp mapred-site.xml  yarn-site.xml  node02:$PWD
scp mapred-site.xml  yarn-site.xml  node03:$PWD

4).重启yarn以及jobhistory服务

  • node01执行以下命令重启yarn
cd /kkb/install/hadoop-3.1.4
sbin/stop-yarn.sh
sbin/start-yarn.sh
mapred --daemon stop historyserver
  • 启动jobhistory服务

    在yarn-site.xml中如下属性指定的节点上,运行命令启动

cd /kkb/install/hadoop-3.1.4
mapred --daemon start historyserver

Image201910211719

  • 启动成功后会出现JobHistoryServer进程(使用jps命令查看,下面会有介绍)

  • 并且可以从19888端口进行查看日志详细信息

    node01:19888
    

    点击链接,查看job日志

    Image201910202320

  • 如果没有启动jobhistoryserver,无法查看应用的日志

image-20201109134103014

  • 打开如下图界面,在下图中点击History,页面会进行一次跳转

image-20201109134153645

  • 点击History之后 跳转后的页面如下图是空白的,因为没有启动jobhistoryserver

  • jobhistoryserver启动后,在此运行MR程序,如pi

image-20201109134153645

  • 点击History连接,跳转一个崭新的页面
    • TaskType中列举的map和reduce,Total表示此次运行的mapreduce程序执行所需要的map和reduce的任务数

image-20201109134423728

  • 点击TaskType列中Map连接

image-20201109134456214

  • 看到map任务的相关信息比如执行状态,启动时间,完成时间。

image-20201109134605409

image-20201109134646501

  • 进入任务日志界面

image-20201109134758533

  • 可以使用同样的方式我们查看reduce任务执行的详细信息,这里不再赘述.

  • jobhistoryserver就是进行作业运行过程中历史运行信息的记录,方便我们对作业进行分析.

12.2.6 Timeline Server

  • 用来写日志服务数据 , 一般来写与第三方结合的日志服务数据(比如spark等)
  • 它是对jobhistoryserver功能的有效补充,jobhistoryserver只能对mapreduce类型的作业信息进行记录
  • 它记录除了jobhistoryserver能够对作业运行过程中信息进行记录之外,还记录更细粒度的信息,比如任务在哪个队列中运行,运行任务时设置的用户是哪个用户。
  • timelineserver功能更强大,但不是替代jobhistory两者是功能间的互补关系

1563006522419

12.3. YARN应用运行原理(重点 40分钟)

yarn_architecture

12.3.1 YARN应用提交过程

  • Application在Yarn中的执行过程,整个执行过程可以总结为三步:

    • 应用程序提交
    • 启动应用的ApplicationMaster实例
    • ApplicationMaster 实例管理应用程序的执行
  • 具体提交过程为:

    Image201909161351

    • 客户端程序向 ResourceManager 提交应用,并请求一个 ApplicationMaster 实例;
    • ResourceManager 找到一个可以运行一个 Container 的 NodeManager,并在这个 Container 中启动 ApplicationMaster 实例;
    • ApplicationMaster 向 ResourceManager 进行注册,注册之后客户端就可以查询 ResourceManager 获得自己 ApplicationMaster 的详细信息,以后就可以和自己的 ApplicationMaster 直接交互了(这个时候,客户端主动和 ApplicationMaster 交流,应用先向 ApplicationMaster 发送一个满足自己需求的资源请求);
    • ApplicationMaster 根据 resource-request协议 向 ResourceManager 发送 resource-request请求;
    • 当 Container 被成功分配后,ApplicationMaster 通过向 NodeManager 发送 container-launch-specification信息来启动Container,container-launch-specification信息包含了能够让Container 和 ApplicationMaster 交流所需要的资料;
    • 应用程序的代码以 task 形式在启动的 Container 中运行,并把运行的进度、状态等信息通过 application-specific协议 发送给ApplicationMaster;
    • 在应用程序运行期间,提交应用的客户端主动和 ApplicationMaster 交流获得应用的运行状态、进度更新等信息,交流协议也是 application-specific协议;
    • 应用程序执行完成并且所有相关工作也已经完成,ApplicationMaster 向 ResourceManager取消注册然后关闭,用到所有的 Container 也归还给系统。
  • 精简版的:

    • 步骤1:用户将应用程序提交到 ResourceManager 上;
    • 步骤2:ResourceManager为应用程序 ApplicationMaster 申请资源,并与某个 NodeManager 通信启动第一个 Container,以启动ApplicationMaster;
    • 步骤3:ApplicationMaster 与 ResourceManager 注册进行通信,为内部要执行的任务申请资源,一旦得到资源后,将于 NodeManager 通信,以启动对应的 Task;
    • 步骤4:所有任务运行完成后,ApplicationMaster 向 ResourceManager 注销,整个应用程序运行结束。

12.3.2 MapReduce on YARN

820234-20160604233916133-2026396104

  • 提交作业

    • ①程序打成jar包,在客户端运行hadoop jar命令,提交job到集群运行
    • job.waitForCompletion(true)中调用Job的submit(),此方法中调用JobSubmitter的submitJobInternal()方法;
      • ②submitClient.getNewJobID()向resourcemanager请求一个MR作业id
      • 检查输出目录:如果没有指定输出目录或者目录已经存在,则报错
      • 计算作业分片;若无法计算分片,也会报错
      • ③运行作业的相关资源,如作业的jar包、配置文件、输入分片,被上传到HDFS上一个以作业ID命名的目录(jar包副本默认为10,运行作业的任务,如map任务、reduce任务时,可从这10个副本读取jar包)
      • ④调用resourcemanager的submitApplication()提交作业
    • 客户端每秒查询一下作业的进度(map 50% reduce 0%),进度如有变化,则在控制台打印进度报告;
    • 作业如果成功执行完成,则打印相关的计数器
    • 但如果失败,在控制台打印导致作业失败的原因(要学会查看日志,定位问题,分析问题,解决问题)
  • 初始化作业

    • 当ResourceManager(一下简称RM)收到了submitApplication()方法的调用通知后,请求传递给RM的scheduler(调度器);调度器分配container(容器)
    • ⑤a RM与指定的NodeManager通信,通知NodeManager启动容器;NodeManager收到通知后,创建占据特定资源的container;
    • ⑤b 然后在container中运行MRAppMaster进程
    • ⑥MRAppMaster需要接受任务(各map任务、reduce任务的)的进度、完成报告,所以appMaster需要创建多个簿记对象,记录这些信息
    • ⑦从HDFS获得client计算出的输入分片split
      • 每个分片split创建一个map任务
      • 通过 mapreduce.job.reduces 属性值(编程时,jog.setNumReduceTasks()指定),知道当前MR要创建多少个reduce任务
      • 每个任务(map、reduce)有task id
  • Task 任务分配

    • 如果小作业,appMaster会以uberized的方式运行此MR作业;appMaster会决定在它的JVM中顺序执行此MR的任务;

      • 原因是,若每个任务运行在一个单独的JVM时,都需要单独启动JVM,分配资源(内存、CPU),需要时间;多个JVM中的任务再在各自的JVM中并行运行

      • 若将所有任务在appMaster的JVM中顺序执行的话,更高效,那么appMaster就会这么做 ,任务作为uber任务运行

      • 小作业判断依据:①小于10个map任务;②只有一个reduce任务;③MR输入大小小于一个HDFS块大小

      • 如何开启uber?设置属性 mapreduce.job.ubertask.enable 值为true

        configuration.set("mapreduce.job.ubertask.enable", "true");
        
      • 在运行任何task之前,appMaster调用setupJob()方法,创建OutputCommitter,创建作业的最终输出目录(一般为HDFS上的目录)及任务输出的临时目录(如map任务的中间结果输出目录)

    • ⑧若作业不以uber任务方式运行,那么appMaster会为作业中的每一个任务(map任务、reduce任务)向RM请求container

      • 由于reduce任务在进入排序阶段之前,所有的map任务必须执行完成;所以,为map任务申请容器要优先于为reduce任务申请容器

      • 5%的map任务执行完成后,才开始为reduce任务申请容器

      • 为map任务申请容器时,遵循数据本地化,调度器尽量将容器调度在map任务的输入分片所在的节点上(移动计算,不移动数据

      • reduce任务能在集群任意计算节点运行

      • 默认情况下,为每个map任务、reduce任务分配1G内存、1个虚拟内核,由属性决定mapreduce.map.memory.mb、mapreduce.reduce.memory.mb、mapreduce.map.cpu.vcores、mapreduce.reduce.reduce.cpu.vcores

  • Task 任务执行

    • 当调度器为当前任务分配了一个NodeManager(暂且称之为NM01)的容器,并将此信息传递给appMaster后;appMaster与NM01通信,告知NM01启动一个容器,并此容器占据特定的资源量(内存、CPU)
    • NM01收到消息后,启动容器,此容器占据指定的资源量
    • 容器中运行YarnChild,由YarnChild运行当前任务(map、reduce)
    • ⑩在容器中运行任务之前,先将运行任务需要的资源拉取到本地,如作业的JAR文件、配置文件、分布式缓存中的文件
  • 作业运行进度与状态更新

    • 作业job以及它的每个task都有状态(running、successfully completed、failed),当前任务的运行进度、作业计数器
    • 任务在运行期间,每隔3秒向appMaster汇报执行进度、状态(包括计数器)
    • appMaster汇总目前运行的所有任务的上报的结果
    • 客户端每隔1秒,轮询访问appMaster获得作业执行的最新状态,若有改变,则在控制台打印出来
  • 完成作业

    • appMaster收到最后一个任务完成的报告后,将作业状态设置为成功
    • 客户端轮询appMaster查询进度时,发现作业执行成功,程序从waitForCompletion()退出
    • 作业的所有统计信息打印在控制台
    • appMaster及运行任务的容器,清理中间的输出结果,释放资源
    • 作业信息被历史服务器保存,留待以后用户查询

12.3.3 YARN应用生命周期

  • RM: Resource Manager
  • AM: Application Master
  • NM: Node Manager
  1. Client向RM提交应用,包括AM程序及启动AM的命令。

  2. RM为AM分配第一个容器,并与对应的NM通信,令其在容器上启动应用的AM。

  3. AM启动时向RM注册,允许Client向RM获取AM信息然后直接和AM通信。

  4. AM通过资源请求协议,为应用协商容器资源。

  5. 如容器分配成功,AM要求NM在容器中启动应用,应用启动后可以和AM独立通信。

  6. 应用程序在容器中执行,并向AM汇报。

  7. 在应用执行期间,Client和AM通信获取应用状态。

  8. 应用执行完成,AM向RM注销并关闭,释放资源。

    申请资源->启动appMaster->申请运行任务的container->分发Task->运行Task->Task结束->回收container

12.4. YARN调度器(重点 40分钟)

12.4.1. 资源调度器的职能

  • 资源调度器是YARN最核心的组件之一,是一个插拔式的服务组件,负责整个集群资源的管理和分配。YARN提供了三种可用的资源调度器:FIFO、Capacity Scheduler、Fair Scheduler。

12.4.2. 三种调度器的介绍

1). 先进先出调度器(FIFO)

  • FIFO Scheduler把应用按提交的顺序排成一个队列,这是一个先进先出队列
    • 在进行资源分配的时候,先给队列中最头上的应用进行分配资源
    • 待最头上的应用需求满足后再给下一个分配,以此类推。
  • FIFO Scheduler是最简单也是最容易理解的调度器,也不需要任何配置,但它并不适用于共享集群。
    • 大的应用可能会占用所有集群资源,这就导致其它应用被阻塞。
    • 在共享集群中,更适合采用Capacity Scheduler或Fair Scheduler,这两个调度器都允许大任务和小任务在提交的同时获得一定的系统资源。
  • 可以看出,在FIFO 调度器中,小任务会被大任务阻塞。

2). 容量调度器(Capacity Scheduler)

3). 公平调度器(Fair Scheduler)

12.4.3. 自定义队列,实现任务提交不同队列

建议在集群上做一些没把握的事情前,先给集群虚拟机打个快照再说

  • 前面我们看到了hadoop当中有各种资源调度形式,当前hadoop的任务提交,默认提交到default队列里面去了,将所有的任务都提交到default队列,我们在实际工作当中,可以通过划分队列的形式,对不同的用户,划分不同的资源,让不同的用户的任务,提交到不同的队列里面去,实现资源的隔离
  • 资源隔离目前有2种,静态隔离和动态隔离。
  • 所谓静态隔离是以服务隔离,是通过cgroups(LINUX control groups) 功能来支持的。比如HADOOP服务包含HDFS, HBASE, YARN等等,那么我们固定的设置比例,HDFS:20%, HBASE:40%, YARN:40%, 系统会帮我们根据整个集群的CPU,内存,IO数量来分割资源,先提一下,IO是无法分割的,所以只能说当遇到IO问题时根据比例由谁先拿到资源,CPU和内存是预先分配好的。
  • 上面这种按照固定比例分割就是静态分割了,仔细想想,这种做法弊端太多,假设我按照一定的比例预先分割好了,但是如果我晚上主要跑mapreduce, 白天主要是HBASE工作,这种情况怎么办? 静态分割无法很好的支持,缺陷太大
  • 动态隔离只要是针对 YARN以及impala, 所谓动态只是相对静态来说,其实也不是动态。 先说YARN, 在HADOOP整个环境,主要服务有哪些? mapreduce(这里再提一下,mapreduce是应用,YARN是框架,搞清楚这个概念),HBASE, HIVE,SPARK,HDFS,IMPALA, 实际上主要的大概这些,很多人估计会表示不赞同,oozie, ES, storm , kylin,flink等等这些和YARN离的太远了,不依赖YARN的资源服务,而且这些服务都是单独部署就OK,关联性不大。 所以主要和YARN有关也就是HIVE, SPARK,Mapreduce。这几个服务也正式目前用的最多的(HBASE用的也很多,但是和YARN没啥关系)。
  • 根据上面的描述,大家应该能理解为什么所谓的动态隔离主要是针对YARN。好了,既然YARN占的比重这么多,那么如果能很好的对YARN进行资源隔离,那也是不错的。如果我有3个部分都需要使用HADOOP,那么我希望能根据不同部门设置资源的优先级别,实际上也是根据比例来设置,建立3个queue name, 开发部们30%,数据分析部分50%,运营部门20%。
  • 设置了比例之后,再提交JOB的时候设置mapreduce.queue.name,那么JOB就会进入指定的队列里面。默认提交JOB到YARN的时候,规则是root.users.username , 队列不存在,会自动以这种格式生成队列名称。 队列设置好之后,再通过ACL来控制谁能提交或者KIll job。
  • 从上面2种资源隔离来看,没有哪一种做的很好,如果非要选一种,我会选取后者,隔离YARN资源, 第一种固定分割服务的方式实在支持不了现在的业务
  • 需求:现在一个集群当中,可能有多个用户都需要使用,例如开发人员需要提交任务,测试人员需要提交任务,以及其他部门工作同事也需要提交任务到集群上面去,对于我们多个用户同时提交任务,我们可以通过配置yarn的多用户资源隔离来进行实现

修改调度器前,可以先看下默认情况下,mr应用提交到了哪个队列,默认提交到default队列

image-20201109150242542

1).node01编辑yarn-site.xm

  • node01修改yarn-site.xml添加以下配置
cd /kkb/install/hadoop-3.1.4/etc/hadoop
vim yarn-site.xml
  • 内容如下
<!--  指定我们的任务调度使用fairScheduler调度器  -->
<property>
	<name>yarn.resourcemanager.scheduler.class</name>
	<value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
</property>

<!--  指定我们的任务调度的配置文件路径  -->
<property>
	<name>yarn.scheduler.fair.allocation.file</name>
	<value>/kkb/install/hadoop-3.1.4/etc/hadoop/fair-scheduler.xml</value>
</property>

<!-- 是否启用资源抢占,如果启用,那么当该队列资源使用
yarn.scheduler.fair.preemption.cluster-utilization-threshold 这么多比例的时候,就从其他空闲队列抢占资源
  -->
<property>
	<name>yarn.scheduler.fair.preemption</name>
	<value>true</value>
</property>
<property>
	<name>yarn.scheduler.fair.preemption.cluster-utilization-threshold</name>
	<value>0.8f</value>
</property>

<!-- 设置为true,且没有指定队列名,提交应用到用户名同名的队列;如果设置为false或没设置,默认提交到default队列;如果在allocation文件中指定了队列提交策略,忽略此属性  -->
<property>
	<name>yarn.scheduler.fair.user-as-default-queue</name>
	<value>true</value>
	<description>default is True</description>
</property>

<!-- 是否允许在提交应用时,创建队列;如果设置为true,则允许;如果设置为false,如果要将应用提交的队列,没有在allocation文件中指定,那么则将应用提交到default队列;默认为true,如果队列防止策略在allocation文件定义过,那么忽略此属性  -->
<property>
	<name>yarn.scheduler.fair.allow-undeclared-pools</name>
	<value>false</value>
	<description>default is True</description>
</property>

2).node01添加fair-scheduler.xml配置文件

  • node01执行以下命令,添加fair-scheduler.xml的配置文件
cd /kkb/install/hadoop-3.1.4/etc/hadoop
vim fair-scheduler.xml
  • 内容如下
<?xml version="1.0"?>
<allocations>
	<!-- 每个队列中,app的默认调度策略,默认值是fair -->
	<defaultQueueSchedulingPolicy>fair</defaultQueueSchedulingPolicy>

	<user name="hadoop">
		<!-- 用户hadoop最多运行的app个数 -->
		<maxRunningApps>30</maxRunningApps>
	</user>
	<!-- 如果用户没有设置最多运行的app个数,那么用户默认运行10个 -->
	<userMaxAppsDefault>10</userMaxAppsDefault>

	<!-- 定义我们的队列  -->
	<!-- 
	weight
	资源池权重

	aclSubmitApps
	允许提交任务的用户名和组;
	格式为:“user1,user2 group1,group2” 或 “ group1,group2” -》如果只有组,那么组名前要加个空格
	如果提交应用的用户或所属组在队列的ACLs中,或在当前队列的父队列的ACLs中,那么此用户有权限提交应用到此队列
	下例:
		xiaoli有权限提交应用到队列a;xiaofan在队列a的父队列dev的acls中,所以xiaofan也有权限提交应用到队列a
		比如有队列层级root.dev.a;
		有定义
		<queue name="dev">
			...
			<aclSubmitApps>xiaofan</aclSubmitApps>
			...
			<queue name="a">
				...
				<aclSubmitApps>xiaoli</aclSubmitApps>
			</queue>
		</queue>

	aclAdministerApps
	允许管理任务的用户名和组;
	格式同上。
	 -->
	<queue name="root">
		<minResources>512mb,4vcores</minResources>
		<maxResources>102400mb,100vcores</maxResources>
		<maxRunningApps>100</maxRunningApps>
		<weight>1.0</weight>
		<schedulingMode>fair</schedulingMode>
		<aclSubmitApps> </aclSubmitApps>
		<aclAdministerApps> </aclAdministerApps>

		<queue name="default">
			<minResources>512mb,4vcores</minResources>
			<maxResources>30720mb,30vcores</maxResources>
			<maxRunningApps>100</maxRunningApps>
			<schedulingMode>fair</schedulingMode>
			<weight>1.0</weight>
			<!--  所有的任务如果不指定任务队列,都提交到default队列里面来 -->
			<aclSubmitApps>*</aclSubmitApps>
		</queue>

		<queue name="hadoop">
			<minResources>512mb,4vcores</minResources>
			<maxResources>20480mb,20vcores</maxResources>
			<maxRunningApps>100</maxRunningApps>
			<schedulingMode>fair</schedulingMode>
			<weight>2.0</weight>
			<aclSubmitApps>hadoop hadoop</aclSubmitApps>
			<aclAdministerApps>hadoop hadoop</aclAdministerApps>
		</queue>

		<queue name="develop">
			<minResources>512mb,4vcores</minResources>
			<maxResources>20480mb,20vcores</maxResources>
			<maxRunningApps>100</maxRunningApps>
			<schedulingMode>fair</schedulingMode>
			<weight>1</weight>
			<aclSubmitApps>develop develop</aclSubmitApps>
			<aclAdministerApps>develop develop</aclAdministerApps>
		</queue>

		<queue name="test1">
			<minResources>512mb,4vcores</minResources>
			<maxResources>20480mb,20vcores</maxResources>
			<maxRunningApps>100</maxRunningApps>
			<schedulingMode>fair</schedulingMode>
			<weight>1.5</weight>
			<aclSubmitApps>test1,hadoop,develop test1</aclSubmitApps>
			<aclAdministerApps>test1 group_businessC,supergroup</aclAdministerApps>
		</queue>
	</queue>
	<!-- 
		包含一系列的rule元素;这些rule元素用来告诉scheduler调度器,进来的app按照规则提交到哪个队列中
		有多个rule的话,会从上到下进行匹配;
		rule可能会带有argument;所有的rule都带有create argument,表示当前rule是否能够创建一个新队列;默认值是true
		如果rule的create设置为false,且在allocation中没有配置此队列,那么尝试匹配下一个rule
	-->
	<queuePlacementPolicy>
		<!-- app被提交到指定的队列;create为true,则创建;若为false,如果队列不存在,则不创建 -->
		<rule name="specified" create="false"/>
		<!-- app被提交到提交此app的用户所属组的组名命名的队列;如果队列不存在,则不创建 -->
		<rule name="primaryGroup" create="false" />
		<!-- 如果上边的rule都没有匹配上,则app提交到queue指定的的队列;如果没有指定queue值,默认值是root.default -->
		<rule name="default" queue="root.default"/>
	</queuePlacementPolicy>
</allocations>

3).同步到其他节点

  • 将node01修改后的yarn-site.xml和fair-scheduler.xml配置文件分发到其他服务器上面去
cd /kkb/install/hadoop-3.1.4/etc/hadoop
[hadoop@node01 hadoop]# scp yarn-site.xml  fair-scheduler.xml node02:$PWD
[hadoop@node01 hadoop]# scp yarn-site.xml  fair-scheduler.xml node03:$PWD

4).重启yarn集群

  • 修改完yarn-site.xml配置文件之后,重启yarn集群,node01执行以下命令进行重启
[hadoop@node01 hadoop]# cd /kkb/install/hadoop-3.1.4/
[hadoop@node01 hadoop-3.1.4]# sbin/stop-yarn.sh
[hadoop@node01 hadoop-3.1.4]# sbin/start-yarn.sh

技巧:yarn rmadmin -refreshQueues:已经设置集群使用fairscheduler,然后再修改fair-schduler.xml后,运行此命令,立即生效,不需要再重启集群了

5).修改任务提交的队列

  • 修改代码,添加我们mapreduce任务需要提交到哪一个队列里面去
Configuration configuration = new Configuration();

//情况1
//注释掉 configuration.set("mapreduce.job.queuename", "hadoop");
//匹配规则:<rule name="primaryGroup" create="false" />

//情况2
configuration.set("mapreduce.job.queuename", "hadoop");
//匹配规则:<rule name="specified" create="false"/>

//情况3
configuration.set("mapreduce.job.queuename", "hadoopv1");
//allocation文件中,注释掉<rule name="primaryGroup" create="false" />;刷新配置yarn rmadmin -refreshQueues
//匹配规则:<rule name="default" queue="root.default"/>

  • hive任务指定提交队列,hive-site.xml文件添加
<property>
    <name>mapreduce.job.queuename</name>
    <value>test1</value>
</property>
  • spark任务运行指定提交的队列
1- 脚本方式
--queue hadoop

2- 代码方式
sparkConf.set("spark.yarn.queue", "develop")

12.5. YARN基本使用

12.5.1 配置文件

<!-- $HADOOP_HOME/etc/hadoop/mapred-site.xml -->
<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
</configuration>
<!-- $HADOOP_HOME/etc/hadoop/yarn-site.xml -->
<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
</configuration>

12.5.2 YARN启动停止

  • 启动 ResourceManager 和 NodeManager (以下分别简称RM、NM)
#主节点运行命令
$HADOOP_HOME/sbin/start-yarn.sh
  • 停止 RM 和 NM
#主节点运行命令
$HADOOP_HOME/sbin/stop-yarn.sh
  • 若RM没有启动起来,可以单独启动
#若RM没有启动,在主节点运行命令
#过时$HADOOP_HOME/sbin/yarn-daemon.sh start resouremanager
yarn --daemon start resourcemanager

#相反,可单独关闭
#过时$HADOOP_HOME/sbin/yarn-daemon.sh stop resouremanager
yarn --daemon stop resourcemanager
  • 若NM没有启动起来,可以单独启动
#若NM没有启动,在相应节点运行命令
#过时$HADOOP_HOME/sbin/yarn-daemon.sh start nodemanager
yarn --daemon start nodemanager
#相反,可单独关闭
#过时$HADOOP_HOME/sbin/yarn-daemon.sh stop nodemanager
yarn --daemon stop nodemanager

12.5.3 YARN常用命令

1). YARN命令列表

Image201907162219

2). yarn application命令

Image201907162224

#1.查看正在运行的任务
yarn application -list
#2.杀掉正在运行任务
yarn application -kill 任务id
#3.查看节点列表
yarn node -list

Image201907162252

#4.查看节点状况;所有端口号与上图中端口号要一致(随机分配)
yarn node -status node-03:45568

Image201907171511

#5.查看yarn依赖jar的环境变量
yarn classpath

3). yarn logs

#1.查看应用的日志
yarn logs -applicationId application_1571644866572_0001

Image201910211842

12.6.总结

Image201907172014

13.hadoop的企业级调优

13.1. hdfs调优以及yarn参数调优

1). HDFS参数调优hdfs-site.xml

  • dfs.namenode.handler.count=20 * log2(Cluster Size)

    调整namenode处理客户端的线程数

    比如集群规模为8台时,此参数设置为60

    The number of Namenode RPC server threads that listen to requests from clients. If dfs.namenode.servicerpc-address is not configured then Namenode RPC server threads listen to requests from all nodes.

    NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作。对于大集群或者有大量客户端的集群来说,通常需要增大参数dfs.namenode.handler.count的默认值10。设置该值的一般原则是将其设置为集群大小的自然对数乘以20,即20logN,N为集群大小。

  • 编辑日志存储路径dfs.namenode.edits.dir设置与镜像文件存储路径dfs.namenode.name.dir尽量分开,达到最低写入延迟

  • 元数据信息fsimage多目录冗余存储配置

2). YARN参数调优yarn-site.xml

  • 根据实际调整每个节点和单个任务申请内存值

    • yarn.nodemanager.resource.memory-mb

      表示该节点上YARN可使用的物理内存总量,默认是8192(MB),注意,如果你的节点内存资源不够8GB,则需要调减小这个值,而YARN不会智能的探测节点的物理内存总量。

    • yarn.scheduler.maximum-allocation-mb

      单个任务可申请的最多物理内存量,默认是8192(MB)。

  • Hadoop宕机

    • 如果MR造成系统宕机。此时要控制Yarn同时运行的任务数,和每个任务申请的最大内存。调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是8192MB)
    • 如果写入文件过量造成NameNode宕机。那么调高Kafka的存储大小,控制从Kafka到HDFS的写入速度。高峰期的时候用Kafka进行缓存,高峰期过去数据同步会自动跟上。

13.2. mapreduce运行慢的主要原因可能有哪些

  • MapReduce程序效率的瓶颈在于两点:

  • 一、计算机性能

    CPU、内存、磁盘健康、网络

  • 二、I/O操作优化

    1. 数据倾斜

    2. map和reduce数设置不合理

    3. map运行时间太长,导致reduce等待过久

    4. 小文件过多

    5. 大量的不可分割的超大文件

    6. spill次数过多

    7. merge次数过多

13.3. mapreduce的优化方法

  • MapReduce优化方法主要从六个方面考虑:数据输入、Map阶段、Reduce阶段、IO传输、数据倾斜问题和常用的调优参数。

1). 数据输入阶段

  • 合并小文件:在执行mr任务前将小文件进行合并;因为大量的小文件会产生大量的map任务,任务都需要初始化,从而导致mr运行缓慢
  • 采用CombineTextInputFormat来作为输入,解决输入端大量小文件场景。

2). MapTask运行阶段

  • 减少spill溢写次数:通过调整mapreduce.task.io.sort.mb及mapreduce.map.sort.spill.percent参数的值,增大触发spill的内存上限,减少spill次数,从而减少磁盘io的次数
  • 减少merge合并次数:调整mapreduce.task.io.sort.factor参数,增大merge的文件数,减少merge的次数,从而缩短mr处理时间
  • 在map之后,不影响业务逻辑的情况下,先进行combine处理,减少I/O

3). ReduceTask运行阶段

  • 设置合理的map、reduce个数:两个数值都不能太小,也不能太大;
    • 太少,导致task执行时长边长;
    • 太多,导致map、reduce任务间竞争资源,造成处理超时错误。
  • 设置map、reduce共存
    • 调整mapreduce.job.reduce.slowstart.completedmaps参数(默认0.05),是map运行到一定程度后,reduce也开始运行,减少reduce的等待时间
    • 比如调到0.8
  • 规避使用reduce
    • 因为reduce在用于连接数据集的时候会产生大量的网络消耗
  • 合理设置reduce端的buffer
    • 默认情况下,数据达到一定阈值的时候,Buffer中的数据会写入磁盘,然后reduce会从磁盘中获得所有的数据。即Buffer与reduce没有关联的,中间多次写磁盘、读磁盘的过程。那么可以通过调整参数,使得Buffer中的数据可以直接输送到reduce,从而减少I/O开销;mapreduce.reduce.input.buffer.percent默认为0.0,当值大于0的时候,会保留指定比例的内存读Buffer中的数值直接拿给Reducer使用。这样一来,设置Buffer需要内存,读取数据需要内存,Reduce计算也需要内存,所以要根据作业的运行情况进行调整。

13.4. IO传输阶段

  • 进行数据压缩
    • 减少网络I/O的数据量,安装snappy和lzo压缩编码器
  • 使用SequenceFile二进制文件

13.5. 数据倾斜问题

  • 数据倾斜现象:

    • 数据频率倾斜-- 某一分区的数据量要远远大于其他分区
    • 数据大小倾斜-- 部分记录的大小远远大于平均值
  • 减少数据倾斜的方法

    • 方法1:抽样和范围分区

      对原始数据进行抽样,根据抽样数据集,预设分区边界值

    • 方法2:自定义分区

      基于map输出key的背景知识,进行自定义分区。

      例如,如果map输出键的单词来源于一本书,且其中某些专业词汇出现的次数比较多,那么就可以自定义分区将这些专业词汇发送给固定的若干reduce任务。而将其他的都发送个剩余的reduce任务

    • 方法3:Combine

      使用combine减少数据倾斜。在可能的情况下,Combine的目的是聚合并精简数据

    • 方法4:采用Map Join,尽量避免Reduce Join

13.6. 常用的调优参数

  • 资源相关参数

1). mapred-site.xml

  • 以下参数是在用户自己的MR应用程序中配置就可以生效(mapred-default.xml)
配置参数 参数说明
mapreduce.map.memory.mb 一个MapTask可使用的资源上限(单位:MB),默认为1024。如果MapTask实际使用的资源量超过该值,则会被强制杀死。
mapreduce.reduce.memory.mb 一个ReduceTask可使用的资源上限(单位:MB),默认为1024。如果ReduceTask实际使用的资源量超过该值,则会被强制杀死。
mapreduce.map.cpu.vcores 每个MapTask可使用的最多cpu core数目,默认值: 1
mapreduce.reduce.cpu.vcores 每个ReduceTask可使用的最多cpu core数目,默认值: 1
mapreduce.reduce.shuffle.parallelcopies 每个Reduce去Map中取数据的并行数。默认值是5
mapreduce.reduce.shuffle.merge.percent Buffer中的数据达到多少比例开始写入磁盘。默认值0.66
mapreduce.reduce.shuffle.input.buffer.percent Buffer大小占Reduce可用内存的比例。默认值0.7
mapreduce.reduce.input.buffer.percent 指定多少比例的内存用来存放Buffer中的数据,默认值是0.0
  • 应该在YARN启动之前就配置在服务器的配置文件中才能生效(yarn-default.xml)
配置参数 参数说明
yarn.scheduler.minimum-allocation-mb 给应用程序Container分配的最小内存,默认值:1024
yarn.scheduler.maximum-allocation-mb 给应用程序Container分配的最大内存,默认值:8192
yarn.scheduler.minimum-allocation-vcores 每个Container申请的最小CPU核数,默认值:1
yarn.scheduler.maximum-allocation-vcores 每个Container申请的最大CPU核数,默认值:32
yarn.nodemanager.resource.memory-mb 给Containers分配的最大物理内存,默认值:8192
  • Shuffle性能优化的关键参数,应在YARN启动之前就配置好(mapred-default.xml)
配置参数 参数说明
mapreduce.task.io.sort.mb Shuffle的环形缓冲区大小,默认100m
mapreduce.map.sort.spill.percent 环形缓冲区溢出的阈值,默认80%

2). 容错相关参数(MapReduce性能优化)

配置参数 参数说明
mapreduce.map.maxattempts 每个Map Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
mapreduce.reduce.maxattempts 每个Reduce Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
mapreduce.task.timeout Task超时时间,经常需要设置的一个参数,该参数表达的意思为:如果一个Task在一定时间内没有任何进入,即不会读取新的数据,也没有输出数据,则认为该Task处于Block状态,可能是卡住了,也许永远会卡住,为了防止因为用户程序永远Block住不退出,则强制设置了一个该超时时间(单位毫秒),默认是600000。如果你的程序对每条输入数据的处理时间过长(比如会访问数据库,通过网络拉取数据等),建议将该参数调大,该参数过小常出现的错误提示是“AttemptID:attempt_14267829456721_123456_m_000224_0 Timed out after 300 secsContainer killed by the ApplicationMaster.”。

13.7. hdfs小文件解决方案总结

1). 小文件的问题弊端

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

2). 小文件的解决方案

  • 优化方式:

    方式一:在数据采集的时候,就将小文件或小批数据合成大文件再上传HDFS。

    方式二:在业务处理之前,在HDFS上使用MapReduce程序对小文件进行合并。

    方式三:在MapReduce处理时,可采用CombineTextInputFormat提高效率。

  • 归档命令:hadoop archive

    高效的将小文件进行归档的工具,能将多个小文件打包成一个HAR文件,这样就减少了NameNode的内存使用

  • 利用sequence file作为小文件的容器

    Sequence File由一系列的二进制的key/value组成,如果key为文件名,value为文件内容,则可以将大量小文件合并到一个SequenceFile文件中

  • CombineFileInputFormat

    CombineFileInputFormat用于将多个文件合并成一个单独的split,并且会考虑数据的存储位置。

  • 开启jvm重用

    对于处理大量小文件的job,可以开启jvm重用,约减少40%的运行时间

    jvm重用原理:一个map运行在一个jvm上,开启重用的话,该map在jvm上运行完毕后,此jvm中会继续运行此job的其他map任务

    具体设置:mapreduce.job.jvm.numtasks值在10到20之间

posted @ 2021-05-26 18:14  傅里叶的悲伤  阅读(231)  评论(0)    收藏  举报