Hadoop3.x 三大组件详解
阅读原文时间:2022年04月27日阅读:1

Hadoop

Hadoop适合海量数据分布式存储和分布式计算

运行用户使用简单的编程模型实现跨机器集群对海量数据进行分布式计算处理

1.1 简介

Hadoop核心组件

  • HDFS (分布式文件存储系统):解决海量数据存储
  • YARN(集群资源管理和任务调度框架):解决资源任务调度
  • MapReduce(分布式计算框架):解决海量数据计算

Hadoop发展简史

  • Hadoop起源于Apache Lucen子项目:Nutch

    Nutch的设计目标是构建一个大型的全网搜索引擎

    问题:如何解决数十亿网页的存储和索引问题

  • 三篇论文Google

    • The Google file system 谷歌分布式文件系统 GFS
    • MapReduce: Simplified Data Processing on Large Clusters 谷歌分布式计算框架
    • Bigtable: A Distributed Storage System for Structured Data 谷歌结构化数据存储系统

Hadoop现状

  • HDFS处在生态圈底层与核心地位
  • YARN支撑各种计算引擎运行
  • MapReduce企业一线几乎不再直接使用,很多软件的底层依旧使用MapReduce引擎

Hadoop特性优点

  • 扩容能力(scalability):节点数量灵活变化
  • 成本低(Economical):允许通过部署普通廉价的机器组成集群
  • 效率高(efficiency):并发数据,可以在节点之间动态并行的移动数据
  • 可靠性(reliability):自动维护数据的多份复制,并且在任务失败后能自动重新部署计算任务

发行版本

  • 开源社区版:Apache开源社区发型

    • 更新快
    • 兼容稳定性不好
  • 商业发行版

    • 基于Apache开源协议
    • 稳定兼容好
    • 收费,版本更新慢

架构变迁

  • Hadoop 1.0

    • HDFS

    • MapReduce

  • Hadoop 2.0

    • HDFS

    • MapReduce

    • YARN

  • Hadoop 3.0

    着重于性能优化

    • 精简内核,类路径隔离、shell脚本重构
    • EC纠删码,多NameNode支持
    • 任务本地化优化、内存参数自动推断

1.2 安装部署

Hadoop集群整体概述

  • Hadoop集群包括两个:HDFS集群、YARN集群
  • 两个集群逻辑上分离、通常物理在一起
    • 集群互相之间没有依赖、互不影响
    • 进程部署在同一机器上
  • 两个集群都是标准的主从架构集群

HDFS集群

  • NameNode
  • DataNode
  • SecondaryNameNode

Yarn集群

  • ResourceManager
  • NodeManager

安装Hadoop

Hadoop安装包结构

集群安装部署

  • 伪分布式集群安装:一台机器

    具体安装查看官方文档 英文

  • 分布式集群安装:三台机器

    具体安装查看官方文档 英文

1.3 启动&关闭

  1. 要启动hadoop集群,首先要格式化HDFS

    $HADOOP_HOME/bin/hdfs namenode -format
  2. 启动hdfs

    方法一:在主节点上启动namenode,在每一个从节点上启动datanode

    # node1
    $HADOOP_HOME/bin/hdfs --daemon start namenode
    
    # node2 node3...
    $HADOOP_HOME/bin/hdfs --daemon start datanode
    
    # 关闭
    $HADOOP_HOME/bin/hdfs --daemon stop namenode
    $HADOOP_HOME/bin/hdfs --daemon stop datanode

    方法二:如果配置了 etc/hadoop/workers 且所有的节点都配置了ssh免密登陆,在任意一个节点上都可以启动,运行一次即可

    $HADOOP_HOME/sbin/start-dfs.sh
    
    # 关闭
    $HADOOP_HOME/sbin/stop-dfs.sh
  3. 启动YARN

    方法一:启动ResourceManager,在主角色的节点上运行,启动NodeManager,在每一个从角色上运行

    # node1
    $HADOOP_HOME/bin/yarn --daemon start resourcemanager
    
    # node2 ...
    $HADOOP_HOME/bin/yarn --daemon start nodemanager
    
    #关闭
    $HADOOP_HOME/bin/yarn --daemon stop resourcemanager
    $HADOOP_HOME/bin/yarn --daemon stop nodemanager

    方法二:如果配置了 etc/hadoop/workers 且所有的节点都配置了ssh免密登陆,在任意一个节点上运行都可以启动

    $HADOOP_HOME/sbin/start-yarn.sh
    
    #关闭
    $HADOOP_HOME/sbin/stop-yarn.sh
  4. 也可以使用一键运行的脚本开启yarn和hdfs

    $HADOOP_HOME/sbin/start-all.sh
    
    $HADOOP_HOME/sbin/stop-all.sh
  5. 开启日志服务器(可选)

    开启之前需要开启日志聚合功能,需要修改bin/yarn-site.xml,添加如下内容

    要根据自己的配置,修改服务器地址

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

    启动日志服务器

    $HADOOP_HOME/bin/mapred --daemon start historyserver
    
    #关闭
    $HADOOP_HOME/bin/mapred --daemon stop historyserver

Hadoop Distributed File System, Hadoop的分布式文件系统

2.1 概述

  • HDFS主要是解决大数据如何存储问题的。分布式意味着是HDFS是横跨在多台计算机上的存储系统。

  • HDFS是一种能够在普通硬件上运行的分布式文件系统,它是高度容错的,适应于具有大数据集的应用程序,它非 常适于存储大型数据 (比如 TB 和 PB)。

  • HDFS使用多台计算机存储文件, 并且提供统一的访问接口, 像是访问一个普通文件系统一样使用分布式文件系统

特点

  1. 分布式存储
  2. 元数据记录
  3. 分块存储
  4. 副本备份

设计目标

  • 硬件故障(Hardware Failure)是常态, HDFS可能有成百上千的服务器组成,每一个组件都有可能出现故障。因此故障检测和自动快速恢复是HDFS的核心架构目标。
  • HDFS上的应用主要是以流式读取数据(Streaming Data Access)。HDFS被设计成用于批处理,而不是用户交互式的。相较于数据访问的反应时间,更注重数据访问的高吞吐量
  • 典型的HDFS文件大小是GB到TB的级别。所以,HDFS被调整成支持大文件(Large Data Sets)。它应该提供很 高的聚合数据带宽,一个集群中支持数百个节点,一个集群中还应该支持千万级别的文件。
  • 大部分HDFS应用对文件要求的是write-one-read-many访问模型。一个文件一旦创建、写入、关闭之后就不需要修改了。这一假设简化了数据一致性问题,使高吞吐量的数据访问成为可能。
  • 移动计算的代价比之移动数据的代价低。一个应用请求的计算,离它操作的数据越近就越高效。将计算移动到数据 附近,比之将数据移动到应用所在显然更好。
  • HDFS被设计为可从一个平台轻松移植到另一个平台。这有助于将HDFS广泛用作大量应用程序的首选平台

应用场景

主要特性

  • 主从架构

    • HDFS集群是标准的master/slave主从架构集群。
    • 一般一个HDFS集群是有一个Namenode和一定数目的Datanode组成。
    • Namenode是HDFS主节点,Datanode是HDFS从节点,两种角色各司其职,共同协调完成分布式的文件存储服 务。
    • 官方架构图中是一主五从模式,其中五个从角色位于两个机架(Rack)的不同服务器上。
  • 分块存储

    • HDFS中的文件在物理上是分块存储(block)的,默认大小是128M(134217728),不足128M则本身就是一块 。
    • 块的大小可以通过配置参数来规定,参数位于hdfs-default.xml中:dfs.blocksize
  • 副本机制

    • 文件的所有block都会有副本。副本系数可以在文件创建的时候指定,也可以在之后通过命令改变。
    • 副本数由参数dfs.replication控制,默认值是3,也就是会额外再复制2份,连同本身总共3份副本。
  • 元数据记录

    在HDFS中,Namenode管理的元数据具有两种类型:

    • 文件自身属性信息 文件名称、权限,修改时间,文件大小,复制因子,数据块大小。

    • 文件块位置映射信息 记录文件块和DataNode之间的映射信息,即哪个块位于哪个节点上。

  • 抽象统一的目录树结构(namespace)

    • HDFS支持传统的层次型文件组织结构。用户可以创建目录,然后将文件保存在这些目录里。文件系统名字空间的 层次结构和大多数现有的文件系统类似:用户可以创建、删除、移动或重命名文件。

    • Namenode负责维护文件系统的namespace名称空间,任何对文件系统名称空间或属性的修改都将被Namenode 记录下来。

    • HDFS会给客户端提供一个统一的抽象目录树,客户端通过路径来访问文件,形如:hdfs://namenode:port/dira/dir-b/dir-c/file.data

  • 数据库存储

    • 文件的各个block的具体存储管理由DataNode节点承担
    • 每一个block都可以在多个DataNode上存储。

2.2 HDFS Shell操作

简介

  • 命令行界面(英语:command-line interface,缩写:CLI),是指用户通过键盘输入指令,计算机接收到指令后 ,予以执行一种人际交互方式。
  • Hadoop提供了文件系统的shell命令行客户端: hadoop fs [generic options]

文件系统协议

  • HDFS Shell CLI支持操作多种文件系统,包括本地文件系统(file:///)、分布式文件系统(hdfs://nn:8020)
  • 具体操作的是什么文件系统取决于命令中文件路径URL中的前缀协议。
  • 如果没有指定前缀,则将会读取环境变量中的fs.defaultFS属性,以该属性值作为默认文件系统。

区别

  • hadoop dfs只能操作HDFS文件系统(包括与Local FS间的操作),不过已经Deprecated
  • hdfs dfs 只能操作HDFS文件系统相关(包括与Local FS间的操作),常用
  • hadoop fs可操作任意文件系统,不仅仅是hdfs文件系统,使用范围更广

目前版本来看,官方最终推荐使用的是hadoop fs。当然hdfs dfs在市面上的使用也比较多。

参数说明

  • HDFS文件系统的操作命令很多和Linux类似,因此学习成本相对较低。
  • 可以通过hadoop fs -help命令来查看每个命令的详细用法。

操作命令

hadoop fs -xxx [x] <path> ...
  • ls :查询指定路径信息

    hadoop fs -ls [-h] [-R] [<path> ...]
    • [-h]:人性化显示文件size
    • [-R] :递归查看指定目录及其子目录
  • put:从本地上传文件

    hadoop fs -put [-f] [-p] <localsrc> ... <dst>
    • [-f] 覆盖目标文件(已存在下)
    • [-p] 保留访问和修改时间,所有权和权限
    • localsrc本地文件系统(客户端所在机器)
    • dst目标文件系统(HDFS)
  • get:下载文件到本地

    hadoop fs -get [-f] [-p] <src> ... <localdst>
    • 下载文件到本地文件系统指定目录,localdst必须是目录
    • -f 覆盖目标文件(已存在下)
    • -p保留访问和修改时间,所有权和权限
  • cat:查看HDFS文件内容

    hadoop fs -cat <src> ...
  • cp

    hadoop fs -cp [-f] <src> ... <dst>
  • mkdir:创建文件夹

    • [-p]:递归创建文件夹
  • rm [-r]:删除文件/文件夹

  • apped:追加文件

    hadoop fs -appendToFile <localsrc> ... <dst>
    • 将所有给定本地文件的内容追加到给定dst文件

    • dst如果文件不存在,将创建该文件

    • 如果<localSrc>-,则输入为从标准输入中读取

    • 适合小文件合并

      #追加内容到文件尾部 appendToFile
      [root@node3 ~]# echo 1 >> 1.txt
      [root@node3 ~]# echo 2 >> 2.txt
      [root@node3 ~]# echo 3 >> 3.txt
      [root@node3 ~]# hadoop fs -put 1.txt /
      [root@node3 ~]# hadoop fs -cat /1.txt
      1
      [root@node3 ~]# hadoop fs -appendToFile 2.txt 3.txt /1.txt
      [root@node3 ~]# hadoop fs -cat /1.txt
      1
      2
      3

命令官方指导文档

示例

# 完整命令
bin/hadoop fs -xxx scheme://authority/path

# 显示文件
hadoop fs -ls /
# 上传文件
hadoop fs -put readme.txt /
# 显示文件
hadoop fs -ls /
# Found 1 items
# -rw-r--r--   1 root supergroup          0 2022-04-18 21:37 /readme.txt
# 查看文件内容
hadoop fs -cat /readme.txt
# 下载文件到本地
hadoop fs -get /readme.txt read.txt

# 创建文件夹(-p递归创建目录)
hadoop fs -mkdir /test
hadoop fs -mkdir -p /test2/cur/

统计文件数量

hadoop fs -ls / | grep / | wc -l

统计文件大小

hadoop fs -ls / | grep / | awk '{print $8,$5}'
/readme.txt 0
/test 0

2.3 节点概述

架构图

NameNode

主角色

  • NameNode是Hadoop分布式文件系统的核心,架构中的主角色。
  • NameNode维护和管理文件系统元数据,包括名称空间目录树结构、文件和块的位置信息、访问权限等信息。
  • 基于此,NameNode成为了访问HDFS的唯一入口
  • NameNode内部通过内存和磁盘文件两种方式管理元数据。
  • 其中磁盘上的元数据文件包括
    • Fsimage内存元数据镜像文件
    • edits log(Journal)编辑日志,记录用户的操作

DataNode

从角色

  • DataNode是Hadoop HDFS中的从角色,负责具体的数据块存储
  • DataNode的数量决定了HDFS集群的整体数据存储能力。通过和NameNode配合维护着数据块。
  • 多副本机制:默认为3

SecondaryNameNode

  • Secondary NameNode充当NameNode的辅助节点,但不能替代NameNode。
  • 主要是帮助主角色进行元数据文件的合并动作。可以通俗的理解为主角色的“秘书
  • 负责定期的把edits文件中的内容合并到fsimage中,合并操作称为checkpoint,在合并的时候会对edits中的内容进行转换,生成新的内容保存到fsimage文件中
  • SecondaryNameNode进程并不是必须的。

NameNode职责

  • NameNode仅存储HDFS的元数据:文件系统中所有文件的目录树,并跟踪整个集群中的文件,不存储实际数据。
  • NameNode知道HDFS中任何给定文件的块列表及其位置。使用此信息NameNode知道如何从块中构建文件。
  • NameNode不持久化存储每个文件中各个块所在的datanode的位置信息,这些信息会在系统启动时从DataNode重建。
  • NameNode是Hadoop集群中的单点故障
  • NameNode所在机器通常会配置有大量内存(RAM)

DataNode职责

  • DataNode负责最终数据块block的存储。是集群的从角色,也称为Slave。
  • DataNode启动时,会将自己注册到NameNode并汇报自己负责持有的块列表
  • 当某个DataNode关闭时,不会影响数据的可用性。 NameNode将安排由其他DataNode管理的块进行副本复制 。
  • DataNode所在机器通常配置有大量的硬盘空间,因为实际数据存储在DataNode中。

2.4 HDFS写数据流程

写数据完整流程

pipeline管道

  • Pipeline,中文翻译为管道。这是HDFS在上传文件写数据过程中采用的一种数据传输方式。
  • 客户端将数据块写入第一个数据节点,第一个数据节点保存数据之后再将块复制到第二个数据节点,后者保存后将 其复制到第三个数据节点。

  • 为什么DataNode之间采用pipeline线性传输,而不是一次给三个DataNode拓扑式传输呢?

    • 因为数据以管道的方式,顺序的沿着一个方向传输,这样能够充分利用每个机器的带宽,避免网络瓶颈和高延迟时 的连接,最小化推送所有数据的延时
    • 在线性推送模式下,每台机器所有的出口宽带都用于以最快的速度传输数据,而不是在多个接受者之间分配宽带。

ACK应答响应

  • ACK (Acknowledge character)即是确认字符,在数据通信中,接收方发给发送方的一种传输类控制字符。表示 发来的数据已确认接收无误。
  • 在HDFS pipeline管道传输数据的过程中,传输的反方向会进行ACK校验,确保数据传输安全。

默认3副本存储策略

  • 默认副本存储策略是由BlockPlacementPolicyDefault指定。

  • 第一块副本:优先客户端本地,否则随机
  • 第二块副本:不同于第一块副本的不同机架。
  • 第三块副本:第二块副本相同机架不同机器。

实际流程

  1. HDFS客户端创建对象实例DistributedFileSystem, 该对象中封装了与HDFS文件系统操作的相关方法。

  2. 调用DistributedFileSystem对象的create()方法,通过RPC请求NameNode创建文件。

    • NameNode执行各种检查判断:目标文件是否存在、父目录是否存在、客户端是否具有创建该文件的权限。检查通过 ,NameNode就会为本次请求记下一条记录,返回FSDataOutputStream输出流对象给客户端用于写数据。
  3. 客户端通过FSDataOutputStream输出流开始写入数据。

  4. 客户端写入数据时,将数据分成一个个数据包(packet 默认64k), 内部组件DataStreamer请求NameNode挑 选出适合存储数据副本的一组DataNode地址,默认是3副本存储

    • DataStreamer将数据包流式传输到pipeline的第一个DataNode,该DataNode存储数据包并将它发送到pipeline的第二个DataNode。同样,第二个DataNode存储数据包并且发送给第三个(也是最后一个)DataNode
  5. 传输的反方向上,会通过ACK机制校验数据包传输是否成功

  6. 客户端完成数据写入后,在FSDataOutputStream输出流上调用close()方法关闭

  7. DistributedFileSystem联系NameNode告知其文件写入完成,等待NameNode确认

    因为NameNode已经知道文件由哪些块组成(DataStream请求分配数据块),因此仅需等待最小复制块即可成功返回 。 最小复制是由参数dfs.namenode.replication.min指定,默认是1.

2.5 HDFS读数据流程

流程图

  1. HDFS客户端创建对象实例DistributedFileSystem, 调用该对象的open()方法来打开希望读取的文件

  2. DistributedFileSystem使用RPC调用namenode来确定文件中前几个块的块位置(分批次读取)信息。 对于每个块,namenode返回具有该块所有副本的datanode位置地址列表,并且该地址列表是排序好的,与客户端的网络拓扑距离近的排序靠前

  3. DistributedFileSystem将FSDataInputStream输入流返回到客户端以供其读取数据

  4. 客户端在FSDataInputStream输入流上调用read()方法。

    然后,已存储DataNode地址的InputStream连接到文件 中第一个块的最近的DataNode。数据从DataNode流回客户端,结果客户端可以在流上重复调用read()

  5. 当该块结束时,FSDataInputStream将关闭与DataNode的连接,然后寻找下一个block块的最佳datanode位置。 这些操作对用户来说是透明的。所以用户感觉起来它一直在读取一个连续的流。

    客户端从流中读取数据时,也会根据需要询问NameNode来检索下一批数据块的DataNode位置信息

  6. 一旦客户端完成读取,就对FSDataInputStream调用close()方法

2.6 HDFS的高可用和高扩展

High Available

Federation

高可用(High Available)

HDFS的HA,指的是在一个集群中存在多个NameNode,分别运行在独立的物理节点上。在任何时间点,只有一个NameNode是处于Active状态,其它的是处于Standby状态。 Active NameNode(简写为Active NN)负责所有的客户端的操作,而Standby NameNode(简写为Standby NN)用来同步Active NameNode的状态信息,以提供快速的故障恢复能力。

为了保证Active NN与Standby NN节点状态同步,即元数据保持一致。除了DataNode需要向这些NameNode发送block位置信息外,还构建了一组独立的守护进程”JournalNodes”(简写为JN),用来同步Edits信息。当Active NN执行任何有关命名空间的修改,它需要持久化到一半以上的JNs上。而Standby NN负责观察JNs的变化,读取从Active NN发送过来的Edits信息,并更新自己内部的命名空间。一旦Active NN遇到错误,Standby NN需要保证从JNs中读出了全部的Edits,然后切换成Active状态,如果有多个Standby NN,还会涉及到选主的操作,选择一个切换为Active 状态。

需要注意一点,为了保证Active NN与Standby NN节点状态同步,即元数据保持一致

这里的元数据包含两块,一个是静态的,一个是动态的

静态的是fsimage和edits,其实fsimage是由edits文件合并生成的,所以只需要保证edits文件内容的一致性。这个就是需要保证多个NameNode中edits文件内容的事务性同步。这块的工作是由JournalNodes集群进行同步的

动态数据是指block和DataNode节点的信息,这个如何保证呢? 当DataNode启动的时候,上报数据信息的时候需要向每个NameNode都上报一份。 这样就可以保证多个NameNode的元数据信息都一样了,当一个NameNode down掉以后,立刻从Standby NN中选择一个进行接管,没有影响,因为每个NameNode 的元数据时刻都是同步的。

注意:使用HA的时候,不能启动SecondaryNameNode,会出错。 之前是SecondaryNameNode负责合并edits到fsimage文件 那么现在这个工作被standby NN负责了。

NameNode 切换可以自动切换,也可以手工切换,如果想要实现自动切换,需要使用到zookeeper集群。

使用zookeeper集群自动切换的原理是这样的

当多个NameNode 启动的时候会向zookeeper中注册一个临时节点,当NameNode挂掉的时候,这个临时节点也就消失了,这属于zookeeper的特性,这个时候,zookeeper就会有一个watcher监视器监视到,就知道这个节点down掉了,然后会选择一个节点转为Active,把down掉的节点转为Standby

高扩展(Federation)

HDFS Federation可以解决单一命名空间存在的问题,使用多个NameNode,每个NameNode负责一个命令空间

这种设计可提供以下特性:

  1. HDFS集群扩展性:多个NameNode分管一部分目录,使得一个集群可以扩展到更多节点,不再因内存的限制制约文件存储数目。
  2. 性能更高效:多个NameNode管理不同的数据,且同时对外提供服务,将为用户提供更高的读写吞吐率。
  3. 良好的隔离性:用户可根据需要将不同业务数据交由不同NameNode管理,这样不同业务之间影响很小。

如果真用到了Federation,一般也会和前面我们讲的HA结合起来使用,来看这个图

3.1 分治思想

  • MapReduce的思想核心是“先分再合,分而治之”。
  • 所谓“分而治之”就是把一个复杂的问题,按照一定的“分解”方法分为等价的规模较小的若干部分,然后逐个解 决,分别找出各部分的结果,然后把各部分的结果组成整个问题的最终结果
  • Map表示第一阶段,负责“拆分”:即把复杂的任务分解为若干个“简单的子任务”来并行处理。可以进行拆分的 前提是这些小任务可以并行计算,彼此间几乎没有依赖关系
  • Reduce表示第二阶段,负责“合并”:即对map阶段的结果进行全局汇总。
  • 这两个阶段合起来正是MapReduce思想的体现。

3.2 设计构思

1. 如何对付大数据处理场景

  • 对相互间不具有计算依赖关系的大数据计算任务,实现并行最自然的办法就是采取MapReduce分而治之的策略。
  • 首先Map阶段进行拆分,把大数据拆分成若干份小数据,多个程序同时并行计算产生中间结果;然后是Reduce聚 合阶段,通过程序对并行的结果进行最终的汇总计算,得出最终的结果。
  • 不可拆分的计算任务或相互间有依赖关系的数据无法进行并行计算!

2. 构建抽象编程模型

  • MapReduce借鉴了函数式语言中的思想,用MapReduce两个函数提供了高层的并行编程抽象模型。

  • map: 对一组数据元素进行某种重复式的处理

  • reduce: 对Map的中间结果进行某种进一步的结果整理

  • MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现:

    map: (k1; v1) → (k2; v2)
    reduce: (k2; [v2]) → (k3; v3)

    通过以上两个编程接口,大家可以看出MapReduce处理的数据类型是键值对

3. 统一架构、隐藏底层细节

  • 如何提供统一的计算框架,如果没有统一封装底层细节,那么程序员则需要考虑诸如数据存储、划分、分发、结果 收集、错误恢复等诸多细节;为此,MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层 面的处理细节。
  • MapReduce最大的亮点在于通过抽象模型和计算框架把需要做什么(what need to do)与具体怎么做(how to do)分开了,为程序员提供一个抽象和高层的编程接口和框架。 (业务和底层技术分开)
  • 程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的业务程序代码。
  • 至于如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:从分布代码的执行,到大到数千小到单个节点集群的自动调度使用。

3.3 介绍

分布式计算概念

  • 分布式计算是一种计算方法,和集中式计算是相对的。
  • 随着计算技术的发展,有些应用需要非常巨大的计算能力才能完成,如果采用集中式计算,需要耗费相当长的时间 来完成。
  • 分布式计算将该应用分解成许多小的部分,分配给多台计算机进行处理。这样可以节约整体计算时间,大大提高计算效率。

MapReduce

  • Hadoop MapReduce是一个分布式计算框架,用于轻松编写分布式应用程序,这些应用程序以可靠,容错的方式 并行处理大型硬件集群(数千个节点)上的大量数据(多TB数据集)。
  • MapReduce是一种面向海量数据处理的一种指导思想,也是一种用于对大规模数据进行分布式计算的编程模型。

产生背景

  • MapReduce最早由Google于2004年在一篇名为《MapReduce:Simplified Data Processingon Large Clusters 》的论文中提出。
  • 论文中谷歌把分布式数据处理的过程拆分为Map和Reduce两个操作函数(受到函数式编程语言的启发),随后被 Apache Hadoop参考并作为开源版本提供支持,叫做Hadoop MapReduce。
  • 它的出现解决了人们在最初面临海量数据束手无策的问题,同时它还是易于使用和高度可扩展的,使得开发者无需 关系分布式系统底层的复杂性即可很容易的编写分布式数据处理程序,并在成千上万台普通的商用服务器中运行。

特点

  • 易于编程

    MapReduce框架提供了用于二次开发的接口;简单地实现一些接口,就可以完成一个分布式程序。任务计算交给计算 框架去处理,将分布式程序部署到hadoop集群上运行,集群节点可以扩展到成百上千个等。

  • 良好的扩展性

    当计算机资源不能得到满足的时候,可以通过增加机器来扩展它的计算能力。基于MapReduce的分布式计算得特点可 以随节点数目增长保持近似于线性的增长,这个特点是MapReduce处理海量数据的关键,通过将计算节点增至几百或 者几千可以很容易地处理数百TB甚至PB级别的离线数据。

  • 高容错性

    Hadoop集群是分布式搭建和部署得,任何单一机器节点宕机了,它可以把上面的计算任务转移到另一个节点上运行, 不影响整个作业任务得完成,过程完全是由Hadoop内部完成的。

  • 适合海量数据的离线处理

    可以处理GB、TB和PB级别得数据量

局限性

MapReduce虽然有很多的优势,也有相对得局限性,局限性不代表不能做,而是在有些场景下实现的效果比较差,并 不适合用MapReduce来处理,主要表现在以下结果方面:

  • 实时计算性能差

    MapReduce主要应用于离线作业,无法作到秒级或者是亚秒级得数据响应。

  • 不能进行流式计算

    流式计算特点是数据是源源不断得计算,并且数据是动态的;而MapReduce作为一个离线计算框架,主要是针对静态 数据集得,数据是不能动态变化得。

实例进程

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

  • MRAppMaster:负责整个MR程序的过程调度及状态协调
  • MapTask:负责map阶段的整个数据处理流程
  • ReduceTask:负责reduce阶段的整个数据处理流程

阶段组成

  • 一个MapReduce编程模型中只能包含一个Map阶段和一个Reduce阶段,或者只有Map阶段
  • 不能有诸如多个map阶段、多个reduce阶段的情景出现
  • 如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序串行运行

数据类型

  • 注意:整个MapReduce程序中,数据都是以kv键值对的形式流转
  • 在实际编程解决各种业务问题中,需要考虑每个阶段的输入输出kv分别是什么
  • MapReduce内置了很多默认属性,比如排序、分组等,都和数据的k有关,所以说kv的类型数据确定及其重要的

3.3 官方示例

  • 一个最终完整版本的MR程序需要用户编写的代码和Hadoop自己实现的代码整合在一起才可以
  • 其中用户负责map、reduce两个阶段的业务问题,Hadoop负责底层所有的技术问题
  • 由于MapReduce计算引擎天生的弊端(慢),所以在企业中工作很少涉及到MapReduce直接编程,但是某些软件的背后还依赖MapReduce引擎
  • 可以通过官方提供的示例来感受MapReduce及其内部执行流程,因为后续的新的计算引擎比如Spark,当中就有 MapReduce深深的影子存在。

示例说明

  • 示例程序路径:/XXX/hadoop-XXX/share/hadoop/mapreduce/
  • 示例程序:hadoop-mapreduce-examples-3.3.0.jar
  • MapReduce程序提交命令:[hadoop jar|yarn jar] hadoop-mapreduce-examples-XXX.jar args…
  • 提交到哪里去?提交到YARN集群上分布式执行。

1. 评估圆周率的值

蒙特卡洛方法

  • 运行MapReduce程序评估一下圆周率的值,执行中可以去YARN页面上观察程序的执行的情况。

  • 第一个参数:pi表示MapReduce程序执行圆周率计算任务

  • 第二个参数:用于指定map阶段运行的任务task次数,并发度,这里是10

  • 第三个参数:用于指定每个map任务取样的个数,这里是50。

    /opt/hadoop-3.3.0/share/hadoop/mapreduce# hadoop jar hadoop-mapreduce-examples-3.3.0.jar pi 10 50

2. WordCount单词词频统计

WordCount中文叫做单词统计、词频统计,指的是统计指定文件中,每个单词出现的总次数

实现思路

  • map阶段的核心:把输入的数据经过切割,全部标记1,因此输出就是<单词,1>

  • shuffle阶段核心:经过MR程序内部自带默认的排序分组等功能,把key相同的单词会作为一组数据构成新的kv对

  • reduce阶段核心:处理shuffle完的一组数据,该组数据就是该单词所有的键值对。对所有的1进行累加求和,就是 单词的总次数

程序提交

  • 自己随便写个文本文件1.txt到HDFS文件系统的/input目录下,如果没有这个目录,使用shell创建

    • hadoop fs -mkdir /input
    • hadoop fs -put 1.txt /input
  • 准备好之后,执行官方MapReduce实例,对上述文件进行单词次数统计

    • 第一个参数:wordcount表示执行单词统计任务
    • 第二个参数:指定输入文件的路径
    • 第三个参数:指定输出结果的路径(该路径不能已存在)

    hadoop jar hadoop-mapreduce-examples-3.3.0.jar wordcount /input /output

3.4 MapReduce执行流程

流程图

Map执行过程

  • 第一阶段:把输入目录下文件按照一定的标准逐个进行逻辑切片,形成切片规划。

    默认Split size = Block size(128M),每一个切片由一个MapTask处理。(getSplits)

  • 第二阶段:对切片中的数据按照一定的规则读取解析返回键值对

    默认是按行读取数据。key是每一行的起始位置偏移量,value是本行的文本内容。(TextInputFormat)

  • 第三阶段:调用Mapper类中的map方法处理数据

    每读取解析出来的一个<key, value>,调用一次map方法

  • 第四阶段:按照一定的规则对Map输出的键值对进行分区partition。默认不分区,因为只有一个reducetask。 分区的数量就是reducetask运行的数量。 (分区方法默认是hash求余法)

  • 第五阶段:Map输出数据写入内存缓冲区,达到比例溢出到磁盘上。溢出spill的时候根据key进行排序sort。 默认根据key字典序排序。

  • 第六阶段:对所有溢出文件进行最终的merge合并,成为一个文件。

Reduce执行过程

  • 第一阶段:ReduceTask会主动从MapTask复制拉取属于需要自己处理的数据。
  • 第二阶段:把拉取来数据,全部进行合并merge,即把分散的数据合并成一个大的数据。再对合并后的数据排序
  • 第三阶段:对排序后的键值对调用reduce方法键相等的键值对调用一次reduce方法。最后把这些输出的键值对写入到HDFS文件中。

shuffle

  • Shuffle的本意是洗牌、混洗的意思,把一组有规则的数据尽量打乱成无规则的数据。
  • 而在MapReduce中,Shuffle更像是洗牌的逆过程,指的是将map端的无规则输出按指定的规则“打乱”成具有一 定规则的数据,以便reduce端接收处理。
  • 一般把从Map产生输出开始到Reduce取得数据作为输入之前的过程称作shuffle。

Map端Shuffle

一个map最后只会产生一个文件

  • Collect阶段:将MapTask的结果收集输出到默认大小为100M的环形缓冲区,保存之前会对key进行分区的计算, 默认Hash分区(分区数量为reducetask的数量,对每个key的hash值对reducetask数求余映射到某个reducetask,key值相同的会映射到相同的reducetask)。
  • Spill阶段:当内存中的数据量达到一定的阀值(默认80%)的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了combiner,还会将有相同分区号和key的数据进行排序。 (示例图上显示有三个分区,即三个reducetask)
  • Merge阶段:把所有溢出的临时文件进行一次合并操作,以确保一个MapTask最终只产生一个中间数据文件。(三个分区会分别给三个reducetask,不同map中的相同分区会到同一个reducetask上合并)

Reducer端Shuffle

  • Copy阶段: ReduceTask启动Fetcher线程到已经完成MapTask的节点上复制一份属于自己的数据。
  • Merge阶段:在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作 。
  • Sort阶段:在对数据进行合并的同时,会进行排序操作,由于MapTask阶段已经对数据进行了局部的排序, ReduceTask只需保证Copy的数据的最终整体有效性即可。

Shuffle机制弊端

  • Shuffle是MapReduce程序的核心与精髓,是MapReduce的灵魂所在。
  • Shuffle也是MapReduce被诟病最多的地方所在。MapReduce相比较于Spark、Flink计算引擎慢的原因,跟 Shuffle机制有很大的关系。
  • Shuffle中频繁涉及到数据在内存、磁盘之间的多次往复

3.5 任务日志查看

需要开启YARN的日志聚合功能,把散落在NodeManager节点上的日志统一收集管理,方便查看日志

  • 启动:bin\mapred --daemion start historyserver
  • yarn logs -applicationId <ID>

3.6 中止任务

在命令行中ctrl+c无法停止程序,因为程序已经提交到Hadoop集群运行 了

  • yarn application -kill <ID>

4.1 介绍

  • Apache Hadoop YARN (Yet Another Resource Negotiator,另一种资源协调者)是一种新的Hadoop资源管 理器。
  • YARN是一个通用资源管理系统和调度平台,可为上层应用提供统一的资源管理和调度。
  • 它的引入为集群在利用率、资源统一管理和数据共享等方面带来了巨大好处。

功能

  • 资源管理系统:集群的硬件资源,和程序运行相关,比如内存、CPU等。
  • 调度平台:多个程序同时申请计算资源如何分配,调度的规则(算法)。
  • 通用:不仅仅支持MapReduce程序,理论上支持各种计算程序。YARN不关心你干什么,只关心你要资源,在有的情况下给你,用完之后还我。

概述

  • 可以把Hadoop YARN理解为相当于一个分布式的操作系统平台,而MapReduce等计算程序则相当于运行于操作 系统之上的应用程序,YARN为这些程序提供运算所需的资源(内存、CPU等)。
  • Hadoop能有今天这个地位,YARN可以说是功不可没。因为有了YARN ,更多计算框架可以接入到 HDFS中,而 不单单是 MapReduce,正是因为YARN的包容,使得其他计算框架能专注于计算性能的提升
  • HDFS可能不是最优秀的大数据存储系统,但却是应用最广泛的大数据存储系统, YARN功不可没。
  • yarn.nodemanager.resource.memory-mb:单节点可分配的物理内存总量,默认是8MB*1024,即8G
  • yarn.nodemanager.resource.cpu-vcores:单节点可分配的虚拟CPU个数默认是8

4.2 架构组件

架构图

client

container 容器(资源的抽象):容器之间逻辑上隔离的

YARN三大组件

  • ResourceManager(RM)

    • YARN集群中的主角色,决定系统中所有应用程序之间资源分配的最终权限,即最终仲裁者
    • 接收用户的作业提交,并通过NM分配、管理各个机器上的计算资源。
  • NodeManager(NM)

    • YARN中的从角色,一台机器上一个,负责管理本机器上的计算资源
    • 根据RM命令,启动Container容器、监视容器的资源使用情况。并且向RM主角色汇报资源使用情况
  • ApplicationMaster (App Mstr) (AM)

    • 用户提交的每个应用程序均包含一个AM。
    • 应用程序内的“老大”,负责程序内部各阶段的资源申请,监督程序的执行情况。
    • 管理程序的进行

4.3 程序提交交互流程

核心交互流程

  • MR作业提交 Client-->RM
  • 资源的申请 MrAppMaster-->RM
  • MR作业状态汇报 Container(Map|Reduce Task)-->Container(MrAppMaster)
  • 节点的状态汇报 NM-->RM

整体概述

当用户向 YARN 中提交一个应用程序后, YARN将分两个阶段运行该应用程序 。

  • 第一个阶段是客户端申请资源启动运行本次程序的ApplicationMaster
  • 第二个阶段是由ApplicationMaster根据本次程序内部具体情况,为它申请资源,并监控它的整个运行过程,直 到运行完成。

MR提交YARN交互流程

  • 第1步:用户通过客户端向YARN中ResourceManager提交应用程序(比如hadoop jar提交MR程序)
  • 第2步:ResourceManager为该应用程序分配第一个Container(容器),并与对应的NodeManager通信,要求它在这个Container中启动这个应用程序的ApplicationMaster
  • 第3步:ApplicationMaster启动成功之后,首先向ResourceManager注册并保持通信,这样用户可以直接通过 ResourceManager查看应用程序的运行状态(处理了百分之几)
  • 第4步:AM为本次程序内部的各个Task任务向RM申请资源,并监控它的运行状态
  • 第5步:一旦 ApplicationMaster 申请到资源后,便与对应的 NodeManager 通信,要求它启动任务。
  • 第6步:NodeManager 为任务设置好运行环境后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务
  • 第7步:各个任务通过某个 RPC 协议向 ApplicationMaster 汇报自己的状态和进度,以让 ApplicationMaster 随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。在应用程序运行过程中,用户可随时通过 RPC 向 ApplicationMaster 查询应用程序的当前运行状态
  • 第8步:应用程序运行完成后,ApplicationMaster 向 ResourceManager 注销并关闭自己

4.4 调度器Scheduler

  • 在理想情况下,应用程序提出的请求将立即得到YARN批准。但是实际中,资源是有限的,并且在繁忙的群集上, 应用程序通常将需要等待其某些请求得到满足。YARN调度程序的工作是根据一些定义的策略为应用程序分配资源

  • 在YARN中,负责给应用分配资源的就是Scheduler,它是ResourceManager的核心组件之一。

    Scheduler完全专用于调度作业,它无法跟踪应用程序的状态。

  • 一般而言,调度是一个难题,并且没有一个“最佳”策略,为此,YARN提供了多种调度器和可配置的策略供选择

调度器策略

  • FIFO Scheduler(先进先出调度器)
  • Capacity Scheduler(容量调度器)(Apache版本YARN默认使用Capacity Scheduler)
  • Fair Scheduler(公平调度器)

FIFO

  • FIFO Scheduler是Hadoop1.x中JobTracker原有的调度器实现,此调度器在YARN中保留了下来。
  • FIFO Scheduler是一个先进先出的思想,即先提交的应用先运行。调度工作不考虑优先级和范围,适用于负载较低的小规模集群。当使用大型共享集群时,它的效率较低且会导致一些问题。
  • FIFO Scheduler拥有一个控制全局的队列queue,默认queue名称为default,该调度器会获取当前集群上所有的 资源信息作用于这个全局的queue

优势

  • 无需配置、先到先得、易于执行

劣势

  • 任务的优先级不会变高,因此高优先级的作业需要等待
  • 不适合共享集群

Capacity

FIFO Schedule的多队列版本

  • Capacity Scheduler容量调度是Apache Hadoop3.x默认调度策略。该策略允许多个组织共享整个集群资源,每个 组织可以获得集群的一部分计算能力。通过为每个组织分配专门的队列,然后再为每个队列分配一定的集群资源, 这样整个集群就可以通过设置多个队列的方式给多个组织提供服务了。
  • Capacity可以理解成一个个的资源队列,这个资源队列是用户自己去分配的。队列内部又可以垂直划分,这样一个 组织内部的多个成员就可以共享这个队列资源了,在一个队列内部,资源的调度是采用的是先进先出(FIFO)策略

资源队列划分

  • Capacity Scheduler调度器以队列为单位划分资源。简单通俗点来说,就是一个个队列有独立的资源,队列的结构 和资源是可以进行配置的

优势

  • 层次化的队列设计(Hierarchical Queues)

    • 层次化的管理,可以更容易、更合理分配和限制资源的使用
  • 容量保证(Capacity Guarantees)

    • 每个队列上都可以设置一个资源的占比,保证每个队列都不会占用整个集群的资源
  • 安全(Security)

    • 每个队列有严格的访问控制。用户只能向自己的队列里面提交任务,而且不能修改或者访问其他队列的任务
  • 弹性分配(Elasticity)

    • 空闲的资源可以被分配给任何队列。 当多个队列出现争用的时候,则会按照权重比例进行平衡

Fair

  • Fair Scheduler叫做公平调度,提供了YARN应用程序公平地共享大型集群中资源的另一种方式。使所有应用在平均情况下随着时间的流逝可以获得相等的资源份额。
  • Fair Scheduler设计目标是为所有的应用分配公平的资源(对公平的定义通过参数来设置)。
  • 公平调度可以在多个队列间工作,允许资源共享和抢占

如何理解公平共享

  • 有两个用户A和B,每个用户都有自己的队列。
  • A启动一个作业,由于没有B的需求,它分配了集群所有可用的资源。
  • 然后B在A的作业仍在运行时启动了一个作业,经过一段时间,A,B各自作业都使用了一半的资源。
  • 现在,如果B用户在其他作业仍在运行时开始第二个作业,它将与B的另一个作业共享其资源,因此B的每个作业将拥有资源的四分之一,而A的继续将拥有一半的资源。结果是资源在用户之间公平地共享。

优势

  • 分层队列:队列可以按层次结构排列以划分资源,并可以配置权重以按特定比例共享集群。
  • 基于用户或组的队列映射:可以根据提交任务的用户名或组来分配队列。如果任务指定了一个队列,则在该队列中提交任务。
  • 资源抢占:根据应用的配置,抢占和分配资源可以是友好的或是强制的。默认不启用资源抢占。
  • 保证最小配额:可以设置队列最小资源,允许将保证的最小份额分配给队列,保证用户可以启动任务。当队列不能满足最小资源时,可以从其它队列抢占。当队列资源使用不完时,可以给其它队列使用。这对于确保某些用户、组或生产应用始终获得足够的资源。
  • 允许资源共享:即当一个应用运行时,如果其它队列没有任务执行,则可以使用其它队列,当其它队列有应用需要资源 时再将占用的队列释放出来。所有的应用都从资源队列中分配资源。
  • 默认不限制每个队列和用户可以同时运行应用的数量。可以配置来限制队列和用户并行执行的应用数量。限制并行 执行应用数量不会导致任务提交失败,超出的应用会在队列中等待。

4.5 多资源队列使用

修改hadoop文件中 etc/hadoop/capacity-scheduler.xml

下面增加了两个队列online和offline,将以下内容添加进去,而不是覆盖。

  <property>
    <name>yarn.scheduler.capacity.root.queues</name>
    <value>default,online,offline</value>
    <description>队列列表,多个队列之间使用逗号分割</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.default.capacity</name>
    <value>70</value>
    <description>default队列70%</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.online.capacity</name>
    <value>10</value>
    <description>online队列10%</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.offline.capacity</name>
    <value>20</value>
    <description>offline队列20%</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.default.maximum-capacity</name>
    <value>70</value>
    <description>Default队列可使用的资源上限.</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.online.maximum-capacity</name>
    <value>10</value>
    <description>online队列可使用的资源上限.</description>
  </property>
  <property>
    <name>yarn.scheduler.capacity.root.offline.maximum-capacity</name>
    <value>20</value>
    <description>offline队列可使用的资源上限.</description>
  </property>

为了提高磁盘IO性能,Hadoop弃用了java中的序列化,自己编写了writable实现类

注意:

  • Text等价于java.lang.String的writable,针对utf-8序列
  • NullWritable是单例,获取实例使用NullWritable.get()

Hadoop序列化机制的特点

  • 紧凑:高效使用存储空间
  • 快速:读写数据的额外开销小
  • 可扩展:可透明地读取老格式的数据
  • 互操作:支持多语言的交互

Java序列化的不足

  • 不精简,附加信息多,不太适合随机访问
  • 存储空间大,递归地输出类的超类描述知道不再有超类

源码

  1. getSplits: 对文件进行分区

  2. createRecordReader : 将InputSplit中的数据解析成Record,即<k1, v1>

    public abstract class InputFormat {

    /**

    • Logically split the set of input files for the job.
      *

    • Each {@link InputSplit} is then assigned to an individual {@link Mapper}

    • for processing.


      *

    • Note: The split is a logical split of the inputs and the

    • input files are not physically split into chunks. For e.g. a split could

    • be <input-file-path, start, offset> tuple. The InputFormat

    • also creates the {@link RecordReader} to read the {@link InputSplit}.
      *

    • @param context job configuration.

    • @return an array of {@link InputSplit}s for the job.
      */
      public abstract
      List getSplits(JobContext context
      ) throws IOException, InterruptedException;

      /**

    • Create a record reader for a given split. The framework will call

    • {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before

    • the split is used.

    • @param split the split to be read

    • @param context the information about the task

    • @return a new record reader

    • @throws IOException

    • @throws InterruptedException
      */
      public abstract
      RecordReader createRecordReader(InputSplit split,
      TaskAttemptContext context
      ) throws IOException,
      InterruptedException;

    }

仅对FileInputFormat源码进行分析

6.1 InputSplit

源码注解(Hadoop3.3.0)

  /**
   * Generate the list of files and make them into FileSplits.
   * @param job the job context
   * @throws IOException
   */
  public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();

    /*
       getFormatMinSplitSize() = 1
       getMinSplitSize(job) = 0
       minSize = 1
     */
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));

    /*
        没有默认值
        getMaxSplitSize(job) = Long.MAX_VALUE
        所以maxSize等于Long的最大值
     */
    long maxSize = getMaxSplitSize(job);

    // generate splits
    // 创建List,总部内保存生成的InputSplit
    List<InputSplit> splits = new ArrayList<InputSplit>();
    // 获取输入文件列表
    List<FileStatus> files = listStatus(job);

    // ignoreDirs = false
    boolean ignoreDirs = !getInputDirRecursive(job)
      && job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);

    // 迭代输入文件列表
    for (FileStatus file: files) {
      // 是否忽略子目录,默认不忽略
      if (ignoreDirs && file.isDirectory()) {
        continue;
      }
      // 获取 文件/目录 路径
      Path path = file.getPath();
      // 获取 文件/目录 长度
      long length = file.getLen();
      if (length != 0) {
        // 保存文件的Block块所在的位置
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          FileSystem fs = path.getFileSystem(job.getConfiguration());
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        // 判断文件是否支持切割,默认为true
        if (isSplitable(job, path)) {

          // 获取文件的Block大小,默认128M
          long blockSize = file.getBlockSize();

          // 计算split的大小
          /*
            内部使用的公式是: Math.max(minSize, Math.min(maxSize, blockSize))
                           Math.max(1, Math.min(Long.MAX_VALUE, 128))
            默认情况下split的大小和Block size相等
           */
          long splitSize = computeSplitSize(blockSize, minSize, maxSize);

          // 还需要处理的文件剩余字节大小,其实就是这个文件的原始大小
          long bytesRemaining = length;

          /*
            SPLIT_SLOP = 1.1
            只要剩余文件大于1.1倍的分区size就继续切割
           */
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
            // 获取block的索引
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            /*
              组装InputSplit
              path: 路径
              length-bytesRemaining 起始位置
              splitSize 大小
              blkLocations[blkIndex].getHosts() 和 blkLocations[blkIndex].getCachedHosts() 所在的主机

             */
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                        blkLocations[blkIndex].getHosts(),
                        blkLocations[blkIndex].getCachedHosts()));
            bytesRemaining -= splitSize;
          }
          if (bytesRemaining != 0) {
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
                       blkLocations[blkIndex].getHosts(),
                       blkLocations[blkIndex].getCachedHosts()));
          }
        } else { // not splitable
          // 不支持切割
          if (LOG.isDebugEnabled()) {
            // Log only if the file is big enough to be splitted
            if (length > Math.min(file.getBlockSize(), minSize)) {
              LOG.debug("File is not splittable so no parallelization "
                  + "is possible: " + file.getPath());
            }
          }
          // 整个作为一个InputSplit
          splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
                      blkLocations[0].getCachedHosts()));
        }
      } else {
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    // Save the number of input files for metrics/loadgen
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    return splits;
}

6.2 RecordReader

  • 每一个InputSplit都有一个RecordReader,作用是把InputSplit中的数据解析成Record,即<k1, v1>

行阅读器的初始化方法源码

  // 初始化方法
  public void initialize(InputSplit genericSplit,
                         TaskAttemptContext context) throws IOException {
    // 获取传过来的InputSplit,将InputSplit转换成子类FileSplit
    FileSplit split = (FileSplit) genericSplit;
    Configuration job = context.getConfiguration();
    // MAX_LINE_LENGTH对应的参数默认没有配置,所以会取Integer.MAX_VALUE
    this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
    // 获取InputSplit的起始位置
    start = split.getStart();
    // 获取InputSplit的结束位置
    end = start + split.getLength();
    // 获取InputSplit的路径
    final Path file = split.getPath();

    // open the file and seek to the start of the split
    // 打开文件,并跳到InputSplit的起始位置
    final FutureDataInputStreamBuilder builder =
        file.getFileSystem(job).openFile(file);
    FutureIOSupport.propagateOptions(builder, job,
        MRJobConfig.INPUT_FILE_OPTION_PREFIX,
        MRJobConfig.INPUT_FILE_MANDATORY_PREFIX);
    fileIn = FutureIOSupport.awaitFuture(builder.build());

    // 获取文件的压缩信息
    CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
    // 如果文件是压缩文件,则执行if中的语句
    if (null!=codec) {
      //... 省略代码
    } else {
      // 跳转到文件的起始位置
      fileIn.seek(start);
      // 针对未压缩文件,创建一个阅读器读取一行行的数据
      in = new UncompressedSplitLineReader(
          fileIn, job, this.recordDelimiterBytes, split.getLength());
      filePosition = fileIn;
    }
    // If this is not the first split, we always throw away first record
    // because we always (except the last split) read one extra line in
    // next() method.
    /*
      注意:如果这个InputSplit不是第一个InputSplit,我们将会丢弃读取出来的第一行
      因为我们总是通过next方法多读取一行
      因此,如果一行数据被拆分到了两个InputSplit中,不会产生问题。
     */
    // 如果start不等于0,表示不是第一个inputsplit,所以把start的值重置为第二行的起始位置
    if (start != 0) {
      start += in.readLine(new Text(), 0, maxBytesToConsume(start));
    }
    this.pos = start;
  }

7.1 小文件问题

  • Hadoop的HDFS和MR框架是针对大数据文件来设计的,在小文件的处理上不但效率低下,而且十分消耗内存资源
  • HDFS提供了两种类型的容器,SequenceFile和MapFile

SequenceFile

  • 二进制文件,直接将<key, value>对序列化到文件中
  • 一般对小文件可以使用这种文件合并,即将文件名作为key,文件内容作为value序列化到大文件中
  • 注意:SequenceFile需要一个合并的过程,文件较大,且合并后的文件将不方便查看,必须通过遍历查看每一个小文件

代码实现

import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;

import java.io.File;

/*
    small files
 */
public class SmallFileSeq {

    public static void main(String[] args) throws Exception {
        write("/root/smallfiles", "/seqFile");
        read("/seqFile");
    }

    /**
     * 生成SequenceFile文件
     * @param inputDir 本地文件
     * @param outputFile hdfs文件
     * @throws Exception
     */
    private static void write(String inputDir,String outputFile) throws Exception {
        // 创建一个配置对象
        Configuration conf = new Configuration();
        // 指定hdfs路径
        conf.set("fs.defaultFS", "hdfs://node1:8020");

        // 删除输出文件
        FileSystem fileSystem = FileSystem.get(conf);
        fileSystem.delete(new Path(outputFile), true);

        // 三个元素:输出路径、key的类型、value的类型
        SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{
                SequenceFile.Writer.file(new Path(outputFile)),
                SequenceFile.Writer.keyClass(Text.class),
                SequenceFile.Writer.valueClass(Text.class)
        };

        // 创建一个writer实例
        SequenceFile.Writer writer = SequenceFile.createWriter(conf, opts);

        // 指定需要压缩的文件的目录
        File inputDirPath = new File(inputDir);
        if (inputDirPath.isDirectory()) {
            // 获取目录中的文件
            File[] files = inputDirPath.listFiles();

            assert files != null;
            for (File file : files) {
                // 获取文件的全部内存
                String content = FileUtils.readFileToString(file, "UTF-8");
                // 获取文件名
                String fileName = file.getName();
                Text key = new Text(fileName);
                Text value = new Text(content);
                // 写入数据
                writer.append(key, value);
            }
        }
        writer.close();
    }

    /**
     * 读取SequenceFile文件
     * @param inputFile
     * @throws Exception
     */
    private static void read(String inputFile) throws Exception {
        // 创建一个配置对象
        Configuration conf = new Configuration();
        // 指定hdfs路径
        conf.set("fs.defaultFS", "hdfs://node1:8020");
        // 创建阅读器
        SequenceFile.Reader reader = new SequenceFile.Reader(conf, SequenceFile.Reader.file(new Path(inputFile)));

        Text key = new Text();
        Text value = new Text();
        while (reader.next(key, value)) {
            System.out.println("文件名:" + key.toString() + ",");
            System.out.println("文件内容:\n" + value.toString() + ".");
        }
        reader.close();
    }
}

MapFile

  • MapFile是排序后的SequenceFile,MapFile由两部分组成,分别是index和data
  • index作为文件的数据索引,主要记录了每个Record的key值,以及该Record在文件中的偏移位置
  • 在MapFile被访问的时候,索引文件会被加载到内存,通过索引映射关系可迅速定位到指定Record所在文件位置

SequenceFile文件是用来存储key-value数据的,但它并不保证这些存储的key-value是有序的,而MapFile文件则可以看做是存储有序key-value的SequenceFile文件。MapFile文件保证key-value的有序(基于key)是通过每一次写入key-value时的检查机制,这种检查机制其实很简单,就是保证当前正要写入的key-value与上一个刚写入的key-value符合设定的顺序,但是,这种有序是由用户来保证的,一旦写入的key-value不符合key的非递减顺序,则会直接报错而不是自动的去对输入的key-value排序

代码实例

import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.MapFile;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;

import java.io.File;

/*
    small files
 */
public class SmallFilemap {

    public static void main(String[] args) throws Exception {
        write("/root/smallfiles", "/mapFile");
        read("/mapFile");
    }

    /**
     * 生成MapFile文件
     * @param inputDir 本地目录
     * @param outputDir hdfs目录
     * @throws Exception
     */
    private static void write(String inputDir,String outputDir) throws Exception {
        // 创建一个配置对象
        Configuration conf = new Configuration();
        // 指定hdfs路径
        conf.set("fs.defaultFS", "hdfs://node1:8020");

        // 删除输出文件
        FileSystem fileSystem = FileSystem.get(conf);
        fileSystem.delete(new Path(outputDir), true);

        // 两个元素:key的类型、value的类型
        SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{
                MapFile.Writer.keyClass(Text.class),
                MapFile.Writer.valueClass(Text.class)
        };

        // 创建一个writer实例
        MapFile.Writer writer = new MapFile.Writer(conf, new Path(outputDir), opts);

        // 指定需要压缩的文件的目录
        File inputDirPath = new File(inputDir);
        if (inputDirPath.isDirectory()) {
            // 获取目录中的文件
            File[] files = inputDirPath.listFiles();

            for (File file : files) {
                // 获取文件的全部内存
                String content = FileUtils.readFileToString(file, "UTF-8");
                // 获取文件名
                String fileName = file.getName();
                Text key = new Text(fileName);
                Text value = new Text(content);
                // 写入数据
                writer.append(key, value);
            }
        }
        writer.close();
    }

    /**
     * 读取MapFile文件
     * @param inputDir MapFile文件路径
     * @throws Exception
     */
    private static void read(String inputDir)throws Exception{
        // 创建一个配置对象
        Configuration conf = new Configuration();
        // 指定hdfs路径
        conf.set("fs.defaultFS", "hdfs://node1:8020");
        //创建阅读器
        MapFile.Reader reader = new MapFile.Reader(new Path(inputDir),conf);
        Text key = new Text();
        Text value = new Text();
        //循环读取数据
        while(reader.next(key,value)){
            //输出文件名称
            System.out.print("文件名:"+key.toString()+",");
            //输出文件内容
            System.out.println("文件内容:"+value.toString()+"");
        }
        reader.close();
    }
}

7.2 数据倾斜问题

  • MapReduce程序执行时,Reduce节点大部分执行完毕,但是有一个或者几个Reduce节点运行很慢,导致整个程序处理时间变得很长,具体表现为:Reduce阶段卡着不动

    • 示例:利用hash分区方法,如果某个key值特别多,那么会导致这个key值对应的Reducetask运行量很大,而其他的task则很快执行完毕。

解决方法

  1. 增加reduce个数(但不一定有用)
  2. 将倾斜数据打散

https://www.bilibili.com/video/BV1CU4y1N7Sh?p=1

http://wiki.xuwei.tech/