首页 > 代码库 > HDFS追本溯源:租约,读写过程的容错处理及NN的主要数据结构
HDFS追本溯源:租约,读写过程的容错处理及NN的主要数据结构
1. Lease 的机制:
hdfs支持write-once-read-many,也就是说不支持并行写,那么对读写的互斥同步就是靠Lease实现的。Lease说白了就是一个有时间约束的锁。客户端写文件时需要先申请一个Lease,对应到namenode中的LeaseManager,客户端的client name就作为一个lease的holder,即租约持有者。LeaseManager维护了文件的path与lease的对应关系,还有clientname->lease的对应关系。LeaseManager中有两个时间限制:softLimitand hardLimit。
软限制就是写文件时规定的租约超时时间,硬限制则是考虑到文件close时未来得及释放lease的情况强制回收租约。
LeaseManager中还有一个Monitor线程来检测Lease是否超过hardLimit。而软租约的超时检测则在DFSClient的LeaseChecker中进行。
当客户端(DFSClient)create一个文件的时候,会通过RPC 调用 namenode 的createFile方法来创建文件。进而又调用FSNameSystem的startFile方法,又调用 LeaseManager 的addLease方法为新创建的文件添加一个lease。如果lease已存在,则更新该lease的lastUpdate (最近更新时间)值,并将该文件的path对应该lease上。之后DFSClient 将该文件的path 添加 LeaseChecker中。文件创建成功后,守护线程LeaseChecker会每隔一定时间间隔renew该DFSClient所拥有的lease。
LeaseManagement是HDFS中的一个同步机制,用于保证同一时刻只有一个client对一个文件进行写或创建操作。如当 新建一个文件f时,client向NameNode发起一个create请求,那么leaseManager会想该client分配一个f文件的 lease。client凭借该lease完成文件的创建操作。此时其他client无法获得f的当client长时间(默认为超过1min)不进行操作 时,发放的lease将被收回。
LeaseManager主要完成两部分工作:
- 文件create,write,complete操作时,创建lease、更新时间戳、回收lease
- 一个后台线程定期检查是否有过期的lease
LeaseManager的代码结构如下
其中Lease表示一个租约,包括一个client(holder)所拥有的所有文件锁(paths)。
Monitor是检查是否有过期租约的线程。
LeaseManager中有几个主要数据结构:
- leases(TreeMap<String, Lease>):维护holder -> leased的映射集合
- sortedLeases (TreeSet): lease集合
- sortedLeaseByPath(TreeMap<String, Lease>): 维护paths->lease的映射集合
一、创建lease
当client向NameNode发起create操作时,NameNode.create()调用FSNameSystem.startFile()->FSNameSystem.startFileInternal(),该方法最终会调用 leaseManager.addLease(cons.clientName, src)来创建lease。
LeaseRecovery ——租约回收
leaserecovery时机
lease发放之后,在不用时会被回收,回收的产经除上述Monitor线程检测lease过期是回收外,还有:
- NameNode收到DataNode的Sync block command时
- DFSClient主动关闭一个流时
- 创建文件时,如果该DFSClient的lease超过soft limit时
lease recovery 算法
1) NameNode查找lease信息
2) 对于lease中的每个文件f,令b为f的最后一个block,作如下操作:
2.1) 获取b所在的datanode列表
2.2) 令其中一个datanode作为primarydatanode p
2.3) p 从NameNode获取最新的时间戳
2.4) p 从每个DataNode获取block信息
2.5) p 计算最小的block长度
2.6) p 用最小的block长度和最新的时间戳来更新具有有效时间戳的datanode
2.7) p 通知NameNode更新结果
2.8) NameNode更新BlockInfo
2.9) NameNode从lease中删除f,如果此时该lease中所有文件都已被删除,将删除该lease
2.10) Name提交修改的EditLog
Client续约 —— DFSClient.LeaseChecker
在NameNode上的LeaseManager.Monitor线程负责检查过期的lease,那么client为了防止尚在使用的lease过期,需要定期想NameNode发起续约请求。该任务有DFSClient中的LeaseChecker完成。
LeaseChecker结构如下:
其中pendingCreates是一个TreeMap<String, OutputStream>用来维护src->当前正在写入的文件的DFSOutputStream的映射。
其核心是周期性(每个1s)调用run()方法来对租约过半的lease进行续约
NameNode接收到renewLease请求后,调用FSNameSystem.renewLease()并最终调用LeaseManager.renewLease()完成续约。
2. 机架感知
HDFS机架感知
通常,大型 Hadoop 集群是以机架的形式来组织的,同一个机架上不同 节点间的网络状况比不同机架之间的更为理想。 另外, NameNode 设法将 数据块副本保存在不同的机架上以提高容错性。
而 HDFS 不能够自动判断集群中各个 datanode 的网络拓扑情况 Hadoop允 许集群的管理员通过配置 dfs.network.script 参数来确定节点所处的机架。 文件提供了 IP->rackid 的翻译。 NameNode 通过这个得到集群中各个 datanode 机器的 rackid 。 如果 topology.script.file.name 没有设定,则每个 IP 都会翻译 成/ default-rack 。
有了机架感知, NameNode 就可以画出上图所示的 datanode 网络拓扑图。D1,R1 都是交换机,最底层是 datanode 。 则 H1 的 rackid=/D1/R1/H1 , H1的 parent 是 R1 , R1 的是 D1 。 这些 rackid 信息可以通过topology.script.file.name 配置。有了这些 rackid 信息就可以计算出任意两台datanode 之间的距离。
distance(/D1/R1/H1,/D1/R1/H1)=0 相同的 datanode
distance(/D1/R1/H1,/D1/R1/H2)=2 同一 rack 下的不同 datanode
distance(/D1/R1/H1,/D1/R1/H4)=4 同一 IDC 下的不同 datanode
distance(/D1/R1/H1,/D2/R3/H7)=6 不同 IDC 下的 datanode
3. HDFS 文件删除恢复机制
当用户或应用程序删除某个文件时,这个文件并没有立刻从 HDFS 中删除。实际上, HDFS 会将这个文件重命名转移到 /trash 目录。只要文件还在/trash 目录中,该文件就可以被迅速地恢复。文件在 /trash 中保存的时间是可 配置的,当超过这个时间时, Namenode 就会将该文件从名字空间中删除。 删除文件会使得该文件相关的数据块被释放。注意,从用户删除文件到 HDFS 空闲空间的增加之间会有一定时间的延迟。
只要被删除的文件还在 /trash 目录中,用户就可以恢复这个文件。如果 用户想恢复被删除的文件,他 / 她可以浏览 /trash 目录找回该文件。 /trash 目 录仅仅保存被删除文件的最后副本。 /trash 目录与其他的目录没有什么区别 ,除了一点:在该目录上 HDFS 会应用一个特殊策略来自动删除文件。目前 的默认策略是删除 /trash 中保留时间超过 6 小时的文件。将来,这个策略可以 通过一个被良好定义的接口配置。
开启回收站
Hdfs -site.xml
<configuration>
<property>
<name>fs.trash.interval</name>
<value> 1440 </value>
<description>Number ofminutes betweentrash checkpoints.
If zero,the trashfeature is disabled.
</description>
</property>
</configuration>
1, fs.trash.interval 参数设置保留时间为 1440 秒 (1 天 )
2, 回收站的位置:在 HDFS 上的 / user/$USER/.Trash/Current/
4. 数据完整性
从某个 Datanode 获取的数据块有可能是损坏的,损坏可能是由Datanode 的存储设备错误、网络错误或者软件 bug 造成的。 HDFS 客户端软 件实现了对 HDFS 文件内容的校验和 (checksum) 检查。当客户端创建一个新 的HDFS 文件,会计算这个文件每个数据块的校验和,并将校验和作为一个 单独的隐藏文件保存在同一个 HDFS 名字空间下。当客户端获取文件内容后 ,它会检验从Datanode 获取的数据跟相应的校验和文件中的校验和是否匹 配,如果不匹配,客户端可以选择从其他 Datanode 获取该数据块的副本。
5. 修改副本数
1.集群只有三个Datanode,hadoop系统replication=4时,会出现什么情况?
对于上传文件到hdfs上时,当时hadoop的副本系数是几,这个文件的块数副本数就会有几份,无论以后你怎么更改系统副本系统,这个文件的副本数都不 会改变,也就说上传到分布式系统上的文件副本数由当时的系统副本数决定,不会受replication的更改而变化,除非用命令来更改文件的副本数。因为 dfs.replication实质上是client参数,在create文件时可以指定具体replication,属性dfs.replication是不指定具体replication时的采用默认备份数。文件上传后,备份数已定,修改dfs.replication是 不会影响以前的文件的,也不会影响后面指定备份数的文件。只影响后面采用默认备份数的文件。但可以利用hadoop提供的命令后期改某文件的备份 数:hadoop fs-setrep -R 1。如果你是在hdfs-site.xml设置了dfs.replication,这并一定就得了,因为你可能没把conf文件夹加入到你的project的classpath里,你的程序运行时取的dfs.replication可能是hdfs-default.xml里的dfs.replication,默认是3。可能这个就是造成你为什么dfs.replication老是3的原因。你可以试试在创建文件时,显式设定 replication。replication一般到3就可以了,大了意义也不大。
6. HDFS的安全模式
Namenode 启动后会进入一个称为安全模式的特殊状态。处于安全模式 的Namenode 是不会进行数据块的复制的。 Namenode 从所有的 Datanode 接收心跳信号和块状态报告。块状态报告包括了某个 Datanode 所有的数据 块列表。每个数据块都有一个指定的最小副本数。当 Namenode 检测确认某 个数据块的副本数目达到这个最小值,那么该数据块就会被认为是副本安全 (safely replicated) 的;在一定百分比(这个参数可配置)的数据块被 Namenode 检测确认是安全之后(加上一个额外的 30 秒等待时间), Namenode 将退出安全模式状态。接下来它会确定还有哪些数据块的副本没 有达到指定数目,并将这些数据块复制到其他 Datanode上。
7. 读过程分析
?使用HDFS提供的客户端开发库Client,向远程的Namenode发起RPC请求;
? Namenode会视情况返回文件的部分或者全部block列表,对于每个block,Namenode都会返回有该block拷贝的DataNode地址;
?客户端开发库Client会选取离客户端最接近的DataNode来读取block;如果客户端本身就是DataNode,那么将从本地直接获取数据.
?读取完当前block的数据后,关闭与当前的DataNode连接,并为读取下一个block寻找最佳的DataNode;
?当读完列表的block后,且文件读取还没有结束,客户端开发库会继续向Namenode获取下一批的block列表。
?读取完一个block都会进行checksum验证,如果读取datanode时出现错误,客户端会通知Namenode,然后再从下一个拥有该block拷贝的datanode继续读。
8. 写过程流程分析
?使用HDFS提供的客户端开发库Client,向远程的Namenode发起RPC请求;
?Namenode会检查要创建的文件是否已经存在,创建者是否有权限进行操作,成功则会为文件 创建一个记录,否则会让客户端抛出异常;
?当客户端开始写入文件的时候,会将文件切分成多个packets,并在内部以数据队列"data queue"的形式管理这些packets,并向Namenode申请新的blocks,获取用来存储replicas的合适的datanodes列表, 列表的大小根据在Namenode中对replication的设置而定。
?开始以pipeline(管道)的形式将packet写入所有的replicas中。把packet以流的方式写入第一个datanode, 该datanode把该packet存储之后,再将其传递给在此pipeline中的下一个datanode,直到最后一个datanode,这种写数据 的方式呈流水线的形式。
?最后一个datanode成功存储之后会返回一个ack packet,在pipeline里传递至客户端,在客户端的开发库内部维护着"ack queue",成功收到datanode返回的ackpacket后会从"ackqueue"移除相应的packet。
?如果传输过程中,有某个datanode出现了故障,那么当前的pipeline会被关闭,出现故障的datanode会从当前的pipeline中移除,剩余的block会继续剩下的datanode中继续以pipeline的形式传输,同时Namenode会分配一个新的datanode,保持replicas设定的数量。
流水线复制
当客户端向 HDFS 文件写入数据的时候,一开始是写到本地临时文件中。假设该文件的副 本系数设置为 3 ,当本地临时文件累积到一个数据块的大小时,客户端会从 Namenode 获取一个 Datanode 列表用于存放副本。然后客户端开始向第一个 Datanode 传输数据,第一个 Datanode 一小部分一小部分 (4 KB) 地接收数据,将每一部分写入本地仓库,并同时传输该部分到列表中 第二个 Datanode节点。第二个 Datanode 也是这样,一小部分一小部分地接收数据,写入本地 仓库,并同时传给第三个 Datanode 。最后,第三个 Datanode 接收数据并存储在本地。因此, Datanode 能流水线式地从前一个节点接收数据,并在同时转发给下一个节点,数据以流水线的 方式从前一个 Datanode 复制到下一个
更细节的原理
客户端创建文件的请求其实并没有立即发送给 Namenode ,事实上,在刚开始阶 段 HDFS 客户端会先将文件数据缓存到本地的一个临时文件。应用程序的写操作被透 明地重定向到这个临时文件。当这个临时文件累积的数据量超过一个数据块的大小 ,客户端才会联系 Namenode 。 Namenode 将文件名插入文件系统的层次结构中,并 且分配一个数据块给它。然后返回 Datanode 的标识符和目标数据块给客户端。接着 客户端将这块数据从本地临时文件上传到指定的 Datanode 上。当文件关闭时,在临 时文件中剩余的没有上传的数据也会传输到指定的 Datanode 上。然后客户端告诉 Namenode 文件已经关闭。此时 Namenode 才将文件创建操作提交到日志里进行存储 。如果 Namenode 在文件关闭前宕机了,则该文件将丢失。
整个写流程如下:
第一步,客户端调用DistributedFileSystem的create()方法,开始创建新文件:DistributedFileSystem创建DFSOutputStream,产生一个RPC调用,让NameNode在文件系统的命名空间中创建这一新文件;
第二步,NameNode接收到用户的写文件的RPC请 求后,谁偶先要执行各种检查,如客户是否有相关的创佳权限和该文件是否已存在等,检查都通过后才会创建一个新文件,并将操作记录到编辑日志,然后DistributedFileSystem会将DFSOutputStream对象包装在FSDataOutStream实例中,返回客户端;否则文件 创建失败并且给客户端抛IOException。
第三步,客户端开始写文 件:DFSOutputStream会将文件分割成packets数据包,然后将这些packets写到其内部的一个叫做dataqueue(数据队列)。dataqueue会向NameNode节点请求适合存储数据副本的DataNode节点的列表,然后这些DataNode之前生成一个Pipeline数据流管 道,我们假设副本集参数被设置为3,那么这个数据流管道中就有三个DataNode节点。
第四步,首先DFSOutputStream会将packets向Pipeline数据流管道中的第一个DataNode节点写数据,第一个DataNode接收packets然后把packets写向Pipeline中的第二个节点,同理,第二个节点保存接收到的数据然后将数据写向Pipeline中的第三个DataNode节点。
第五步,DFSOutputStream内部同样维护另 外一个内部的写数据确认队列——ackqueue。当Pipeline中的第三个DataNode节点将packets成功保存后,该节点回向第二个DataNode返回一个确认数据写成功的 信息,第二个DataNode接收到该确认信息后在当前节点数据写成功后也会向Pipeline中第一个DataNode节点发送一个确认数据写成功的信 息,然后第一个节点在收到该信息后如果该节点的数据也写成功后,会将packets从ackqueue中将数据删除。
在写数据的过程中,如果Pipeline数据流管道中的一个DataNode节点写失败了会发生什问题、需要做哪些内部处理呢?如果这种情况发生,那么就会执行一些操作:
首先,Pipeline数据流管道会被关闭,ack queue中的packets会被添加到dataqueue的前面以确保不会发生packets数据包的丢失;
接着,在正常的DataNode节点上的以保存好的block的ID版本会升级——这样发生故障的DataNode节点上的block数据会在节点恢复正常后被删除,失效节点也会被从Pipeline中删除;
最后,剩下的数据会被写入到Pipeline数据流管道中的其他两个节点中。
如果Pipeline中的多个节点在写数据是发生失败,那么只要写成功的block的数量达到dfs.replication.min(默认为1),那么就任务是写成功的,然后NameNode后通过一步的方式将block复制到其他节点,最后事数据副本达到dfs.replication参数配置的个数。
第六步,,完成写操作后,客户端调用close()关闭写操作,刷新数据;
第七步,,在数据刷新完后NameNode后关闭写操作流。到此,整个写操作完成。
least recently used
9. HDFS负载均衡
HDFS的数据也许并不是非常均匀的分布在各个DataNode中。一个常见的原因是在现有的集群上经常会增添新的DataNode节点。当新增一个 数据块(一个文件的数据被保存在一系列的块中)时,NameNode在选择DataNode接收这个数据块之前,会考虑到很多因素。其中的一些考虑的是:
?将数据块的一个副本放在正在写这个数据块的节点上。
?尽量将数据块的不同副本分布在不同的机架上,这样集群可在完全失去某一机架的情况下还能存活。
?一个副本通常被放置在和写文件的节点同一机架的某个节点上,这样可以减少跨越机架的网络I/O。
?尽量均匀地将HDFS数据分布在集群的DataNode中。
10. 基本数据结构
FSNameSystem
FSNameSystem是HDFS文件系统实际执行的核心,提供各种增删改查文件操作接口。其内部维护多个数据结构之间的关系:
- fsname->block列表的映射
- 所有有效blocks集合
- block与其所属的datanodes之间的映射(该映射是通过block reports动态构建的,维护在namenode的内存中。每个datanode在启动时向namenode报告其自身node上的block)
- 每个datanode与其上的blocklist的映射
- 采用心跳检测根据LRU算法更新的机器(datanode)列表
FSDirectory
FSDirectory用于维护当前系统中的文件树。
其内部主要组成结构包括一个INodeDirectoryWithQuota作为根目录(rootDir)和一个FSImage来持久化文件树的修改操作。
INode
HDFS中文件树用类似VFS中INode的方式构建,整个HDFS中文件被表示为INodeFile,目录被表示为INodeDirectory。INodeDiretoryWithQuota是INodeDirectory的扩展类,即带配额的文件目录
INodeFile表示INode书中的一个文件,扩展自INode,除了名字(name),父节点(parent)等之外,一个主要元素是blocks,一个BlockInfo数组,表示该文件对应的block信息。
BlocksMap
BlocksMap用于维护Block-> { INode, datanodes, self ref } 的映射 BlocksMap结构比较简单,实际上就是一个Block到BlockInfo的映射。
Block
Block是HDFS中的基本读写单元,主要包括:
- blockId: 一个long类型的块id
- numBytes: 块大小
- generationStamp: 块更新的时间戳
BlockInfo
BlockInfo扩展自Block,除基本信息外还包括一个inode引用,表示该block所属的文件;以及一个神奇的三元组数组Object[] triplets,用来表示保存该block的datanode信息,假设系统中的备份数量为3。那么这个数组结构如下:
- DN1,DN2,DN3分别表示存有改block的三个datanode的引用(DataNodeDescriptor)
- DN1-prev-blk表示在DN1上block列表中当前block的前置block引用
- DN1-next-blk表示在DN1上block列表中当前block的后置block引用
DN2,DN3的prev-blk和next-blk类似。 HDFS采用这种结构存放block->datanodelist的信息主要是为了节省内存空间,block->datanodelist之间的映射关系需要占用大量内存,如果同样还要将datanode->blockslist的信息保存在内存中,同样要占用大量内存。采用三元组这种方式能够从其中一个block获得到改 block所属的datanode上的所有block列表。
FSImage
FSImage用于持久化文件树的变更以及系统启动时加载持久化数据。HDFS启动时通过FSImage来加载磁盘中原有的文件树,系统Standby之后,通过FSEditlog来保存在文件树上的修改,FSEditLog定期将保存的修改信息刷到FSImage中进行持久化存储。FSImage中文件元信息的存储结构如下(参见FImage.saveFSImage()方法)
FSImage头部信息
- layoutVersion(int):image layout版本号,0.19版本的hdfs中为-18
- namespaceId(int): 命名空间ID,系统初始化时生成,在一个namenode生命周期内保持不变,datanode想namenode注册是返回改id作为 registerId,以后每次datanode与namenode通信时都携带该id,不认识的id的请求将被拒绝。
- numberItemOfTree(long): 系统中的文件总数
- generationTimeStamp: 生成image的时间戳
参考资料:
1. http://blog.csdn.net/cklsoft/article/details/8917899
2. http://www.iteye.com/topic/1126509
3. http://jiangbo.me/blog/2012/10/18/hdfs-namenode-lease-management/
4. http://flyingdutchman.iteye.com/blog/1900536