在前一篇文章中,我们简略宏观的介绍了Hadoop的整体技术架构,并介绍了Hadoop的三个主要组件:负责存储的hdfs、负责计算的mapreduce、负责调度的yarn。另外,从存储的角度划分,Hadoop物理集群又分为两种:存储数据的datanode和维护元数据的namenode,当然还有secondarynamenode协助namenode一起维护元数据信息。本篇文章,我们将详细介绍Hadoop的核心存储组件——Hdfs。因为前一篇文章我们已经从宏观角度和话题引入的方式介绍了Hadoop的整体架构,因此,本篇文章将以具体的技术点方式来介绍详细内容。建议在阅读本文之前,先阅读前一篇文章。

1. 背景

我们知道,Hadoop的诞生最早来源于Google的三篇论文。最初,Google内部开发了Nutch用于构建一个全网的大型搜索引擎,包括网页抓取、查询、索引等功能,但随着抓取网页数量的增加,遇到了严重的可扩展性问题,即如何解决海量网页的存储和索引问题。后来,Google发表了三篇论文,为该问题提供了可行的解决方案。其中一篇介绍了分布式文件系统(GFS),可用于解决海量数据的存储,这就是Hdfs的雏形。

2. 分片存储

为了解决海量数据的存储问题,Hdfs引入了文件分片存储策略,这也是Hdfs的核心思想。Hdfs将每个大文件进行切片处理,每个block作为一份独立文件存储(在Hadoop2中block大小默认是128M,Hadoop1中默认是64M,可以通过设置 dfs.blocksize 参数来设置block的大小),这样就可以存储在不同的服务器上,从而解决了大文件的存储问题。

分片存储虽然解决了存储问题,但是没有解决安全问题,如果某台服务器挂了或者磁盘损坏,那么这台服务器存储的数据就无法提供服务了,甚至会造成数据丢失问题。因此,为了解决数据安全问题,Hadoop又引入了数据备份机制。既然一份文件有丢失风险,那么我就存储多份,例如,每个block在服务器A、B、C上都存一份,这样即使A服务器宕机了,B和C也可以正常提供服务,即使挂掉两台机器,依然可以正常运行。在Hadoop中,可以通过 dfs.replication 参数设置文件副本数量,一般默认 dfs.replication = 3 即每个block保存3份。Hadoop会自动维持副本数量,如果发现因某台机器宕机,导致某些文件的副本数只有两份,则Hadoop会自动复制一份新的拷贝到其他机器上,如果机器修复后重新加入集群,则Hadoop也会删除多余的副本。显然,一个合理的Hadoop集群datanode的数量不能小于 replication 的大小,不然replication的设置就没有意义。

3. 机架感知策略

前面我们介绍了Hadoop的多文件副本机制,每个block都存储多份,那么每一份都存在哪里呢?是不是随便找几台机器就可以了呢?假如我们的数据中心分布在3个地方:贵州、深圳和北京,那么最安全的方法当然是3份文件分别存在3个地方的服务器是最好的,但是这势必会带来高额的通信成本。如果都存在深圳,那风险性就比较高,不满足数据中心建设的灾备要求。因此,Hadoop为了兼顾效率和安全,采用机架感知策略,保证数据块的存放有一个最高效的策略,通常是先选择一个节点,然后选择同一个机架上的另外一个节点,最后再从其他机架上再选择一个节点。例如,在两份存在深圳的两台datanode上,另一份存在贵州的一台datanode上。因为,Hadoop已经采用了多备份机制,因此Hadoop的datanode服务器可以不使用RAID进行磁盘备份。

4. Namenode

前面我们介绍了Hdfs的分片存储策略,一个大文件,切分成不同的block块存储在多个datanode上面。那么问题来了,当我们需要访问数据的时候,我们怎么知道这个文件的不同block存储在哪里呢?显然我们不可能让客户端自己去找不同的block。当某个datanode节点宕机的时候,Hadoop集群又怎么能很快的知道呢?看起来,我们似乎还缺少一个集群的大管家。因此,Hadoop又设计了Namenode来扮演数据存储的管家角色。Namenode会保存整个集群的所有block元数据信息,主要包括block所属的文件和所在的位置信息(文件路径、副本数、block id等)。另外,datanode在存活时还要定时向namenode发送自身的block信息,fsimage文件中不包含block所在的datanode信息,block所在的datanode信息,namenode只保存在内存中。namenode为客户端提供一个统一的抽象目录树(可以借鉴Linux系统的目录树概念),客户端可以像windows、linux系统一样通过路径来直接访问文件,用户也可以通过namenode的50070端口在浏览器上浏览文件目录结构。因为要维护数据的元数据信息,所以Hadoop是不适合存储小文件的,一个block元信息消耗大约150 byte内存,存储大量小文件会浪费namenode的资源,对于小文件的存储,应尽量先合并再保存到hdfs,而且hdfs对于小文件的访问效率也不高。此外,针对小文件的存储,Hadoop还提供了一种归档机制,归档机制不改变原文件的存储大小,而是压缩小文件的元数据信息,从而节约namenode的资源,但是,在执行mapreduce时,会把多个小文件交给同一个mapreduce去split,这会降低mapreduce的效率。

5. 心跳机制

在第4节中,我们介绍了namenode的元数据管理功能,解决了我们在第4节提到的第一个问题,但是还有另外一个问题没有解决:当某个datanode节点宕机的时候,Hadoop集群又怎么能很快的知道呢?当有新的节点加入到集群时,可以通知集群,我们有新的节点上线了,但是如果节点宕机了,那就来不及通知集群了。因此,Hadoop为了解决这个问题,又引入了心跳机制。Hadoop要求datanode在active时,需要定时向namenode发送心跳,报告自己还活着。一般datanode默认 dfs.heartbeat.interval = 3 秒向namenode发送一次心跳,如果namenode没有收到datanode的心跳,我们就认为这个节点挂了。

现在,我们似乎解决了datanode的在线管理问题。但是,我们还会经常遇到网络拥堵的情况,可能datanode发送了心跳,但是因为网络堵塞导致namenode没有收到,可能过了七八秒后,namenode收到了多个心跳,这就导致我们不能根据一次或者两次的心跳包丢失就认为datanode失联了(聪明的你,是不是有了一丝丝的熟悉的感觉,是不是想到了计算机网络里面的滑动窗口协议和数据包校验机制)。因此,Hadoop又为namenode引入了检查机制,默认情况下,如果namenode在10次心跳时间后依然没有收到某个datanode的心跳,那么namenode开始启动检查机制,即每过 heartbeat.recheck.interval 时间向datanode发送一次问询请求(可以认为是反向心跳),如果两次以后,依然没有收到datanode的响应,则认为该datanode丢失,开始修改该节点的元数据信息并进行负载均衡。heartbeat.recheck.interval 的默认时间一般是5分钟,因此,datanode的超时时间为:

timeout = 2 * heartbeat.recheck.interval + 10 * dfs.heartbeat.interval = 630 s

心跳机制只是管理datanode的一种方式,也可以通过ZooKeeper对集群进行管理。例如,datanode在线的时候,不断地修改在ZooKeeper上的注册文件,一旦经过某个时间段后 ZooKeeper 发现该注册文件一直没有被修改,则认为该节点失联,后面我们介绍Hadoop的HA机制时,会再详细介绍ZooKeeper的使用。

6. 负载均衡

前面我们已经多次提到了负载均衡,注意区别这里的负载均衡并不是web应用的高并发请求负载均衡。在第1节中,我们提到了文件是分片存储的,一个大文件会被切分为不同的block文件块,Hadoop集群会有多个datanode节点,怎么选择存储的节点的呢?在机架感知策略小节我们也介绍了会按照兼顾高效和安全的原则选择存储节点,这是不是代表我们找到三台合适的节点后,所有的文件都先存储到这三台,直到这三台磁盘耗尽以后,再选择新的机器存储呢?显然这种方式是不合理的,上一篇文章我们已经介绍了这会造成数据存储的严重倾斜,而且在分布式计算的时候,也不能很好地利用分布式资源,造成高额的数据通信成本。最好的方法是,尽量让数据均匀的分布在不同的节点上,即负载均衡策略。在后面介绍Yarn组件的时候,我们会提到,Yarn在每个datanode上还维护了一个nodemanager RPC服务,用于监控datanode的资源情况,并报告给Yarn的中央政府——Resourcemanager,Hadoop就可以根据各个节点的资源使用情况,进行负载均衡。如,上传新的文件时,尽量上传到剩余资源较多的datanode上。当有节点宕机、某些节点上的文件被大量删除或者修改了 replication  大小时,Hadoop都会视情况进行负载均衡处理,使得各个datanode存储的文件尽可能平衡。因为负载均衡需要进行节点间的文件传输,还需要修改namenode上的元数据信息,又不是紧急操作,因此多在集群相对空闲的时候进行。一般Hadoop也提供了负载均衡的操作脚本start-balancer.sh,可以手动进行负载均衡,该脚本提供了一个参数,当集群节点磁盘使用率最高值和最低值之差大于该阈值(百分比)时,启动负载均衡。

7. SecondaryNamenode

在第4节我们介绍了namenode记录元数据信息的功能,如果元数据信息保存在namenode的内存中,访问速度快,但是不安全,如果保存在namenode的磁盘中,安全但是又会拉低访问速度。Hadoop为了解决这个问题,namenode在内存和磁盘中都维持了一份元数据信息,并且引入了 SecondaryNamenode 辅助namenode工作。namenode会保存所有元数据信息在fsImage文件中,每当有数据变化时,内存中的元数据会立即修改,并追加到edits.log文件(只进行追加操作的小文件,效率很高),此时并不会立刻同步到fsImage中,因为对fsImage文件的操作效率较低。然后,SecondaryNamenode会定时去合并namenode上的edits.log文件到fsImage中,这个过程也称之为checkpoint,具体执行过程如下:

  1. secondary namenode请求namenode停止使用edits文件,暂时将新的元数据更新信息写到一个新文件中(edits_inprogress_...)。
  2. secondary namenode通过http请求从namenode获取fsimage和edits文件。
  3. secondary namenode将fsimage文件载入到内存,逐一执行edits文件中的操作,创建新的fsimage文件。
  4. secondary namenode将新的fsimage文件发送回namenode。
  5. namenode节点将从secondary namenode节点接收的fsimage文件替换旧的fsimage文件。

※ 问题:在面试的时候可能会遇到一个问题,如果namenode因故障宕机,该如何挽救集群和数据呢?

根据checkpoint机制,这时候如果namenode上的文件还在,就可以拿最新的镜像文件fsimage和最新的预写操作日志文件edits合并得到完整的元数据信息;如果namenode因为磁盘损坏导致镜像文件都没有了,这时只能把SecondaryNamenode上的fsimage与edits文件拷贝给namenode使用,但是这些文件不包括之前namenode上最新的预写操作日志文件,因为SecondaryNamenode只是定时的去namenode上拉取镜像数据文件,因为延时原因,会有一部分信息丢失。此外,namenode的元数据信息保存文件可以配置多个目录,甚至是其他集群的hdfs上,为了提高安全性,也可以多配置几个目录,不过这些方法都不是官方推荐的做法,因为单节点namenode即使可以恢复数据,在修复期间也是不能提供服务的,这在生产系统上是决不允许的,后面我们介绍HA功能的时候,会介绍更好的方法。

和checkpoint相关的配置参数主要有:

dfs.namenode.checkpoint.check.period=60   ## 每60s进行一次checkpoint
dfs.namenode.checkpoint.max-retries=3     ## 最大重试次数
dfs.namenode.checkpoint.period=3600       ## 两次checkpoint之间的时间间隔3600秒
dfs.namenode.checkpoint.txns=1000000      ## 两次checkpoint之间最大的操作记录数

由SecondaryNamenode的工作机制可知,SecondaryNamenode并不能单纯认为是namenode的备份。现在,我们对namenode的主要功能做一个小结:

  1. 保存并维护Hdfs上数据的元数据信息,维护文件目录树,如block所属文件、block大小、block位置等,当有文件增删或者负载均衡操作时,及时修改元数据信息。
  2. 监听datanode的心跳,当有新的节点加入时,能够进行在线扩容(即响应),并将其加入监听队列,当有节点失联时,能够及时感知,并进行善后处理(修改元数据信息,并进行负载均衡)。
  3. 响应客户端的请求,例如当客户端需要读写文件时,获取元数据信息。HDFS的内部工作机制对客户端保持透明,客户端请求访问HDFS都是通过向namenode申请来进行。
  4. 响应SecondaryNamenod的请求,协同维护元数据信息。

8. shell命令

HDFS也提供了非常便利的客户端shell操作,且都是类linux命令,熟悉linux命令的情况下极易上手,不管是原生的Hadoop,还是CDH等二次包装后的框架,命令都是统一的。HDFS的命令如下:

hadoop守护进程内存消耗过大 hadoop日常维护内容_hadoop

其中最常用的就是 hdfs dfs 命令,这里面包括了所有的常用文件操作,可以通过help选项自行查看:

hadoop守护进程内存消耗过大 hadoop日常维护内容_hadoop_02

9. 客户端身份伪造

在shell命令或者API操作中,可能会遇到权限问题。例如某个目录是hdfs用户创建的,当你用其他用户去上传文件时,可能就会出现权限拒绝问题,此时我们就需要设置 HADOOP_USER_NAME 环境变量。如果是shell操作,直接设置喜下面的环境变量即可: 

export HADOOP_USER_NAME=hdfs

如果是API操作,如Java,也可以在程序中设置相应的环境变量:

System.setProperty("HADOOP_USER_NAME", "hdfs");

python访问HDFS的操作,可以参考这篇文章, 文中提供了三种不同的访问HDFS HA的方法。

10. safemode

Hadoop除了正常工作模式以外,还有两种不常见的模式:safemode(安全模式)和 standby。safemode表示集群出现了问题,不能对外提供正常服务,一般进入safemode 有三种情况:

  1. 集群刚刚启动时,此时集群初始化还没有完成,无法对外正常提供服务。namenode此时还没有收到完整的block dn信息,它会认为有文件丢失,直到所有的datanode汇报完block信息后才会退出安全模式。
  2. 当数据损坏达到 0.1% (这个值是可以设置的)时会自动进入安全模式,表示HDFS中有超过 0.1% 的block没有保证正常的最小副本数(dfs.replication.min,可设置)。
  3. 手动进入安全模式,当然也可以手动退出。
  4. 当集群磁盘不足(爆掉)时有时也会进入安全模式。

这里主要说一下第2中情况,在安全模式下,集群无法对外提供写服务,包括上传数据、删除数据、修改数据等。一般情况下,HDFS会自动修复退出safemode,但也存在不能修复的特殊情况,此时可能是某些block的副本都损坏了,就需要手动退出 safemode:

hdfs dfsadmin -safemode leave

查看损坏的文件:

hdfs fsck -list-corruptfileblocks

可以手动删除损坏的文件,然后从其他地方重新上传该文件:

hdfs fsck / -delete

当然,最安全的处理方法还是要先分析datanode文件损坏的原因,如果是服务器的原因(例如磁盘损坏),应先修复服务器查看集群是否能自行退出安全模式。

Hadoop关于安全模式的主要操作命令如下:

  • hdfs dfsadmin -safemode get                  ## 查看安全模式状态
  • hdfs dfsadmin -safemode enter               ## 进入安全模式
  • hdfs dfsadmin -safemode leave              ## 退出安全模式
  • hdfs dfsadmin -safemode wait                ## 等待安全模式结束

wait 命令常用于程序中需要上传文件到 HDFS,但是HDFS处于safemode的情况,此时我们可以使用 wait 操作等待HDFS退出safemode,类似于sleep操作。

standby 状态和 HA 结构相关,在后面介绍HA的时候会再说明。

11. HDFS上传数据流程

本节我们详细介绍HDFS上传数据的详细流程和内部工作机制。

当客户端需要上传数据时,首先要跟 NameNode 通信以确认可以写文件并获得接收文件 block 的 DataNode。然后,客户端按顺序将文件逐个 block 传递给相应 DataNode,并由接收到 block 的 DataNode 负责向其他 DataNode 复制 block 的副本,如下图。

hadoop守护进程内存消耗过大 hadoop日常维护内容_客户端_03

以上传一个300M大小的 test.csv 文件为例,在副本数为3的情况下具体流程如下:

  1. 客户端向namenode发送请求,请求上传test.csv文件,附带上传目录以及用户信息。
  2. namenode根据请求检查目录树,判断目录是否存在,当前用户是否有写权限。
  3. 如果上传信息无误,namenode向客户端返回确认信息:可以上传。
  4. 客户端请求上传第一个block文件。
  5. namenode检查datanode信息池,为待上传的文件分配三个合适的datanode列表(机架感知策略):dn1、dn2、dn3
  6. 向客户端返回分配的datanode信息。
  7. 客户端向第一个datanode dn1(一般是网络拓扑最近的)发送传输数据请求,并请求递归建立 pipeline。关于pipeline的解释在下文。
  8. 从最后一个datanode反向发送建立pipeline确认信息。
  9. 客户端通过第7步建立好的pipeline开始传输数据,每个datanode收到数据包(packet)后都要反向向前一个节点返回确认信息,客户端也会维护一个数据队列,每收到一个数据包的确认信息,就从队列中删除相应的数据包。
  10. 客户端完成数据的传输后,会对数据流调用 close() 方法,关闭数据流。

客户端传输文件的方式有多种,可以先上传到dn1,上传完毕后再上传到dn2和dn3(dn2、dn3文件的上传可以由客户端完成,也可以由dn1同步过去),这种方式好处是稳定,但是效率慢,也可以由客户端同时向dn1、dn2、dn3上传文件(并发),这种方式的好处是效率高,但是稳定性低一些,且对客户端的要求高。HDFS没有采用上述两种方法,而是采用了流水式的pipeline方法传输,这里的pipeline和linux下的管道流以及机器学习建模中的串联处理步骤概念类似,客户端向dn1发送建立pipeline请求,并将dn2、dn3的信息发送给dn1,并要求dn1向后递归建立pipeline,dn1收到请求后,会向dn2发送建立pipeline的请求,同样会把dn3的信息传输给dn2,dn2收到请求后,向dn3发送建立pipeline的请求。dn3和dn2建立pipeline后,会给dn2返回确认信息,dn2收到确认信息后,同样会和dn1建立pipeline,并返回确认信息,同样dn1也会和客户端之间进行同样的操作。pipeline 建立完毕后,客户端的文件流把数据传输给 dn1 ,dn1收到数据包后会同步传输给dn2,同样dn2也会同步传输给dn3,这样即分担了客户端的压力,又提高了传输效率。

如果传输过程中,有某个 DataNode 出现了故障,那么当前的 pipeline 会被关闭,出现故障的 DataNode 会从当前的 pipeline 中移除,剩余的 block 会在剩下的 DataNode 中继续以 pipeline 的形式传输,同时 NameNode 会分配一个新的 DataNode,保持 replicas 设定的数量。只要写入了 dfs.replication.min (最小写入成功的副本数)的复本数(默认为1),写操作就会成功,并且这个块可以在集群中异步复制,直到达到其目标复本数( dfs.replication ),因为 NameNode 已经知道文件由哪些块组成,所以它在返回成功前只需要等待数据块进行最小量的复制。

12. HDFS下载数据流程

相比于上传数据,HDFS下载数据的过程就比较简单了。例如客户端执行 hdfs dfs -get /data/test.csv 下载数据到当前目录,具体流程如下:

  1. 客户端向namenode发送下载(读取)数据请求。
  2. namenode收到请求后,会查询元数据信息,判断要下载的文件是否存在,该用户是否有读权限。
  3. 如果满足条件,则返回文件的元数据信息,主要包括文件的副本数和文件的block信息。
  4. 客户端收到元数据信息后,首先获取block1的元数据信息,向block1的所有副本存储节点中网络拓扑最近的节点发送数据读取请求,并通过socketstream下载block1数据到客户端。
  5. 按照4的方法,依次下载所有的block数据到客户端,合并(依次追加)后的文件就是完整文件。客户端在下载数据时为了保证数据安全,也会做文件校验(如CRC校验),如果读取 DataNode 时出现错误,客户端会通知NameNode,然后再从下一个拥有该 block 副本的 DataNode 继续读。

至此,上面我们已经介绍了HDFS的主要功能和工作机制,Hadoop适合存储大量数据,可构建在廉价机器上,通过多副本提高可靠性,副本丢失后,可以自动恢复,提供了容错和恢复机制,但是不适合存储需要频繁修改的文件,也不适合存储小文件。