卷1:第8章 HDFS——Hadoop分布式文件系统

Hadoop分布式文件系统(HDFS)的设计主旨,在于对超大规模数据集提供可靠的存储功能,并对用户应用程序提供高带宽的输入输出数据流。在大型的集群里,上千台服务器均可直接参与到数据存储和应用程序任务执行。通过多服务器,分布式的存储和计算,计算资源的规模能够按照需要增长,并兼顾在各种规模上经济适用性。 本文主要描述了HDFS的架构,并以Yahoo!企业数据服务为例,介绍了如何使用HDFS系统管理高达4O PB规模的数据库的经验。
8.1 介绍

Hadoop采用MapReduce范式[DG04]进行设计,提供了一套分布式文件系统和框架,用以对超大规模的数据集进行分析和变换。HDFS的接口沿袭了Unix文件系统的设计模式,但在其基础上做出了改进,以提高在实际应用中的访问性能。

Hapoop所具有一个重要特点,就是把数据和运算分开,并将二者分布存放在数以千计的服务器主机上,应用程序计算以及相关数据都以并行方式处理。一个Hadoop集群可以仅仅通过增加普通服务器的方式,来扩展其运算、存储和I/O带宽的规模。Yahoo所使用的Hadoop集群组,共包含40,000台服务器,存储并处理多达40 PB(1 Petabytes = 1000000000000000 字节)的应用数据,其中最大的单个集群,使用多达4000个服务器。此外,在世界范围内,还有100多家其他的组织和机构表示,他们也使用Hadoop来进行数据存储和处理。

HDFS将文件系统元数据(File System Metadata)和应用数据(Application Data)分离存放。与其他种类的分布式文件系统类似,例如PVFS[CIRT00], Lustre2, 以及GFS[GGL03],HDFS将元数据存放在专用服务器上,该服务器称为“NameNode”(名称节点);应用数据被存放在其他的服务器上,这些服务器称为“DataNode”(数据节点)。在该分布式系统中,各个服务器之间均通过网络连接,确保节点之间可以通过基于TCP族的协议进行相互通信。HDFS不像Lustre或者PVFS,它并不依赖于数据保护机制(例如RAID)来确保数据的稳定性,而是像GFS那样,在多个DataNode节点上保存数据的多个副本,以此来确保数据的稳定。采用这样的策略,其好处不仅仅在于数据安全方面,在数据传输带宽方面,由于一个数据有多个副本,因此可以通过多线程访问倍速提高带宽(就像迅雷下载的原理一样——译者注),并且采用此种方式还可以提高从较近的服务器节点上获取数据的几率。
8.2 架构
8.2.1 NameNode(名称节点)
HDFS命名空间采用层次化(树状——译者注)的结构存放文件和目录。文件和目录用NameNode上的inodes表示。Inode记录了权限,修改和访问时间,命名空间,磁盘容量等属性。文件内容会被分成不同的“大块”(典型分块策略是每块128M,不过用户可以对每个文件的分块大小进行选择)。NameNode负责维护命名空间树以及与DataNode上文件分块的映射关系。目前采用的设计结构是,没一个集群只有一个NameNode,一个NameNode可以对应多个DataNode以及成千上万的HDFS客户端。一个DataNode可以同步执行多个应用任务。
8.2.2 映像和日志
Inode和定义metadata的系统文件块列表统称为Image(映像). NameNode将整个命名空间映像保存在RAM中。而映像的持久化记录则保存在NameNode的本地文件系统中,该持久化记录被称为Checkpoint(检查点)。NameNode还会记录HDFS中写入的操作,并将其存入一个记录文件,存放在本地文件系统中,这个记录文件被叫做Journal(日志)。存放块位置的副本不属于持久化检查点(persistent checkpoint)的一个部分。 每个客户端发起的事务都会被记录到日志里,然后日志文件会被刷新和同步,再发送回客户端。NameNode上的检查点文件(Checkpoint file)一旦生成,就不允许再修改。如果NameNode重启,在系统管理员的要求下,或者根据CheckpointNode的定义(下章介绍),可以生成一个新的文件记录checkpoint。 在启动过程中,NameNode会从checkpoint中初始化命名空间映像,然后根据日志重现所有的写入更改操作。在NameNode开始响应客户端之前,一个新的checkpoint和一个空的日志将被保存到存储目录当中。
为了提高持久性,系统会将checkpoint文件和日志的多个冗余备份存储到多个独立的本地卷以及远程NFS服务器上。之所以存储到独立卷标,是为了避免单个卷标失效后造成文件丢失;存储到远程服务器则是为了预防整个节点崩溃后造成所有本地文件丢失。如果NameNode遇到了错误,无法将日志信息写入到某个存储目录,那么系统就会自动将该有问题的目录排除到存储目录列表的范围之外。如果NameNode发现连一个可用的存储目录都找不到,则会执行自动关闭操作(节点失效)。
NameNode是一个多线程的系统应用,可以同时处理多个客户端的申请。不过,将事务存储到磁盘是一个较大的性能瓶颈,因为如果有一个线程正在存储中,其他线程都必须等待该线程完成其刷新和同步过程完成后,才能继续进行操作。为了优化这一过程,NameNode采用将多个事务批处理的方式,当某个NameNode线程初始化了一个刷新同步操作时,所有的事务会被一次性批处理,然后一起提交。其他的线程只需要检查他们的事务是否被存储了即可,而不需要再去提交刷新同步操作。
8.2.3 数据节点
DataNode上的每一个块(block)副本都由两个本地文件系统上的文件共同表示。其中一个文件包含了块(block)本身所需包含的数据,另一个文件则记录了该块的元数据,包括块所含数据大小和文件生成时间戳。数据文件的大小等于该块(block)的真实大小,而不是像传统的文件系统一样,需要用额外的存储空间凑成完整的块。因此,如果一个块里只需要一半的空间存储数据,那么就只需要在本地系统上分配半块的存储空间即可。
在启动过程中,每个DataNode通过“握手”的方式与另外一个NameNode节点连接。之所以采用“握手”方式,是为了验证DataNode的命名空间ID以及软件的版本号。如果一个节点的ID或者版本号不匹配,那么DataNode节点就会自动关闭。
命名空间ID是在文件系统实例格式化的时候就分配好的。命名空间ID被在集群内的所有节点上都有持久化存储。由于不同命名空间ID的节点无法加入到集群中,因此能够保证集群文件系统的统一性。一个DataNode在刚初始化的时候没有命名空间ID,此时该节点被允许加入集群,一旦加入,该节点就会以加入的集群的ID作为自己的ID。
“握手”之后,DataNode被注册到NameNode。DataNode持久化保存其唯一的存储ID(storage ID)。存储ID是一个DataNode的内部标识符,该标识符能够确保即使是服务器用不同的IP地址或者端口启动,仍然可以被识别。存储ID在DataNode首次注册到NameNode时即被分配,一旦分配后便无法更改。
DataNode采用发送“块报告”(block report)的形式,向NameNode标识其所包含的块副本。块报告包含了块ID,生成时间戳,以及每个块副本的长度等等。 首个块报告会在DataNode注册后立即发送。随后的块报告会每小时发送一次,以确保NameNode能够知道集群中块副本的最新情况。
在正常情况下,DataNode想NameNode发送“心跳信号”,以确认DataNode运行正常,以及其所包含的块数据可用。默认的“心跳信号”的时间间隔是3秒。如果NameNode长达10分钟没有接受到来自于DataNode的心跳信号,那么久会认为为该DataNode节点已经失效,其所包含的块(block)已经无法使用。接下来NameNode就会计划在其他的DataNode上创建新的块数据。
来自DataNode的心跳信号也会附带包括总存储容量,存储使用量,当前数据传输进度等附加信息。这些统计数据可用于NameNode块分配,以及作为负载均衡决策的参考。
NameNode不能直接向DataNode发送请求。它只通过回复心跳信号的方式来向DataNode发送指令。指令的内容包括,将块移到其他节点,移除本地块副本,重新注册和发送即时块报告,关闭当前节点等等。
这些命令对于维护整个系统的完整性来说非常关键,因此即使是在超大集群上,保持心跳信号的频率也是至关重要的。NameNode每秒能够处理上千条心跳信号,并且不影响到NameNode的其他正常操作。
8.2.4 HDFS客户端
用户应用程序通过HDFS客户端连接到HDFS文件系统,通过库文件可导出HDFS文件系统的接口。
像很多传统的文件系统一样,HDFS支持文件的读、写和删除操作,还支持对目录的创建和删除操作。用户通过带命名空间的路径对文件和目录进行引用。用户程序不需要知道文件系统的元数据和具体存储在哪个服务器上,也不需要关心一个块有多少个副本。
当一个应用程序读一个文件的时候,HDFS客户端首先向NameNode索要包含该文件的文件块的DataNode节点的列表。该列表会按照网络拓扑距离的远近进行排序。然后客户端会直接与相应的DataNode节点进行联系,要求传输所需的文件块。当客户端写一个文件的时候,它会首先要求NameNode选择一个DataNode,该DataNode需要包含所写入的文件的首个文件块。接下来,客户端会搭建一个从节点到节点的通信管道,用以进行数据传输。当第一个块被写入后,客户端会申新的DataNode,用以写入下一个块。此时,新的通信管线建立,客户端会通过管线写入更多的数据。每个文件块所写入的DataNode节点也许会完全不同。客户端,NameNode和DataNode之间的关系如图8.1所示。
图8.1 HDFS客户端创建新文件

与传统的文件系统不同的是,HDFS提供一个API用以暴露文件块的位置。这个功能允许应用程序,例如MapReduce框架,去数据所存放的地点进行任务调度,以此来提高读数据的新能。API也允许一个应用程序设定文件的复制因子。默认情况下,文件的复制因子是3,。对于关键的文件或者使用频率较多的文件,使用更高的复制因子,能够提高容错性,以及提升文件的访问带宽。
8.2.5 检查点节点
HDFS中的NameNode节点,除了其主要职责是相应客户端请求以外,还能够有选择地扮演一到两个其他的角色,例如做检查点节点或者备份节点。该角色是在节点启动的时候特有的。
检查点节点定期地域已经存在的检查点和日志一起,创建新的检查点和空日志。检查点节点通常运行于一个非NameNode节点的主机上,但它和NameNode节点拥有相同的内存需求配置。检查点节点从NameNode上下载当前的检查点和日志文件,将其在本地进行合并,并将新的检查点返回到NameNode.
创建一个定期检查点是保护文件系统元数据的一种方式。如果命名空间映像中的所有其他持久化拷贝均无法使用,系统还能够从最近一次的checkpoint文件中启动。当创建一个新的checkpoint被更新到NameNode的时候,还能让NameNode产生截断日志的效果。HDFS集群组可以长时间持续运行,无需重启,但这也导致了系统日志的大小会不断增长。当系统日志大到一定程度的时候,日志文件丢失或者损坏的几率就会增加。所以,一个日志太大的节点需要重启一下来对日志文件进行更新(截断)。对于一个较大的集群来说,平均处理一周的日志内容需要耗费一小时的时间。所以较好的频率是,每天创建一次新日志。

8.2.6 备份节点
HDFS的备份节点是最近在加入系统的一项特色功能。就像CheckpintNode一样,备份节点能够定期创建检查点,但是不同的是,备份节点一直保存在内存中,随着文件系统命名空间的映像更新和不断更新,并与NameNode的状态随时保持同步。
备份节点从活动的NameNode节点中接受命名空间事务的日志流,并将它们以日志的形式存储在其自身所带的存储目录里,并使用自身的内存和命名空间映像来执行这个事务。NameNodez则将BackupNode当做日志一样看待,就仿佛是存储在其自身的存储目录里一样。如果NameNode失效,那么BackupNode节点内存中的映像和磁盘上的checkpoint文件就可以作为最近的命名空间状态的备份,以备还原。
BackupNode能够动态创建一个checkpoint,而不需要从活动的NameNode上下载其checkpoint文件和日志文件。因为BackupNode始终把最新的状态保存在它自身命名空间的内存中。这一特性使得在BackupNode节点上处理checkpoint变得非常高效,因为只需要把命名空间存储到本地服务器就可以了,而不需要和NameNode再进行交互。
BackupNode还可以被看做是一个只读的NameNode. 它包含了除文件块位置以外的所有文件系统的元数据信息。除了修改命名空间或者文件块位置以外,BackupNode可以做NameNode所能做的所有操作。使用BackupNode能够使NameNode在运行的时候不进行持久化存储,从而把持久化命名空间状态的任务挪到BackupNode节点上进行。
8.2.7 系统更新和文件系统快照
在软件更新的过程中,由于软件的bug或者人为操作的失误,文件系统损坏的几率会随之提升。在HDFS中创建系统快照的目的,就在于把系统升级过程中可能对数据造成的隐患降到最低。
快照机制让系统管理员将当前系统状态持久化到文件系统中,这样以来,如果系统升级后出现了数据丢失或者损坏,便有机会进行回滚操作,将HDFS的命名空间和存储状态恢复到系统快照进行的时刻。
系统快照(只能有一个)在系统启动后,根据集群管理员的选择可随时生成。如果要求生成一个系统快照,NameNode首先会读取checkpoint和日志文件,并将其在内存中合并。然后,NameNode在新的位置创建并写入一个新的checkpoint和空的日志,保证旧的Checkpoint和日志不会被改变。
在进行“握手”方式通信时,NameNode指示DataNode是否要创建一个本地的系统快照。DataNode上的本地系统快照不会复制本地的存储目录中包含的数据信息,因为如果这么做的话,会使得每个集群上的DataNode上的数据占据双倍的存储空间。因此,取而代之的策略是,建立一份目录结构的副本,并用“链接”的方式将已经存在的块文件指向到目录副本。当DataNode尝试移除一个文件块时,只需要移除链接就可以了,在文件块增量时,也是使用copy-on-wirte技术。所以旧的块副本在其原先的目录结构中保持不变。
集群管理员可以选择在重启系统时,将HDFS回滚到快照所表示的状态。NameNode在快照恢复时,会恢复所记录的checkpoint。DataNode则会恢复之前被更名的文件目录,并初始化一个幕后进程,用以删除在snapshot创建之后系统新增的文件块副本。选择回滚后,回滚之前的操作将无法再继续(无法前滚)。集群管理员可以下达放弃快照的指令,来恢复被快照功能所占用的存储权。如果再系统升级期间进行快照,那么升级过程将被终止。
随着系统的升级,NameNode的checkpoint文件和日志文件的格式可能会发生变化,或者块文件的数据结构也可能发生改变。因此,系统中使用“设计版本号”(layout version)来标识数据表现格式,该版本号被持久化地存储在NameNode以及DataNode的存储目录中。每个节点在启动时,会将其系统设计版本号和存储目录中的设计版本号进行对比,并自动尝试将旧的数据格式转换到新的版本。为了实现系统版本升级,新版本重启时会强制创建系统快照。
8.3 文件 I/O 操作和复制管理
当然,一个文件系统的根本任务是用来存储数据和文件。如果要理解HDFS如何完成这一基本任务,我们就必须从它如何进行数据的读写,以及文件块如何管理这两方面来说明。
8.3.1 文件读写
应用程序通过创建新文件以及向新文件写数据的方式,给HDFS系统添加数据。文件关闭以后,被写入的数据就无法再修改或者删除,只有以“追加”方式重新打开文件后,才能再次为文件添加数据。HDFS采用单线程写,多线程读的模式。
HDFS客户端需要首先获得对文件操作的授权,然后才能对文件进行写操作。在此期间,其他的客户端都不能对该文件进行写操作。被授权的客户端通过向NameNode发送心跳信号来定期更新授权的状态。当文件关闭时,授权会被回收。文件授权期限分为软限制期和硬限制期两个等级。当处于软限制期内时,写文件的客户端独占对文件的访问权。当软限制过期后,如果客户端无法关闭文件,或没有释放对文件的授权,其他客户端即可以预定获取授权。当硬限制期过期后(一小时左右),如果此时客户端还没有更新(释放)授权,HDFS会认为原客户端已经退出,并自动终止文件的写行为,收回文件控制授权。文件的写控制授权并不会阻止其他客户端对文件进行读操作。因此一个文件可以有多个并行的客户端对其进行读取。
HDFS文件由多个文件块组成。当需要创建一个新文件块时,NameNode会生成唯一的块ID,分配块空间,以及决定将块和块的备份副本存储到哪些DataNode节点上。DataNode节点会形成一个管道,管道中DataNode节点的顺序能够确保从客户端到上一DataNode节点的总体网络距离最小。文件的则以有序包(sequence of packets)的形式被推送到管道。应用程序客户端创建第一个缓冲区,并向其中写入字节。第一个缓冲区被填满后(一般是64 KB大小),数据会被推送到管道。后续的包随时可以推送,并不需要等前一个包发送成功并发回通知(这被称为“未答复发送”——译者注)。不过,这种未答复发送包的数目会根据客户端所限定的“未答复包窗口”(outstanding packets windows)的大小进行限制。
在数据写入HDFS文件后,只要文件写操作没有关闭,HDFS就不保证数据在此期间对新增的客户端读操作可见。如果客户端用户程序需要确保对写入数据的可见性,可以显示地执行hflush操作。这样,当前的包就会被立即推送到管道,并且hflush操作会一直等到所有管道中的DataNode返回成功接收到数据的通知后才会停止。如此就可以保证所有在执行hflush之前所写入的数据对试图读取的客户端用户均可见。
图8.2 数据块写入管线
如果没有错误发生,文件块会经历如图8.2所示的3个步骤,该图演示了一个三DataNode节点的管道以及其通信的过程。在图中,实线代表数据包,虚线代表确认信息,细线代表控制信息,用以创建和关闭管道。竖直线代表客户端和三个DataNode节点的活动,其时间顺序是从上至下的。从t0到t1是管道的建立阶段,t1是第一个数据包发送的时刻,t2是最后一个数据包接受成功并返回通知的时刻。在传送数据包2的时候有一次hflush操作。Hflush说明数据包的传输并不是一个单独分离的过程。T2和t3之间是该文件块在管道中的关闭阶段。
在一个集群的数千个节点里,节点失效(往往是因为存储故障造成的)每天都有可能发生。DataNode中所包含的文件块备份可能会因为内存、磁盘或者网络的错误而造成损坏。为了避免这种错误的形成,HDFS会为其文件的每个数据块生成并存储一份Checksum(总和检查)。Checksum主要供HDFS客户端在读取文件时检查客户端,DataNode以及网络等几个方面可能造成的数据块损坏。当客户端开始建立HDFS文件时,会检查文件的每个数据块的checksum序列,并将其与数据一起发送给DataNode。 DataNode则将checksum存放在文件的元数据文件里,与数据块的具体数据分开存放。当HDFS读取文件时,文件的每个块数据和checksum均被发送到客户端。客户端会即时计算出接受的块数据的checksum, 并将其与接受到的checksum进行匹配。如果不匹配,客户端会通知NameNode,表明接受到的数据块已经损坏,并尝试从其他的DataNode节点获取所需的数据块。
当客户端打开一个文件进行读取时,会从NameNode中获得一个文件数据块列表,列表中包含了每一个所需的数据块的具体位置。这些位置会按照与客户端的距离进行排序。当具体进行数据块读取时,客户端总是尝试首先从最近的位置获取数据。如果尝试失败,客户端会根据排序的顺寻,从下一个位置获取数据。下列情况可能会造成数据读取失败:DataNode不可用,节点不再包含所需数据块,或者数据块备份损坏,以及checksum验证失败。
HDFS允许客户端从正在进行写操作的文件中读取数据。当进行这样的操作时,目前正在被写入的数据块对于NameNode来说是未知的。在这样的情况下,客户端会从所有数据块备份中挑选一个数据块,以这个数据块的最后长度作为开始读取数据之前的数据长度。
HDFS I/O的设计是专门针对批处理系统进行优化的,比如MapReduce系统,这类系统对顺序读写的吞吐量都有很高的要求。针对于那些需要实时数据流以及随机读写级别的应用来说,系统的读/写响应时间还有待于优化,目前正在做这方面的努力。
8.3.2 数据块部署
对于巨大的集群来说,把所有的节点都部署在一个平行的拓扑结构里是不太现实的。比较实际且通用的做法是,把所有的节点分布到多个Rack(服务器机架)上。每个Rack上的节点共享一个交换机,Rack之间可以使用一个或者多个核心交换机进行互联。在大多数情况下,同一Rack中的节点间通信的带宽肯定会高于不同Rack间节点的通信带宽。图8.3描述了一个拥有两个Rack的集群,每个Rack内部包含3个节点。
图8.3 集群拓扑结构图
HDFS默认两个节点之间的网络带宽与他们的物理距离成正比。从一个节点到其父节点的距离被认为是常数1。这样,两个节点之间的距离可以通过将其到各级祖先节点的距离相加计算出来。两个节点之间的距离越短,就意味着他们之间传输数据的带宽越大。
HDFS允许管理员通过配置脚本,返回一个节点的rack标识符,作为节点地址。NameNode位于整个结构的最中央,负责解析每一个DataNode的rack位置。当DataNode注册到NameNode时,NameNode会运行这些配置脚本,确定节点属于哪个rack。如果没有进行脚本配置,NameNode则会认为所有的节点都属于一个默认的Rack。
数据块备份的部署对于HDFS数据的可靠性和读写性能都有至关重要的影响。良好的数据块部署策略能够有效地改进数据的可靠性,可用性,甚至提高网络带宽的利用率。目前的HDFS系统提供了可配置的数据块部署策略接口,以此来让用户和研究人员能够对不同的部署策略进行测验,从而达到对系统应用进行优化的目的。
缺省的HDFS数据块部署策略企图在降低数据写入代价,最大化数据可靠性,可用性,以及整合读数据带宽等几个方面做出权衡。当一个新的数据块被创建,HDFS会把第一个数据开备份放到写入程序所在的位置。第二个和第三个数据块备份会被部署到不同rack的其他两个不同的节点。剩余的数据块备份则被放到随机的节点上,但是限制每个节点不会部署多于一个相同的数据块,每个rack不会部署都与两个相同的数据块(如果条件满足的话)。之所以要把第二个和第三个数据块备份放到不同的rack上,是为了考虑到一个集群上的文件所应当具有的分布性。如果头两个数据块备份放到相同的rack上,那么对于任何文件来说,其2/3的文件块会被存放在同一rack上。
在所有目标节点都被选择后,这些节点会被有组织地按照其亲近程度,以流水线的方式被传输到第一个备份上。数据会被以这个顺序推送到节点。在读取的时候,NameNode首先会检查客户端所对应的主机是否位于集群当中。如果是,那么数据块的位置会被返回到客户端,并以按照距离远近排序。然后数据块就会按照顺序从DataNode中进行读取。
这一策略会降低rack之间以及节点之间的写入时间,普遍提高写入效率。因为rack故障的几率远低于节点故障的几率,所以该策略不会影响到数据的有效性和可用性。在大多数使用3数据块备份的情况下,该策略能够降低网络带宽的消耗,因为一个数据块只需要部署到两个不同的rack上,而不是3个。
8.3.3 数据块备份管理
NameNode会尽力保证们每个数据块都有所需的备份数量。当Block Report从DataNode提交上来的时候,NameNode会侦测到数据块的备份数量是少于所需还是超过所需。当超过时,NameNode会选择删除一个数据备份。NameNode倾向于不减少rack的数量,并在DataNode中选择一个剩余磁盘空间最小的节点进行备份移除。这样做的主要目的是平衡利用DataNode的存储空间,并其不降低到数据块的可用性。
当一个数据块处于低于其备份需求数的状态时,该数据块就会被放入到备份优先队列中。仅拥有一个数据备份的数据块处于最高优先级,其数据备份数高于其备份因子2/3的数据块则处于最低优先级。有一个后台进程专门负责定期对备份优先队列进行扫面,以确定将备份部署到何处。数据块备份遵循与数据块部署相似的策略。如果数据块当前只有一个备份,那么HDFS会把一个新的备份放到不同rack上。如果数据块当前有两个备份,并且连个备份都存在与相同的rack上,第三个备份就会被放到不同的rack上。否则,第三个备份就被放到同一rack的不同节点上。这么做的目的也是为了降低创建备份的代价。
NameNode也会确保不把所有的数据块备份都部署到同一个rack上。如果NameNode侦测到某数据块的所有备份都在一个rack上,那么它就会把这个数据块当做是mis-replicated(误备份),然后它就会用上面所提到的策略,在其他的rack上把这个数据块再备份一次。在NameNode收到异地rack备份成功后,该数据块就成为了“备份数量高于所需备份数”状态。此时NameNode会根据策略把本地的一个备份删除,因为策略规定不能减少rack的数量。
8.3.4 均衡器
HDFS数据块部署策略并不负责DaraNode的负载均衡。这是为了避免把新的(多半是用于备份)的数据放到很少一部分拥有大量闲置存储空间的DataNode上。因此数据不一定会被负载合理地存放在各个DataNode中。在新的节点加入到集群中的时候,也会造成负载不均衡的情况。
均衡器就是一个用来在HDFS集群上平衡磁盘使用情况的工具。它采用一个阈值作为输入参数,其范围在0到1之间。如果某些节点的存储利用率(也就是该节点磁盘存储利用率)不同于整个集群的存储利用率(也就是整个集群的总的存储利用率),那么当类节点占集群总节点数的比例小于阈值时,集群便是负载均衡的。
这个工具是以一个应用程序的形式存在的,它可以根据管理员的要求运行。它能够不断地从高利用率的DataNode中把数据备份移动到低利用率的DataNode中。负载均衡的一个重要因素是要保证维持数据的可用性。当决定把一个数据备份移动到某个目标时,负载均衡器需要保证其决定不会降低这个数据备份所覆盖的rack的数目。
均衡器可以通过最小化rack内数据拷贝的方式来优化均衡过程。如果均衡器决定将数据块备份A移动到不同rack上的节点里,而凑巧此时该节点本身就包含了同一数据块的另一个备份B,这时均衡器就会直接在目标节点上拷贝一份B,用来代替A。
均衡器操作的带宽可以通过一个配置参数进行限制。允许使用的带宽越高,达到均衡状态的时间就越短,但是其与正常的业务进程的竞争冲突也会越大。
8.3.5 数据块扫描器
每个DataNode都会定期运行扫描器,来扫描其数据块备份,并验证存贮的checksum是否与数据块匹配。在每个扫描周期内,扫描器会调整读取带宽,以确保在规定的时间内完成校验工作。如果客户端读完了一个数据块,并且验证checksum成功,它会发一个信息给DataNode. DataNode将其当做对数据块备份的一次验证。
每个数据块的验证时间会被存储到一个可阅读的日志文件里。无论何时,在DataNode的顶级目录里,至少应该包含两个文件:当前日志文件和以前的日志文件。每做一次验证,新的验证时间记录就会被追缴到当前的日志文件中。相应的,每个DataNode的内存中也有一个扫描列表,并且按照验证的时间进行排序。
当客户端或者块扫描器侦测到了一个损坏的块数据时,就会通知NameNode. NameNode会标记相应的块数据为“已损坏”,但是不会立刻安排删除损坏的数据,而是开始进行数据拷贝。当且仅当正常的数据块拷贝超过了其备份因子的时候,损坏的数据才会被安排移除。这个策略主要是为了尽可能长时间地保存数据。即使一个数据块的所有备份都坏了,采用这个策略能让用户有机会从损坏的数据中回复尽可能多有用数据。
8.3.6 停用
集群管理员可以指定一个列表,列出需要停用的节点。一旦某个DataNode节点被标记为停用,它就不会再被选作可以部署的目标节点,但是该节点依然处于可读状态。NameNode开始规划将该节点上的数据块备份到其他的DataNodeshang . 一旦NameNode侦测到所有数据块都已完成备份后,该节点就进入到停用状态。 此时该节点就可以安全地从集群里移除了,而不会对数据的可用性造成影响。

8.3.7 集群内数据拷贝

当与超大数据集协同工作时,将数据导入/导出到HDFS集群里是一个令人畏惧的工作。HDFS提供了一个工具叫做DistCp, 用于大型集群内(局域网内)数据的并行拷贝。它是一个MapReduce任务,每一个Map任务拷贝元数据的一部分到目标文件系统。MapReduce框架会自动处理并行任务的分配,错误侦测和恢复工作。

8.4. Yahoo!的实践

Yahoo! 所采用的大型的HDFS集群共包含4000个节点。集群中的一个典型的节点包含两个四核Xeon处理器,单核的主频达到2.5 GHz,4-12个直连的SATA硬盘(其中每个容量为2TB), 24G物理内存,以及1G带宽的以太网网卡。整个系统中70%的磁盘空间都是由HDFS进行分配,剩余的存储空间则被预留为操作系统(Red Hat Linux),日志以及Map任务所使用(MapReduce所产生的中间数据不会存放在HDFS里)。
一个Rack上有40节点主机,它们共享一个IP交换机。Rack交换机之间通过8个核心交换机相互联系。核心交换机还提供了这些Rack同集群外资源的通信能力。对于每个集群来,其NameNode和BackupNode主机都配有多达64GB的物理内存,不过客户端应用程序的任务不会由这些主机来处理。总的来说,一个拥有4000节点的集群有11 PB(petabyte; 1000 terabytes)的存储能力,如果数据块采用三次冗余复制的策略,则集群可以为应用程序提供多达3.7 PB的分布式数据存储空间。 HDFS投入使用的几年以来,作为集群节点使用的主机也随着技术的发展而不断更新换代。新的集群节点主机会配置更强大的处理器,更大的磁盘存储空间和物理内存。那些相对速度较慢,存储容量较小的节点主机则逐步退役,或者被降级为集群备用主机,用以作为Hadoop的开发测试机。 以这个大型的集群(4000节点)为例,集群中有大约6500000个文件和8000000个数据块。每个数据块通常都有3个拷贝副本,每个数据节点会负责处理60000个数据节点的备份。每天,用户应用程序会在集群上创建多达2000000的新文件。Yahoo!所使用的Hadoop集群的4000个节点可提供40 PB的在线数据存储容量。
作为Yahoo!技术设施的重要组成部分,HDFS需要处理PB级别的公司企业数据。这就意味着处理HDFS的技术问题,其策略与处理研究项目中的问题完全不同。首当其冲的是要保证系统数据的健壮性和耐久性,其次还要兼顾到系统的运行效率,然后还要对用户资源共享进行配额,并考虑系统管理员操作的方便性。

8.4.1 数据耐久性

通过将数据块进行三次冗余备份,能够有效地避免因为不相关节点失效而造成的数据丢失,从而增强数据的健壮性。不过,Yahoo! 倒是不太可能因为这样的途径丢失数据。对于这么大的集群来说,一年内丢失数据块的几率也不到千分之五。此外,对集群的一个关键共识是,每个月大约有0.8%的节点会发生故障。(就算是节点故障后能被恢复,但是一般来说不会去恢复节点里的数据。)所以,对于上述规模的集群来说,每天有1到2个节点发生故障并丢失数据是很正常的。不过,集群在2分钟的时间内,就能重建发生故障的节点上的大约6000个数据块,之所以重建的速度这么快,是因为集群采用并行的方式完成重建工作,而且集群的规模越大,并发的数目就越大,重建的效率也就越高。这样一来,在两分钟之内,集群中的某些节点的所有数据块彻底丢失(来不及重建)的概率就着实很小了。
节点的相关故障则是另一种完全不同的安全威胁。通常情况下最常发生的该类故障包括:Rack故障或者核心交换机故障。HDFS能够做到对一个Rack交换机故障的容错(因为每个数据块在不同的Rack上都有备份)。但是如果一个核心交换机发生了故障,则会影响到集群中的多个Rack,这样就有可能导致部分数据块完全无法获取。另一种可能造成节点相关故障的情况,是计划外或计划内的断电。如果多个Rack同时断电,很可能造成数据块无效。而且即使是及时恢复了电力也不一定能恢复数据,因为大约有0.5%到1%的节点很可能在完全断电后重启的过程中失效。根据统计数据,在实际情况中,规模较大的集群在断电重启的过程中确实存在这部分节点数据丢失的情况。
除了全部节点失效的情况以外,存储到硬盘的数据也可能发生损坏或者丢失。数据块扫描器对所有的数据块进行全盘扫描,每两周进行一次,通过扫描结果可以发现,在整个过程正大约会发现20个损坏的数据块副本。数据块副本一旦发现损坏,就会被立即替换。

8.4.2 共享HDFS的特性

随着HDFS使用的不断普及,这个文件系统所面对的用户越来越多,在资源共享方面的意义也越来越重要。共享HDFS的第一个特性,在于它的文件和目录的权限构架与Unix系统的权限模式非常接近。在HDFS的权限框架中,文件和目录对于不同的拥有者,以及相关用户组其他成员,都有独立的访问权限。Unix(POSIX)和HDFS的主要区别,在于HDFS中的普通文件既没有执行权限,也没有sticky位(Unix中拥有sticky位的文件,只有其拥有者和root用户才能进行删除和重命名等操作——译者注)。
在HDFS的早期版本中,用户身份标识相对较弱:你的主机认为你是什么身份,你就是什么身份。当接入到HDFS时,应用程序客户端仅仅是简单地要求进行本次操作系统的用户身份认证和用户组身份认证。在新的权限框架中,应用程序客户端必须登录从可信的认证源获取命名系统证书,然后才能登入。HDFS可以选用不同的证书认证管理技术,最初采用的是Kerberos(麻省理工学院开发的安全认证系统)。用户程序可以使用相同的框架来确认命名系统也拥有可靠的身份。命名系统也可以从集群中的任何一个数据节点来申请证书。
HDFS总共的可用数据存储空间是通过数据节点的个数,以及每个节点的存储配比来决定的。HDFS早期的使用经验证明,在用户群中实施一些资源分配策略是非常有必要的。实施策略的目的不仅仅是要保证共享的公平性,还要考虑在多个用户程序同时操作上千台主机,进行写数据操作时,如果才能确保应用程序不会将资源耗尽。对于HDFS来说,因为系统元数据总是存储在RAM中,命名空间的大小(也就是文件和目录的数量)是有限资源。为了管理存储和命名空间资源,目录的配额大小应当由命名空间的子树中文件所占用的大小总和来决定,其中命名空间应当以该目录为起始。当然,也可以为文件的总数和子树中的目录设定单独的配额。
如果说HDFS的架构的主要目的在于应付应用程序产生的大数据集输入,MapReduce程序框架则趋向于生成大量小的输出文件(每个文件对应一个reduce任务),这些文件会给命名空间资源带来压力。通常情况下,一个目录子树会被压缩到一个单独的Hadoop归档文件中(HAR),以节约存储空间。HAR(Hadoop归档文件)类似于常见的tar,JAR,或者Zip文件,HDFS能够随时访问到压缩归档文件中的某个文件,因此,HAR文件可以在MapReduce作业中作为输入文件透明使用。

8.4.3 扩展性和HDFS联盟

NameNode的扩展性曾经是一个关键的难题[Shv10]。因为NameNode将所有的命名空间和数据块的位置存储在了其内存当中,这样一来,NameNode的堆内存就限制了文件的数量,同时也限制了数据块的地址存放的数量。同理,这也限制了整个集群的存储容量,因为集群需要NameNode的支持才能工作。HDFS鼓励用户尽量使用大文件,来减少NameNode的开支,但是在实际应用中,由于应用程序需求不断地变化,这个办法并不奏效。此外,我们发现很多新的HDFS应用程序就是需要存储大量的小文件。我们通过设定配额管理,以及提供归档工具来进行调整,但是这些都没有能够从根本上解决扩展性的问题。
改进后的HDFS增加了一个新的特性,允许多个独立命名空间(以及NameNode)共享集群中的物理存储。命名空间可使用数据块池(Block Pool)中的数据块。数据块池类似于SAN(存储区域网 Storage Area Network)存储系统里的逻辑单元(LUNs),使用数据块池的命名空间就仿佛文件系统中的“卷标”一样。
使用这种方法,不但提高了扩展性,还同时具备了其他几个优点:它可以将不同应用的命名空间分离开来,改善集群的整体可用性。数据块池抽象允许其他服务通过不同的命名空间结构来使用数据块存储。我们正在计划探寻其他的方式来实现扩展性,例如只把部分的命名空间存储在内存里,以及真正实现分布式的NameNode。
应用程序往往连续使用一个命名空间。命名空间能够被挂接(mount),从而形成统一的视图。客户端挂接表为此提供了有效的途径,与服务器端挂接表相比,客户端的挂接表避免了与中央挂接表进行远程过程调用(RPC),它的容错性也更好。将集群范围的命名空间共享是最简单的实现方法,把所有集群的客户端指向同一个客户端挂接表就可以实现。客户端挂接表也允许应用程序创建私有的命名空间视图。这个工作机制与进程独占的命名空间相似,可以用来处理分布系统中的远程调用。

8.5 经验

Hadoop文件系统的开发团队规模很小,却成功地保障了系统的稳定性和健壮性,让它能够符合实际应用的要求。之所以能够成功地做到这一点,很大程度上是鉴于HDFS拥有一套非常简洁的构架:数据块备份,定期数据块报告和中央元数据服务器。简化POSIX语义也很重要。尽管把整个元数据都保存在内存中会影响到命名空间的扩展性,但是却保证了NameNode的简洁性,有效地避免了一般分布式系统采用的复杂的锁机制。Hadoop能够成功的另一原因在于快速地将系统应用到了Yahoo!的产品当中,让系统能够快速地递增改进。HDFS非常健壮,因为NameNode极少出问题;实际上,大多数的机器非运行时间是由于软件升级造成的。直到最近,才有相应的故障备份方案(尽管是手动操作)加入到系统管理中来。
很多人对使用Java语言构建可扩展的数据库系统表示非常惊讶。因为Java通常被认为消耗内存过大,垃圾回收效率太低,会影响到NameNode的扩展性。虽然上述的问题的确存在,不过Java对系统的健壮性也有显著的贡献,因为Java的语言机制成功回避了指针和内存管理bug可能导致的系统问题(C/C++语言在指针和内存管理上被认为非常难以维护——译者注)。

8.6 鸣谢

我们感谢Yahoo!投资给Hadoop项目,并长期将其保持为开源项目。80%的HDFS和MapReduce代码是在Yahoo!开发的。我们感谢所有的Hadoop贡献者和合作者,你们为项目做出了很大的贡献!