作者:Konstantin Shvachko, Hairong Kuang etc. 2010-10
原文:http://storageconference.org/2010/Papers/MSST/Shvachko.pdf
原文:http://www.aosabook.org/en/hdfs.html
译者:phylips@bmy 2011-9-12
译文:http://duanple.blog.163.com/blog/static/70971767201181744412133/
摘要
Hadoop分布式文件系统(HDFS)设计用于为大规模数据集提供可靠性的存储,同时能够将数据集以高带宽的传输速率推送给用户应用程序。在一个大规模集群上,将会有数千台的服务器同时负责数据存储及执行用户应用级的计算任务。通过将存储和计算分布到很多个服务器上,使得存储和计算资源可以在保持低成本的情况下根据数据规模按需增长。在本文中,我们会描述下HDFS的架构,以及我们在Yahoo!使用HDFS来管理25PB的企业数据的相关经验。
1.简介及相关工作
Hadoop提供了一个分布式文件系统及一个使用MapReduce范式进行大规模数据集分析和转换的框架。Hadoop的一个重要特点是,将数据和计算划分在很多(数千台)主机上,同时直接在这些数据附近并行执行计算应用{!即存储数据的跟执行计算的是同一个节点集合,这就可以很容易地将计算移动到数据附近执行}。一个Hadoop集群可以简单地通过增加服务器来对计算能力、存储能力及IO带宽进行扩展。Yahoo!的Hadoop集群目前已包含25000台服务器,存储了25PB的应用数据,最大的集群目前包含3500台服务器。目前世界上已有上百个组织宣布他们采用了Hadoop。
Hadoop是一个Apache项目;所有的组件都遵循Apache开源许可证。在Hadoop核心组件(HDFS和MapReduce)中,其中80%都是由Yahoo!开发和贡献的。HBase最初是在Powerset开发的,现在它已经是微软的一个部门。Hive最初由Facebook开发。Pig,ZooKeeper,及Chukwa都是由Yahoo!发起并开发的。Avro也是源自Yahoo!,目前Cloudera也在参与它的开发。
HDFS是Hadoop的文件系统组件。它的接口类似于Unix文件系统,但是为了提高应用程序性能,它并没有严格遵从标准。
HDFS将文件系统元数据和应用程序数据分开存储。像其他的一些分布式系统比如PVFS,Lustre及GFS一样,HDFS将元数据存放在一个被称作NameNode的专门的服务器上。应用数据则被存储在称作DataNode的其他服务器上。所有的服务器都是相通的,相互之间通过基于TCP的协议进行通信。
与PVFS和Lustre不同,HDFS的DataNodes没有使用像ACID这样的数据保护机制来保证数据的持久性。而是像GFS那样,通过将文件内容复制到多个DataNodes上来保证可靠性。在保证数据持久性的同时,这种方式也带来了一些额外的好处,比如数据传输带宽变成了原来的几倍,同时提高了将计算移动到数据附近的可能性(locality)。
一些分布式文件系统目前也在探索一些名字空间的真正的分布式实现方式。比如Ceph使用一个具有多个名字空间服务器(MDS-MetaDataServer)的集群,同时使用一个动态的子树划分算法来将名字空间树均匀地映射到MDSs上。GFS也已经演化成一个分布式名字空间的实现。新一代的GFS将会具有数百个名字空间服务器(masters),其中的每个都能管理100 million的文件。Lustre在2.2版中,已经具有一个集群化的名字空间实现。目的就是为了将一个目录划分到多个元数据服务器(MDS),让每个服务器负责名字空间的一部分。文件会通过对文件名称使用一个hash函数来分配到特定的MDS上。
2.架构
2.1NameNode
HDFS名字空间是一个由文件和目录组成的层次性结构。在NameNode上,文件和目录通过inode标识,每个inode会记录像访问权限、修改信息、访问时间、名字空间及磁盘空间qutas这样的一些属性。文件内容会被切分成很多大的blocks(通常是128MB,用户可以为每个文件设定自己的block大小)同时组成文件的每个block会被复制到多个DataNodes上(通常是3,用户也可以为每个文件设定自己的副本数)。NameNode维护一个名字空间树及文件blocks到DataNodes的映射信息(即文件数据的物理位置)。当HDFS Client想读取文件时,必须与NameNode联系以获取组成该文件的blocks的位置信息,然后选择一个离它最近的DataNode去读取block内容。在写数据时,client向NameNode发送请求,让它指定应该由哪三个DataNodes来保存该block的三个副本。之后客户端就会以pipeline的模式将数据写入到DataNodes。当前的设计中,每个集群中只有一个NameNode。但是每个集群可以有数千个DataNodes及数万个HDFS clients,因为每个DataNode可能会同时执行多个应用程序任务{!所以HDFS clients的数目可能比DataNodes多个数量级}。
HDFS会将整个名字空间保存在内存中。由inode数据及每个文件包含的所有blocks列表组成的名字系统元数据叫做image。保存在本机本地文件系统中的该image的一个持久化记录称为一个checkpoint。NameNode也会将称为journal的针对该image的修改日志保存到本机本地文件系统中。为了提高持久性,可以在其他服务器上保存checkpoint和journal的多个副本。在重启的时候,NameNode会通过读取名字空间checkpoint及重放journal来恢复名字空间。Block副本位置信息可能会随着时间而改变,同时它们也不是持久化的checkpoint的组成部分。
2.2DataNode
DataNode中的每个block副本由本机本地文件系统中的两个文件组成。第一个文件包含数据本身,第二个文件是该block的元数据包括该block数据的校验和及该block的世代戳(generation stamp)。数据文件大小等于该block的实际长度,同时不需要补上额外的空间以达到标准的块大小{!比如该block只有10MB,那么本地文件系统中的数据文件大小就是10MB,而无需在额外补足让它变成标准的128MB}。因此,如果一个block只有标准大小的一半,那么本地磁盘也只需要半个标准block所需的空间。
在每个DataNode启动时,它会连接到NameNode执行一个握手。握手的目的是为了验证名字空间ID及DataNode的软件版本。如果其中只要有一个无法与NameNode匹配,那么DataNode会自动关闭。
名字空间ID是在文件系统创建时分配给它的实例编号。名字空间ID会持久化存储在集群的所有节点中。具有不同名字空间ID的节点无法加入到集群中,这就保护了文件系统的数据完整性。
软件版本的一致性是非常重要的,因为不兼容的版本可能会导致数据损坏或丢失,同时在一个具有数千个节点的大规模集群上,很容易会在升级期间忽略掉某些节点,比如它没有在升级之前正确的关闭或者在升级时处于不可用的状态。
允许一个新初始化的并且没有任何名字空间ID的DataNode加入到集群中,它会接受集群的名字空间ID。{!这是因为很多情况下我们需要对集群进行扩容,因此HDFS应该允许我们往集群中添加新机器}
在握手过程完成之后,DataNode会与NameNode进行注册。DataNodes会持久化存储它们自己对应的那个唯一的存储ID。存储ID是DataNode的内部标识符,可以保证即使是它更换了IP地址或者端口也能识别出来。存储ID是在DataNode第一次向NameNode进行注册时分配的,之后它就再也不会改变。
DataNode会通过向NameNode发送一个block report来声明它所拥有的block副本。一个block report包含该block的id,世代戳(generation stamp)以及它所持有的block副本长度。当DataNode注册完成之后就会立即发送第一次的block report。之后,会每隔1小时就进行一次block reports发送,从而为NameNode提供关于该集群内的所有block副本的最新位置信息。
在正常情况下,DataNode会向NameNode发送心跳信息以证实它自己正在运行以及它所持有的block副本是可用的。默认的心跳周期是3秒钟。如果NameNode在十分钟内收不到来自某个DataNode的心跳信息,它会认为该DataNode已经不能提供服务,它所持有的block副本就变成了不可用状态。NameNode就会将这些block副本在其他DataNode上创建出来。
来自DataNode的心跳中还会携带一些关于总的存储容量、存储空间使用量及当前正在处理的数据传输量方面的信息。这些统计信息会被用于NameNode的空间分配及负载平衡决定中。
NameNode不会直接联系DataNode,它会通过对心跳的响应信息来向DataNodes发送指令。这些指令包括如下一些命令:
l 复制blocks到其他节点
l 删除本地的block副本
l 重新注册或者关闭节点
l 发送一个即时block report
这些命令对于维护整个系统的完整性是十分重要的,因此就算是在大规模的集群中,保持心跳的通畅也是非常重要的。NameNode每秒可以处理数千个心跳请求而不会影响到其他的NameNode操作。
2.3HDFS Client
用户应用程序通过HDFS Client(一个包含HDFS文件系统接口的代码库)来访问文件系统。
类似于大部分的传统文件系统,HDFS支持文件的读写和删除操作,以及针对目录的创建和删除操作。用户通过名字空间里的路径来访问文件和目录。用户应用程序通常并不需要知道文件系统元数据和数据存储是位于不同的服务器上的,或者是一个block是有多个副本的。
当一个应用程序读取一个文件时,HDFS client首先向NameNode询问持有组成该文件的blocks的DataNodes列表。然后直接联系某个DataNode请求对于它所需要的block的传输。当client进行写的时候,它会首先让NameNode选定持有该文件的第一个block的那些DataNodes。客户端会把这些节点组织成一个pipeline,然后发送数据。当第一个block写出后,客户端会继续请求选定持有下一个block的新的DataNodes。新的pipeline会被建立起来,客户端开始发送该文件后面的那些数据。每次选定的DataNodes可能是不同的。NameNode和DataNodes与客户端的交互如图1所示。
图1.一个HDFS客户端通过向NameNode传输path创建一个新文件。对于该文件的每个block,NameNode返回持有它的副本的那些DataNodes列表。客户端然后将数据通过pipeline的形式传送给选定的DataNodes,DataNodes最终会再联系NameNode对block各副本的创建情况进行确认。
与传统文件系统不同,HDFS提供了一个API用于提供某个文件的blocks的位置信息。这就允许应用程序比如MapReduce框架可以将task调度到数据所在的节点上,这就提高了读性能。同时它也允许应用程序对文件的副本数进行设置。默认情况下,文件的副本数是3。对于某些重要文件或者是某些经常被访问的文件,可以增大该参数值以提高容错性及读取带宽。
2.4Image与Journal
名字空间image是代表应用数据的目录和文件组织方式的文件系统元数据。写入到磁盘中的image的持久化记录称为checkpoint{!image在内存中,checkpoint则是在磁盘中}。Journal是一个记录了那些必须被持久化的文件系统变更的write-ahead commit日志。对于每个客户端发起的事务,变更会被记录到journal中,在变更提交给HDFS客户端之前journal文件必须被flush及sync。checkpoint文件永远不会被NameNode修改;当在重启时创建好新的checkpoint时,或者在管理员或下一节描述的CheckpointNode发出请求时,它会被整个替换掉。在NameNode启动时,会根据checkpoint初始化名字空间image,然后重放journal中的变更直到image更新到文件系统的最终状态。在NameNode提供服务之前,一个新的checkpoint和空的journal会被写回到存储目录下。
如果checkpoint或者是journal丢失或者损坏了,名字空间信息将会部分地或者整个地丢失。为了对关键信息进行保护,可以将HDFS配置成将checkpoint和journal在多个存储目录下存放。推荐性的做法是将这些目录放在不同的逻辑卷上,或者是在远程NFS服务器上的某个存储目录下。第一种做法可以防止单个逻辑卷的损坏造成数据丢失,第二种做法可以应付整个节点失败的情况。如果NameNode在将journal写入到某个存储目录下的过程中出错,那么它会自动地将该目录从存储目录列表中排除。如果没有存储目录可用,NameNode会自动地停止运行。
NameNode是一个多线程系统,可以同时处理来自多个客户端的请求。将事务日志保存到磁盘就成了系统的瓶颈,因为所有的线程都必须等到其中某个线程发起的flush-and-sync调用结束。为了优化该处理过程,NameNode会将由不同客户端产生的多个事务批量进行处理。当其中某个NameNode线程发起flush-and-sync调用时,堆积在此刻的所有事务会一块进行提交。其他线程只需要检查下它们的事务是否被写入了而不需要再发起一个flush-and-sync调用。
2.5CheckpointNode
HDFS中的NameNode,除了可以担任客户端请求服务者这一首要角色外,还可以担任其他的一些角色比如CheckpointNode或者是BackupNode。节点可以在启动时设置它的角色。
CheckpointNode会周期性地合并现有的checkpoint和journal,创建一个新的checkpoint和一个空的journal。CheckpointNode通常运行在与NameNode不同的一个节点上,因为它需要与NameNode等同的内存空间。它会从NameNode下载当前的checkpoint和journal文件,然后在本地对它们进行合并,然后将新的checkpoint返回给NameNode。
创建周期性的checkpoints是保护文件系统元数据的一种方式。如果名字空间image的所有持久化拷贝或者journal不可用了,系统就可以从最近的那个checkpoint处恢复。
当新的checkpoint上传到NameNode后,checkpoint的创建需要NameNode在journal的尾部进行截断{!即此时若要创建checkpoint应该截断当前journal,而新的修改日志应该写入到新的journal中,当前的journal会跟旧的checkpoint一起用于新checkpoint的创建}。HDFS集群如果长期运行而不重启的话,那么在此期间journal会持续增长。如果journal变得很大的话,那么journal文件发生数据丢失或损坏的概率就会上升。同时,一个很大的journal文件也会增加NameNode重启所需的时间。对于一个大规模的集群来说,可能会花一个小时来处理一个已存在一周的journal。因此最好每天都进行checkpoint的创建。
2.6BackupNode
BackupNode是HDFS最近引入的一个feature。与CheckpointNode类似,BackupNode能够创建周期性的checkpoints,但是除此之外它还在内存中维护了一个文件系统名字空间的最新映像,该映像会一直与NameNode状态保持同步。
BackupNode会接受来自处于活动状态的那个NameNode的名字空间事务形成的journal流,它会将它们存放到自己的存储目录下,同时将这些事务应用到它自己的内存映像中。NameNode会像对待存储它的journal文件的存储目录那样,将BackupNode作为它的一个journal存储目标。如果NameNode出错了,那么BackupNode的内存映像以及磁盘上checkpoint都记录了最新的名字空间状态{!即BackupNode的内存映像已经是最新的名字空间,checkpoint+journal也可以用来恢复名字空间状态}。
BackupNode可以不用从处于活动状态的NameNode下载checkpoint和journal文件就能创建一个checkpoint,因为它的内存中已经具有了最新的名字空间状态。这使得在BackupNode上的checkpoint处理更高效,因为它只需要将名字空间保存到本地的存储目录下。
BackupNode可以看做是一个只读的NameNode。它包含除block位置信息之外的所有文件系统元数据信息。除去那些会引入名字空间改变或者是需要了解block位置信息的操作之外,它也可以执行其他所有的常规NameNode操作。通过将名字空间状态的持久化授权让BackupNode处理,这样BackupNode的使用就提供一种不需要持久化存储及名字空间授权的NameNode运行选择{!即NameNode运行时可以自己不进行持久化存储了,而让BackupNode来负责,这就降低了NameNode的负载}。
2.7升级,文件系统快照
在软件升级期间,由软件bug或者人为失误导致的系统崩溃概率会上升。在HDFS中创建快照的目的是为了最小化系统升级期间对存储的数据的潜在威胁。
快照机制使得管理员可以将文件系统的当前状态进行持久化保存,这样如果升级导致数据损坏或丢失时,可以对升级进行回滚,使得HDFS回到快照创建时的名字空间和存储状态。
快照(只能有一个)可以通过集群管理员配置进行创建,而不管系统是何时启动的。当接受到快照请求后,NameNode会首先读取checkpoint和journal文件,然后在内存中合并它们。然后,它会写出一个新的checkpoint及空的journal到一个新的位置,这样旧的checkpoint和journal文件就仍然是保持不变的。
在握手期间,NameNode会向DataNodes发出一个创建本地快照的命令。本地快照不能通过简单地对目录下文件进行复制来实现,因为这会导致集群中所有DataNodes节点的存储空间加倍。每个DataNodes不是真正创建存储目录的一份拷贝,而是为现有的block文件创建出硬链接到存储目录下。当DataNodes删除一个block时,它只是删除了这个硬链接,当append操作导致block内容改变时会采用copy-on-write技术。因此老的目录中的老的block文件依然是处于未改变的状态。
集群管理员可以在重启系统时选择让HDFS回滚到快照状态。NameNode会使用快照创建时保存的那个checkpoint进行恢复。DataNodes会恢复之前被重命名的目录,同时启动一个后台线程去删除在快照之后创建的block副本。一旦选择了回滚,就不能在退回到之前的状态了。集群管理员也可以通过命令系统丢弃快照来释放由快照所占用的空间,然后完成软件升级。
系统的演化可能会导致NameNode的checkpoint和journal文件格式或者是DataNodes上的block副本文件的数据表示方式发生变化。Layout version会被用来标识数据表示格式,它会被持久化地保存到NameNode和DataNodes的存储目录中。在启动时,每个节点都会将当前软件的Layout version与存储在存储目录下的版本进行比较,并自动地将数据从旧的格式转换为新的。当系统使用新的Layout version重启时,该转换会强制性地创建一个快照。{!升级分很多种,对于普通的升级,可以让管理员手动选择是否开启snapshot,但是对于这种涉及到Layout version变更的情况,系统会强制性的进行snapshot}
HDFS并没有区分是NameNode还是DataNodes的Layout versions{!也就是说无论是NameNode还是DataNodes发生了Layout versions的改变,系统都会认为发生了改变,而进行相同的处理},因为snapshot的创建是整个集群层面的事情而不是单个节点级的事件。如果升级后的NameNode因为一个软件bug而清除了它的image,那么如果只是备份了名字空间状态仍然会导致所有数据的丢失,因为NameNode无法识别DataNodes报告的blocks,就会发出一个删除命令。在这种情况下回滚虽然恢复了元数据,但是数据本身还是丢失了。
3.文件IO操作及Replica管理
3.1文件读操作与写操作
应用程序会通过创建新文件然后向文件写入数据来向HDFS添加数据。在文件关闭之后,已写入的字节串就不能被改变或删除,但是可以通过重新打开该文件通过append操作为该文件增加新数据。HDFS实现了一个单写者,多读者模型。
HDFS客户端打开一个文件用于写操作时会被授予该文件的租约;这样其他的客户端就不能再对该文件进行写入。正在进行写入的那个客户端会通过向NameNode发送的心跳信息周期性的更新该租约。当该文件被关闭时,租约就会被释放。租约持续时间通过一个soft limit和hard limt进行限定。在soft limit过期之前,写者肯定会独占针对该文件的访问。如果soft limit过期了,而客户端没有成功的关闭该文件或者更新该租约,另一个客户端将会优先获取到该租约。如果hard limt过期(1小时),同时客户端仍未能成功更新该租约,HDFS会假设该客户端退出了同时会代替该写者自动地关闭该文件,然后释放该租约。写者租约不会阻止其他客户端读取该文件;一个文件可能具有多个并发读者。
一个HDFS文件是由多个blocks组成。当存在一个新block请求时,NameNode会分配一个具有唯一block ID的block,然后确定用于保存该块的多个副本的DataNodes列表。DataNodes会组成一个pipeline,它们的排列顺序会尽量的最小化从客户端到最后一个DataNode的总的网络距离。然后数据会以一系列的packets的形式推送到该pipeline中。应用程序写入的数据会首先缓存在客户端的一个packet缓存中。当一个packet buffer被填满(默认是64KB大小)时,数据就会被推送到pipeline。在收到前面的packets的确认信息之前,下一个packet就可以被直接推送到pipeline中。处于outstanding状态的packets数目是通过客户端的一个发送窗口大小限制的。
在数据写入到HDFS文件之后,在文件关闭之前,HDFS不提供任何保证以确保新的读者可以看到该数据。如果一个用户应用程序需要这种可见性保证,它可以显式地调用hflush操作。这样当前的packet会被立即推送到pipeline中,而hflush操作会等待直到收到来自pipeline中的DataNodes关于该包成功传输的确认为止。这样在hflush操作之前写入的所有数据对于读取者来说就肯定是可见的了。
如果没有错误发生,块的构建就会经过像图2那样的三个阶段。图2展示了一个具有三个DataNodes的流水线及5个pakctes的block。图中,粗线代表了数据包,虚线代表了确认消息,细线代表了用于建立和关闭流水线的控制消息。竖线代表了客户端及3个DataNodes的活动,时间流向是自上而下的。从t0到t1是流水线建立阶段,t1到t2是数据流阶段,t1代表了第一个数据包被发送的时间点,t2代表了针对最后一个数据包的确认信息的接收时间点。在这里,在第二个包传输时有一个hflush操作。hflush操作标识是与数据打包在一块的,而不是独立的一个操作。最后,t2到t3是针对该block的pipeline关闭阶段。
在一个具有数千个节点的集群中,节点失败(通常都是存储系统错误)每天都会发生。因此存储在某个DataNode上的副本可能会因为一个内存,磁盘或网络问题而损坏。HDFS会生成并存储针对HDFS文件中每个block的校验和。校验和在HDFS客户端读取时会进行验证,以检测因客户端,DataNodes或者是网络导致的损坏。当客户端创建一个HDFS文件时,它会为每个block计算校验和,然后将它与实际数据一块发送给DataNodes。DataNode会将校验和存储在与块数据文件独立的一个元数据文件中。在HDFS读取一个文件时,每个block的数据和校验和会被传送给客户端。客户端会对接受到的数据计算校验和,并验证它实时计算出的校验和与收到的校验和是否匹配。如果不匹配,客户端会告知NameNode该副本损坏了,之后会从另一个DataNode上获取该block的另一份副本。
当客户端打开文件进行读操作时,它会从NameNode获取一个blocks列表,及关于每个block副本的位置信息。每个block的位置信息会根据它们与客户端的距离进行排序。在读取block的内容时,客户端会首先尝试从最近的那个副本处进行读取。如果这个读取尝试失败了,客户端会继续尝试从序列中的下一个副本处读取。在目标DataNode不可用的情况下读取可能会失败,比如该DataNode可能不再持有该block的副本了,或者在检查校验和时发现副本是损坏的。
HDFS允许客户端去读取一个已打开的正在用于写操作的文件。在读取正在被写入的文件时,最后一个block因为正在被写入因此对于NameNode它的实际大小是未知的。在这种情况下,客户端在开始读取内容前可以询问其中某个副本得到其最新的长度。
HDFS的IO设计是为像MapReduce这样需要高顺序读写吞吐率的批处理系统特殊优化过。但是,为了支持像Scribe这种实时地向HDFS进行数据流导入,或者是像HBase这种提供对大表格的随机实时性访问的这些应用,还需要花费很大的精力来提高HDFS的读写响应时间。
3.2Block放置
对于一个大规模集群来说,对所有的节点采用一种平摊的拓扑连接方式可能是不切实际的。通常的做法是将它们分布到多个机柜中。单个机柜中的节点共享一个交换机,机柜之间通过一个或多个核心交换机相连。这样在不同机柜中的节点间的通信需要跨越多个交换机。大多数情况下,相同机柜内节点间的网络带宽要比不同机柜的节点间的网络带宽要高。图3描述了一个具有2个机柜的集群,每个机柜包含三个节点。
HDFS会根据两个节点间的距离来估算它们的网络带宽。假设从节点到它的父节点间的距离是1。那么任意两个节点的距离就可以通过将它们到它们的最近公共祖先间的距离求和而得到。距离越短意味着可以用于数据传输的带宽越大。
HDFS允许管理员安装一个脚本,给定一个节点地址该脚本就可以返回该节点所在的机柜信息。NameNode会负责解析各个DataNode的机柜位置。当DataNode向NameNode注册时,NameNode会运行该脚本来确定该DataNode属于哪个机柜。如果该脚本没有安装,NameNode会假设所有DataNode都属于默认的同一个机柜中。
副本的放置对于HDFS的数据可靠性和读写性能都是至关重要的。一个好的副本放置策略可以提高数据可靠性,可用性及网络带宽利用率。当前的HDFS提供一个可配置的块放置策略接口,这样用户和研究人员就可以进行实验测试以为他们的应用选择更好的放置策略。
默认的HDFS block放置策略在最小化写开销和最大化数据可靠性、可用性以及总体读取带宽之间进行了一些折中。当一个新的block创建时,HDFS会将第一个副本放置在writer本身所在的那个节点上,第二个和第三个副本将会被放到另一个机柜的两个不同节点上,再剩下的就会被随机地放置,但需要保证如下几个条件:一个节点上最多只能放一个副本;如果副本数小于机柜数的2倍,那么同一个机柜上最多能放两个副本。我们选择将第二个和第三个副本放到另一个机柜上可以更好的将blocks分布到集群上。如果前两个副本被放置在相同的机柜上,那么对于任意文件来说,那么它的三分之二的blocks副本都会被放到相同的机柜上{!因为第一个副本已经定了,会被放到writer本身所在的那个节点,那么根据这种策略第二个副本也会放到上面,与此同时因为writer一直处在该节点上,那么其他block也会被这样放置,最后就会导致该文件至少有三分之二的blocks副本会被放到writer本身所在的那个节点上}。
当所有的目标节点选定之后,这些节点会以它们与第一个副本的接近程度为序组织成一个流水线的形式。对于读取来说,NameNode会首先判断客户端所在主机是否在集群中,如果是的话,block的位置信息会以它们与该客户端的接近程度为序返回给客户端。Block从DataNodes中读取时就会参照这个顺序。(这对于那些直接运行在集群内部节点上MapReduce很有用,当然了实际上一个主机只要可以连接到NameNode和DataNodes,就可以在它上面运行HDFS client)
这种策略降低了机柜间及节点间的写流量,提高了写性能。由于单个机柜的失败概率要远低于单个节点的失败概率,这种策略也不会影响数据可靠性和可用性。在三个副本的情况下,它也能降低读取时地总的网络带宽,因为一个block仅被放在两个机柜而不是三个上。
默认的HDFS副本放置策略可以概述如下:
1. 每个DataNode最多包含block的一个副本
2. 在集群具有足够的机柜的情况下,每个机柜最多包含同一个block的两个副本
3.3Replication管理
NameNode会尽量保证每个block总是具有期望的副本数。当来自DataNode的block report到达时,NameNode会检测到那些副本数过少(under- replicated)或过多(over-replicated)的block。当一个block的副本数过多,NameNode会选择一个副本进行删除。NameNode首先会尽量不减少持有该副本的机柜数,其次会倾向于从那个具有最少的可用磁盘空间的DataNode上进行删除。目标就是尽量平衡DataNodes的存储空间使用率,同时又不降低block的可用性。
当一个block的副本数过少时,它会被放入一个replication优先队列。只有一个副本的block会具有最高的优先级,那些具有三分之二以上的完好副本数的blocks具有最低的优先级。后台线程会周期性地扫描该队列的头部来决定新副本的放置。Block replication会遵循一个与前面的新副本放置类似的策略。如果现有副本数是一,HDFS会将下一个副本放置到不同的一个机柜上。在现有副本数是二的情况下,如果现存的两个位于同一个机柜上,那么第三个副本将会被放到另一个机柜上;反之,第三个副本将会被放置到与现有的某个副本相同机柜的不同节点上。这里的目标是为了减少新副本的创建开销。
NameNode也会保证一个block的所有副本不会被放置到同一个机柜上。如果NameNode检测到某个block的所有副本都处于同一个机柜上,NameNode会将该block当做是副本数过少的情况,然后使用与前面相同的放置策略将block复制到另一个机柜上。当NameNode收到副本创建成功的通知之后,该block就变成了副本数过多的状态。之后NameNode会决定删除一个旧的副本,因为针对副本数过多的情况,处理策略是尽量不降低机柜数。
3.4Balancer
HDFS块放置策略没有考虑DataNode的磁盘空间使用状况。主要是为了避免将新数据(更有可能被访问)聚集到个别的DataNodes上。因此数据可能并不是总是均匀分布的。同时当有新节点添加到集群中时,集群也会处于imbalance状态。
Balancer是一个用于平衡HDFS集群的磁盘空间使用率的工具。它以一个取值范围在(0,1)的阈值作为输入参数。如果对于每个DataNode来说,它的磁盘空间使用率(已用空间占节点总的存储空间的比率)与整个集群的使用率(整个集群的已用空间占集群总存储空间的比率)差值不超过该阈值,我们就认为该集群已处于平衡状态。
该工具作为一个可以由集群管理员运行的应用程序部署在集群上。它会不断地将副本从使用率高的DataNodes移动到使用率低的DataNodes上。对于Balancer的一个关键需求就是保持数据的可用性。在选择一个副本的移动目标时,Balancer需要保证此次移动既不能降低副本数也不能降低机柜数。
Balancer会通过最小化机柜间的数据拷贝来进行优化。如果Balancer决定副本A需要移到另一个不同的机柜上时,恰好目标机柜上有该block的另一个副本B,那么数据会从B处直接进行拷贝而不需要再从A处。
还有一个配置参数可以用来限制rebalancing操作消耗的带宽。允许它消耗的带宽越高,集群就能越快达到平衡状态,但是也会带来与应用程序进程间更大的资源竞争。
3.5Block Scanner
每个DataNode会运行一个block scanner周期性地扫描它的block副本,验证block数据与存储的校验和是否匹配。在每个扫描周期中,block scanner会调整读取带宽以保证可以在配置的时间周期内完成验证。当客户端读取一个完整的block并且检验和验证成功,它会通知DataNode。DataNode会将它视为一个对该副本的有效验证{!即因为客户端读取时会进行校验和验证,这样我们就可以直接利用它的验证结果,而不需要DataNode的block scanner再去验证,这就节省了计算资源}。
每个block校验的时间点会存储在一个人工可读的日志文件中。在任意时刻顶层的DataNode目录下,都会有两个文件,当前的及前一个日志。新的校验时间会被append到当前的文件中。相应地,每个DataNode在内存中都保存了一个根据副本校验时间排好序的扫描列表。
无论何时当一个正在读的客户端或者block scanner检测到一个损坏的block时,都会通知NameNode。NameNode会将该副本标记为损坏,但是不会立即对该副本进行删除,而是开始为该block复制一个完好的拷贝。只有当好的副本数达到该block的正常副本数的情况下,那个损坏的副本才会被删除。该策略旨在尽可能地对数据进行保护。因此即使某个block的所有副本都损坏了,该策略还能允许用户从损坏的副本中恢复数据。
3.6Decommissioning(下线)
集群管理员可以通过列出允许进行注册的主机地址和不允许进行注册节点的主机地址,来指定可以加入到集群的节点。管理员可以命令系统重新计算这两种列表。如果集群中现有的一个节点出现在了排除列表中,就会被标记为Decommissioning。一旦一个DataNode被标记为Decommissioning,它就不会再被选定为副本放置的目标,但是它仍会继续响应读请求。NameNode会开始将它上面的blocks的副本调度到其他DataNodes上。一旦NameNode检测到该Decommissioning节点上的所有blocks已复制完成,该节点就会进入Decommissioned状态。之后,它就可以安全地从集群中删除而不带来任何数据可用性方面的危害。
3.7跨集群数据拷贝
在处理大规模数据集时,将数据拷入或拷出HDFS集群是很吓人的。HDFS为大规模的集群内/集群间拷贝提供一个叫做DistCp的工具。它是一个MapReduce job;每个Map task会将元数据的一部分拷贝到目标文件系统。MapReduce框架会自动地处理并行task的调度,错误检测和恢复。
4.Practice At Yahoo!
Yahoo!的大规模集群包含大概3500个节点。一个典型的集群节点配置如下:
l 2个4核Xeon处理器@2.5g赫兹
l Red Hat Enterprise Linux服务器Release 5.1
l Sun Jave JDK 1.6.0_13-b03
l 4个SATA磁盘驱动器(每个1TB)
l 16G RAM
l gigabit Ethernet
70%的磁盘空间会被分配给HDFS。剩余的会预留给操作系统,日志,以及map tasks产生的中间输出(MapReduce中间文件没有存储在HDFS上)。单个机柜内的40个节点共享一个IP交换机。机柜上的交换机又会被连接到8个核心交换机中的某一个。核心交换机提供了机柜间的以及到外部集群的连通性。对于每个集群来说,NameNode和BackupNode会被特别安置在具有64GB RAM的机器上;应用程序tasks不会被调度到它们所在的机器上。总共算起来,一个3500个节点的集群具有9.8PB的可用存储空间,因为blocks被存了三份,因此对于应用程序来说只有3.3PB的实际存储。大概算下来,1000个节点代表了1PB的存储。在HDFS投入使用的这些年里(以及未来的日子里),组成集群节点的主机性能伴随着技术的改进也在不断提高。新的集群节点通常具有更高的处理器性能,更大空间的磁盘和内存。慢慢地,那些旧的节点会下线或者用做Hadoop的开发测试集群。关于集群节点的选择很大程度上是计算与存储间的考量。HDFS并没有强制计算与存储之间的比率,或者是对于集群节点的存储空间做出限制。
在一个实际的大规模集群上(3500节点),总共有60 million个文件。这些文件总共有63 million个blocks。因为每个block通常有3个副本,这样每个DataNode大概有54000个block副本。用户应用程序每天会在集群上创建2 million个新文件。在Yahoo!的Hadoop集群中的25000个节点提供了25PB的在线数据存储。在2010年初,Yahoo!的数据处理规模大概在这样一个水平上,当然还在持续增长中。Yahoo!从2004年开始基于分布式文件系统的MapReduce的相关研究。Apache Hadoop项目在2006年成立。在那年年底,Yahoo!已经将Hadoop投入到内部使用,同时有一个用于开发的300个节点的集群。从那时起,HDFS已经成为Yahoo!后台架构的不可或缺的一部分。Web Map(作为搜索引擎关键组件的网页索引)产品一直是针对HDFS的首要应用,它总共运行75个小时,产生500TB的MapReduce中间数据,300TB的最终输出。更多的应用正在迁移到Hadoop,尤其是那些对用户行为进行分析和建模的应用。
Becoming a key component of yahoo’s technology suite meant tackling technical problems that are the difference between being a research project and being the custodian of many petabytes of corporate data。最重要的是数据的健壮性和数据的持久性。当然,性能的经济性,用户间的资源共享及对于系统操作者管理的舒适性也都是很重要的。
4.1数据的持久性
将数据备份三次是为了防止因非关联的节点失效造成数据丢失。通过这种方式,Yahoo!降低了block丢失的概率;对于一个大规模集群来说,在一年的时间内丢失一个block的概率小于0.005。需要注意的是每月的节点失效概率是0.8%。(即使节点最终恢复过来,也不需要再去恢复它曾经持有的数据)。因此,对于我们上面描述的大规模集群来说,每天都会有一两个节点失效。集群大概能在两分钟之内将存放在失效节点上的54000个block副本重新创建出来。(重备份是很快的,因为它是一个可以随集群规模线性扩展的并行问题)。几个节点同时在两分钟内失效的概率是很低的,因此某个block的所有副本都丢失的概率也是很低的。
节点的关联性失效是另一种完全不同的威胁。通常情况下这种失效是因为机柜或者核心交换机的失效造成的。HDFS可以容忍一个机柜交换机的失效(每个block在其他机柜上还会有一份副本)。核心交换机的失效可能会导致集群中的多个机柜的节点无法连通,这种情况下某些blocks可能就是不可用的了。在第二种情况下,需要修复核心交换机来将不可用的副本恢复到集群中。另一种关联性的失效是由集群意外或计划中的电力供应中断引起的。如果某些机柜的电力供应中断,那么某些blocks就可能会变成不可用的。但是恢复电力供应可能也无法解决问题,因为集群中仍可能有一半到1%的节点无法通过加电重启恢复过来。统计学上以及实践表明,一个大规模集群将会在加电重启中丢掉一些节点。
除了节点的完全失效之外,存储数据也可能会损坏或丢失。Block scanner每两星期对一个大规模集群中的blocks进行扫描,通常在这个过程中大概会发现20个左右的坏副本。
4.2Caring for Commons
伴随着HDFS使用的增长,文件系统本身也必须引入一些方式来在一个庞杂的用户群体内共享资源。这样的一个首要feature就是类似于Unix文件目录权限管理模式的权限框架。在该框架内,文件和目录的访问权限分为针对owner,关联到该文件和目录的用户组,及所有其他用户的三种类别。与Unix不同的是,HDFS中文件没有执行权限和粘着位(即t/T特殊权限)。
在现有的权限框架内,用户认证是很弱的;用户身份是由其登陆身份决定的。在访问HDFS的时候,应用程序客户端通过查询操作系统得到用户身份和用户组。一个更强的身份认证模型目前还在开发中。在新的框架中,应用程序客户端必须出示从一个可信任源获取的name system证书。可能会使用不同的证书管理方式,初始实现使用了Kerberos。用户应用程序可以使用同一个框架来确认name system也具有一个可信任的身份。同时name system也可以询问集群中每个DataNode的证书。
总的可用数据存储空间是由DataNodes数和每个DataNode可以提供的存储空间决定的。HDFS的早期经验展示了一种针对不同用户群体之间进行资源分配的需求。不仅要保证资源共享的公平性,还要能够防止一个具有数千个数据写入需求的应用意外地将资源耗尽。对于HDFS来说,因为系统元数据总是存在RAM中,因此名字空间大小(文件和目录树)也是一种有限的资源。为了对存储和名字空间资源进行管理,每个目录可能会被设置一个quota来限制该目录下的存储资源。同时也可以设置另一个quota来对文件和目录数进行限制。
虽然HDFS架构假定大部分的应用程序会以大规模的数据集为输入,但是MapReduce编程框架可能会产生很多小输出文件(每个reduce task产生一个),这会加大对于名字空间资源的占用。为方便起见,一个目录子树可以被合并为一个Hadoop 归档文件。一个HAR文件类似于我们所熟悉的tar,JAR或者Zip文件,但是文件系统操作必须能够识别出归档文件中的内部文件,一个HAR文件应该可以透明地用作一个MapReduce job的输入。
4.3Benchmarks
HDFS的设计目标是为大规模数据集提供高的IO带宽。通常有三种针对该目标的度量方式。
l 通过人为的benchmark观察带宽是怎样的
l 通过在一个具有多个用户job的生产集群里观察带宽是怎样的
l 通过精心构建的大规模用户应用观察带宽是怎样的
这里的统计报告来自于那些至少具有3500个节点的集群。在这个规模上,总带宽与节点数成线性关系,因此单节点的带宽是一个很有意义的统计信息。这些benchmark本身是Hadoop代码的一部分。
DFSIO benchmark用于测量读写及append操作的平均吞吐率。DFSIO作为一个可用的应用程序,目前是Hadoop发布版的一部分。该MapReduce程序会从/向文件中读/写/appends随机数据。Job内的每个map task会在一个不同的文件上执行相同的操作,传输相同大小的数据,同时会将它们的传输速率报告给一个reduce task。Reduce task之后会对这些测量信息进行汇总。这项测试在运行时是独占集群的,同时根据集群大小按固定比例来选定map task的数目。它只是设计用来测量数据传输性能的,会排除掉任务调度,启动及reduce task的开销。
l DFSIO Read:66MB/s per node
l DFSIO Write:40MB/s per node
对于一个生产集群来说,读写的字节数将会被报告给一个metrics收集系统。这些值是几个星期的平均值同时代表着数百个用户的jobs的集群使用情况。平均情况下,每个节点上任意时刻会运行这一两个应用程序tasks(小于可用的处理器核数)。
l Busy Cluster Read:1.02MB/s per node
l Busy Cluster Write:1.09MB/s per node
表2.针对1TB和1PB数据的Sort benchmark。每条数据记录有100字节,其中key有10字节。测试程序是一个通用的排序过程而并未针对记录大小进行特殊处理。在1TB数据排序中,block副本数设成了1,对于一个持续时间比较短的测试来说这是一个合理的设置。在1PB数据排序中,block副本数为2,这样测试程序就可以在即使有节点失效的情况也可以顺利完成。
在2009初,Yahoo!参与了Gray Sort比赛,并拿下了冠军。该task本身对系统将数据移入移出文件系统的能力要求很高(实际上它的关键并不在于排序)。最后一列的I/O rate包含了对HDFS的读入及写出。在第二行里,虽然HDFS的rate有所下降,但是单节点的I/O却大概增加了一倍,这是因为对于更大规模(petabyte!)的数据集来说,MapReduce的中间结果也必须对磁盘进行写入和读取。在小规模的测试里,是不会将MapReduce的中间数据溢写(spill)到磁盘的;它们被直接缓存到了task的内存中。
大规模集群需要HDFS NameNode能够支持与集群规模相对应的大量的client操作。NNThroughput benchmark是一个单机进程,它会启动NameNode应用程序,同时在同一个节点上运行大量的客户端线程。每个客户端线程会通过直接调用NameNode接口来执行同一个NameNode操作。该benchmark是用来测量NameNode每秒可执行的操作数。为避免由RPC连接和序列化引起的开销,该benchmark是运行在本地而不是远程的节点上。通过该测试得到了纯NameNode的性能上界:
5.工作展望
本节提出一些Yahoo的Hadoop团队正在考虑中的一些未来的工作计划;Hadoop作为一个开源项目,意味着很多新的features和变更需要由Hadoop开发者社区来决定。
当NameNode down掉的时候Hadoop集群实际上就会变成不可用的。由于Hadoop主要是作为一种批处理系统使用,重启NameNode也是一种可以接受的恢复方式。但是,我们已经开始向着自动化的故障恢复(failover)而迈进。当前情况下,BackupNode会接受来自primary NameNode的所有事务。如果我们同时将block reports发送给primary NameNode和BackupNode,这就允许一个故障恢复成为warm的或者hot的{!与冷启动,热启动中的含义相同,当然也有处于二者之间的暖}。我们的目标是使用Zookeeper来构建一个自动化的故障恢复解决方案。
NameNode的可扩展性已成为一个首要的需要解决的问题。因为NameNode将名字空间和block位置信息全部保存在内存中,NameNode的堆空间大小已经限制了文件数及可寻址的blocks数。当NameNode的内存使用率接近极限时,NameNode就会变成无响应的有时甚至需要进行重启,这已经成为NameNode面临的主要挑战。虽然我们鼓励用户创建更大的文件,但是有时这也是不可行的,因为这需要对应用程序进行比较大的变更。现在我们已经为HDFS的使用管理提供了quota,同时也提供了一个归档工具。然而,这些都没有从根本上解决可扩展性问题。
我们针对可扩展性的近期解决方案是允许使用多个名字空间(及多个NameNodes)来共享集群内的物理资源。我们正在扩展我们的block IDs,使它可以以一个block pool标识符为前缀。Block pools类似于一个SAN(StorgeAreaNetwork)存储系统中的LUNs(LogicUnitNumber),而具有多个blocks pool的名字空间类似于一个文件系统卷(volume)。
这种策略很简单同时对系统的修改也是最小化的。除可扩展性之外,它还提供了其他一些优点:可以将不同的应用程序集隔离在不同的名字空间下,同时可以提高集群的整体可用性。可以将块存储服务进行通用化,这样就允许其他的一些具有不同名字空间结构的服务来使用这个块存储服务。我们也计划探索一些其他的方式来进行扩展,比如只将部分名字空间存在内存中,在未来提供一个NameNode的真正的分布式实现。此外,我们关于应用程序只会创建少数大文件的假设也是有问题的。如前所述,改变应用程序行为是很难的。此外,针对HDFS的新一类应用程序可能需要存储大量的小文件。
多个独立名字空间的主要缺点是带来的管理开销,尤其是在名字空间数很大的情况下。我们也计划使用以应用程序或者是job为中心的namespaces而不是以集群为中心的—这类似于80年代晚期和90年代早期用于处理分布式系统中远程执行的进程级(per-process)名字空间。
目前我们的集群少于4000节点。我们相信通过上面的解决方案,可以将它扩展到更大的规模。然而,我们认为使用多个小集群与使用单个的大集群(比如3个6000节点集群与一个18000节点集群)相比要更明智一些,因为这样具有更好的可用性和隔离性。最后,我们也计划提供更多的集群间协作支持。比如,为跨越多个集群的文件集合缓存被访问的远程文件或者是降低blocks副本数。
6.经验
{!这一部分来自于:http://www.aosabook.org/en/hdfs.html}
一个非常小的团队构建了Hadoop文件系统,同时使得它稳定健壮的运行在产品系统中。这种成功大部分归因于简单的设计:replicated blocks, 周期性地 block reports以及中央化的元数据服务器。避免了完全的POSIX语义也提供了一定的帮助。尽管将整个元数据保存到内存中限制了可扩展性,这也使得NameNode非常简单:避免的典型文件系统中那种复杂的锁机制。Hadoop成功的另一个原因是在Yahoo!的快速地产品化,这就使得它可以不断地快速地改进。文件系统十分健壮,NameNode很少出错;事实上大部分的停机时间都是由升级造成的。只是最近才引入了自动化的故障恢复机制。
很多人可能会很吃惊,在构建这样一个大型系统中选择了Java语言。尽管由于Java的对象内存和垃圾回收开销给NameNode的扩展造成了一些挑战,但是Java也带来了系统的健壮性;避免了由指针和内存管理bugs造成的危害。
7.致谢
我们需要感谢现在的和过去的所有的Yahoo! HDFS团队成员,感谢他们在构建该文件系统中所做的努力。我们也需要感谢所有的Hadoop committers和collaborators(合作者),感谢他们的宝贵共享。Corinne Chandel绘制了论文中的插图。