在前文《Hadoop部署》中,我们已经提到过HDFS(Hadoop Distributed File System),它是Hadoop核心的一部分,是Hadoop默认使用的一套分布式文件系统。这里之所以说默认,是因为Hadoop项目其实有一层比较通用的文件系统抽象层,这使得它可以使用多种文件系统,比如本地文件系统、Amazon S3等。当然本文主要介绍HDFS。

设计目标

优势

我们知道Hadoop是为了处理大数据而诞生的一个系统,而HDFS是为了存储大数据而生的一个分布式文件系统,所以他在设计上就考虑了很多大数据处理存储的一些特点,下面我们介绍HDFS在设计上就做的一些假设前提和目标:

  1. 硬件错误。硬件错误是常态而不是异常。HDFS可能由成百上千的服务器所构成,每个服务器上存储着文件系统的部分数据。我们面对的现实是构成系统的组件数目是巨大的,而且任一组件都有可能失效,这意味着总是有一部分HDFS的组件是不工作的。因此错误检测和快速、自动恢复是HDFS最核心的架构目标。
  2. 流式数据访问。运行在HDFS上的应用和普通的应用不同,需要流式访问它们的数据集。HDFS的设计中更多的考虑到了数据批处理,而不是用户交互处理。比之数据访问的低延迟问题,更关键的在于数据访问的高吞吐量。POSIX标准设置的很多硬性约束对HDFS应用系统不是必需的。为了提高数据的吞吐量,在一些关键方面对POSIX的语义做了一些修改。
  3. 大规模数据集。运行在HDFS上的应用具有很大的数据集。HDFS上的一个典型文件大小一般都在G字节至T字节。因此,HDFS被调节以支持大文件存储。它应该能提供整体上高的数据传输带宽,能在一个集群里扩展到数百个节点。一个单一的HDFS实例应该能支撑数以千万计的文件。
  4. 简单的一致性模型。HDFS应用需要一个“一次写入多次读取”的文件访问模型。一个文件经过创建、写入和关闭之后就不需要改变。这一假设简化了数据一致性问题,并且使高吞吐量的数据访问成为可能。Map/Reduce应用或者网络爬虫应用都非常适合这个模型。目前还有计划在将来扩充这个模型,使之支持文件的附加写操作。
  5. “移动计算比移动数据更划算”。一个应用请求的计算,离它操作的数据越近就越高效,在数据达到海量级别的时候更是如此。因为这样就能降低网络阻塞的影响,提高系统数据的吞吐量。将计算移动到数据附近,比之将数据移动到应用所在显然更好。HDFS为应用提供了将它们自己移动到数据附近的接口。

:上面5个特点的描述主要引用自Hadoop 1.0.4官方文档,但是Hadoop至今3.x的beta版本都已经出了,所以文档中有些地方已经过时了,上面已经修正了。

我们再简单总结一下:HDFS设计用来存储海量大文件,但是它对硬件并不挑剔,普通的服务器就可以,而且是支持异构的;HDFS所专注的场景主要是流式的,即写一次,但读很频繁的模式;HDFS专注的是提高整体系统的吞吐量,而不是访问数据的实时性。

劣势

下面我们再介绍一下HDFS目前不太适合(以后可能会改善)的几个场景:

  1. 低延迟访问的场景。如前所述,HDFS主要是针对提高系统吞吐量做了很多设计优化,而不是数据访问的实时性。目前来看,如果对于访问延迟有要求的场景,可以选用HBASE等数据库。
  2. 大量小文件存储。HDFS是用来存储海量大文件的,不太适合存储大量小文件。原因是HDFS的Namenode(后面马上会介绍,专门存储HDFS上面的元数据信息,管理HDFS)是将HDFS上面的元数据存储在内存中的,从而限制HDFS存储量的就是机器内存的大小。根据经验值,一个文件、目录或者块(block)的元数据大概会占用150个字节的内存,也就是说,如果现在有100万个文件,每个文件占一个块,那就需要大概300MB的内存。大量小文件的存储目前来看还是用一些对象存储系统比较好。
  3. 多个写入者以及随机修改。现在的HDFS不支持同时有多个写入者(虽然没有强制限制,但如果有多个写入者,写的结果是未知的),而且现在仅支持追加写(append-only)模式,不支持从任意地方进行数据修改。这个场景后续可能会支持,但就算支持了,相对来说,也不会非常高效。

架构解析

先来张官网的架构图:

架构图

HDFS是传统的Master-Slave架构:一个集群由一个Master节点和若干个Slave节点组成。在HDFS中,Master节点称为Namenode,Slave节点称为Datanode。下面我们详细说明。

Block

首先说一个概念:块(block)。在普通的磁盘文件系统中也有block的概念,它是我们读写数据的最小单元,对用户来说一般是透明的。同样,在HDFS中也有block的概念,但它和传统的文件系统的block有以下两个显著的区别:

  • 块大小。普通的文件系统的块大小一般都是KB级别的,比如Linux一般是4KB;而HDFS的block现在默认大小128MB,而且实际中往往可能会设置的更大一些,设置比较大的主要原因是为了缩短寻址(seek)的时间。如果block足够大,那我们从磁盘上传输数据所耗费的时间将远远大于寻找起始block的时间。这样我们传输一个由多个block组成的数据所花费的时间基本就是由传输速率决定。有一个经验上的快速计算公式,如果寻址时间是10ms,数据传输速率是100MB/s,那如果想让寻址时间是传输时间的1%,block的大小就需要是100MB。
  • 在普通磁盘文件系统中,当一个文件不足一个block大小的时候,它也会独占这个block,其他文件不可以再使用该block。但在HDFS中,一个比block小的文件不会独占一个block,比如一个1MB的文件只会占1MB的空间,而不是128MB,其他文件还可以使用该block的其他空间。

Filesystem Namespace

HDFS的文件系统命名空间与传统文件系统结构类似,由目录和文件组成,支持创建、删除、移动、重命名文件和目录等操作。HDFS支持用户配额(users quotas)和权限控制,权限控制与Linux的权限控制类似。用户配额主要包含两个维度:

  • Name Quotas:限制用户根目录所能包含的文件和目录总数。
  • Space Quotas:限制用户根目录的最大容量(字节)。

目前HDFS还不支持软连接和硬链接。

Namenode和Datanode

Namenode保存着HDFS中所有文件和目录的元数据信息,这些元数据信息以namespace imageedit log的形式存在Namenode所在节点的本地硬盘上面。Namenode还记录着一个文件的所有block在哪些Datanode上面,以及具体的位置,这些信息保存着内存里面,不落盘。因为这些信息每个Datanode会周期性的发给Namenode。Datanode是真正存储数据的节点,它会周期性的将自己上面所存储的block列表发给Namenode。当我们要获取一个文件时,从Namenode处查到这个文件的所有block在哪些Datanode上面,然后去这些Datanode上面查出具体的block。

从上面的描述可以看出,如果Namenode节点挂掉,所有的元数据将丢失,导致整个HDFS不可用。针对这个问题,Hadoop提供了两种机制来避免该问题:

  1. 备份所有的元数据信息。我们可以配置HDFS同时向多个文件系统写这些元数据信息,这个写是同步的、原子的。常用的配置策略是本地写一份,远程的NFS文件系统写一份。
  2. 运行一个Secondary NameNode。之前我们安装Hadoop的时候,已经看到有一个进程叫SecondaryNameNode了。需要注意的是这个SecondaryNameNode并不是Namenode的一个热备(standby),它的作用其实是周期性的将namespace imageedit log合并,产生新的image,防止edit log变的非常大。同时会保留一份合并后的image文件。这样当Namenode挂掉后,我们可以从这个保留的image文件进行恢复。但SecondaryNameNode的操作较Namenode还是有一定延迟的,所以这种方式还是会丢一些数据。

当然,上面的两种机制都只能保证Namenode出现故障时数据不丢或者丢失的少,但无法保证服务继续可用。Hadoop 2目前也提供了热备的方式来实现HA,当一个Namenode故障后,另外一个热备的Namenode马上会接替故障的Namenode对外提供服务。当然要实现这种热备需要做一些配置:

  • 两个Namenode使用高可用的共享存储来共享edit log
  • Datanode需要周期性的将自己上面的block信息同时发送给两个Namenode,因为这个信息是存在Namenode的内存里面的。
  • 客户端需要能够在一个Namenode故障后,自动切换到热备Namenode上。

如果配了热备后,就不需要SecondaryNameNode了,因为standby的Namenode会包含SecondaryNameNode的功能,去做合并。

Data Replication

为了保证一些Datanode挂掉的情况下数据不丢失,HDFS和许多分布式文件系统一样做了数据冗余:默认情况下,一份数据会有三份(即Replication Factor为3,可通过dfs.replication配置项更改)。也就是说,我们在HDFS上面存储一份数据的时候,实际存储了Replication Factor份。那这三份数据的存放位置如何选择呢?目前Hadoop的选择机制如下:

  • 第一份数据存储在发起写操作的客户端所在的Datanode上面。如果该客户端不在集群中Datanode节点上,则随机选择一个负载不是很重的Datanode。
  • 如果第一份数据存储在机架A上面的某一个Datanode节点上,那第二份数据就随机找一个非A机架上的一个Datanode节点存储。
  • 第三份数据找一个与第二份数据同机架,但不同节点的Datanode存储。
  • 如果Replication Factor大于3的话,其他的副本在集群内随机找一些节点存储,同时会尽量避免很多个副本存储于同一机架。

Read&Write

这里借Hadoop The Definitive Guide上的读写图。

先看读的过程:

hdfs read

HDFS的读操作相对比较简单,客户端先去Namenode获取block的信息,然后去Datanode获取block。这里有两个注意点:

  1. 一个block有多个副本,具体获取的时候有优先从离客户端比较近的Datanode获取数据。
  2. 从Namenode拿到block信息后,具体获取block只需要和Datanode交互,这样降低了Namenode的负担,不会使之成为读的瓶颈,并且把所有的度分到了各个Datanode上面,达到了负载均衡。

写的过程:这里假设我们创建一个新文件,然后写数据,最后关闭。

hdfs write

写的过程涉及两个队列和一个pipeline:

  • data queue:写数据的时候,其实就是将数据拆成多个packet写到该队列里面。
  • Datanode pipeline:实质就是一个由多个Datanode组成的列表,Datanode的个数由Replication Factor决定。
  • ack queue:该队列里面存放的也是数据packet。

完整的过程如下:

  1. DistributedFileSystem先向Namenode发一个创建的请求,如果Namenode会做一些检查(如同名文件是否存在、是否有创建权限等)。如果检查通过,就会创建该文件的元数据,此时没有任何block与之关联。接下来就是写具体数据。
  2. FSDataOutputStream把数据拆成多个packet写到data queue里面。然后DataStreamer向Namenode申请新的block来存储这些数据,Namenode会返回一个Datanode pipeline,然后DataStreamer把数据发给pipeline里面的第一个Datanode,该Datanode存储数据后,再把数据转发给第二个Datanode,这样一直到pipeline里面的最后一个Datanode。
  3. FSDataOutputStream同时维护着一个ack queue,里面是各Datanode已经存储了去写,但还未确认的packet。等收到pipeline里面所有Datanode数据写成功的ack后,就删掉ack queue队列里面的对应的packet。如果某个Datanode失败的话,就会被Namenode从pipeline中删除,并重新选择一个Datanode加到pipeline里面去,然后重新写数据到新加的Datanode上面。失败的Datanode上面写的部分不完整数据会在该Datanode恢复后删除。

这里需要注意的是并不是等所有副本都写成功后才向客户端返回,而是只要有dfs.namenode.replication.min(默认值是1)个副本写成功,就向客户端返回成功。其他的副本会异步的去写。

References: