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主要完成两部分工作:

  1. 文件create,write,complete操作时,创建lease、更新时间戳、回收lease
  2. 一个后台线程定期检查是否有过期的lease

LeaseManager的代码结构如下

其中Lease表示一个租约,包括一个client(holder)所拥有的所有文件锁(paths)。

Monitor是检查是否有过期租约的线程。

LeaseManager中有几个主要数据结构:

  1. leases(TreeMap<String, Lease>):维护holder -> leased的映射集合
  2. sortedLeases (TreeSet): lease集合
  3. 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过期是回收外,还有:

  1. NameNode收到DataNode的Sync block command时
  2. DFSClient主动关闭一个流时
  3. 创建文件时,如果该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文件系统实际执行的核心,提供各种增删改查文件操作接口。其内部维护多个数据结构之间的关系:

  1. fsname->block列表的映射
  2. 所有有效blocks集合
  3. block与其所属的datanodes之间的映射(该映射是通过block reports动态构建的,维护在namenode的内存中。每个datanode在启动时向namenode报告其自身node上的block)
  4. 每个datanode与其上的blocklist的映射
  5. 采用心跳检测根据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中的基本读写单元,主要包括:

  1. blockId: 一个long类型的块id
  2. numBytes: 块大小
  3. generationStamp: 块更新的时间戳

BlockInfo

BlockInfo扩展自Block,除基本信息外还包括一个inode引用,表示该block所属的文件;以及一个神奇的三元组数组Object[] triplets,用来表示保存该block的datanode信息,假设系统中的备份数量为3。那么这个数组结构如下:

  1. DN1,DN2,DN3分别表示存有改block的三个datanode的引用(DataNodeDescriptor)
  2. DN1-prev-blk表示在DN1上block列表中当前block的前置block引用
  3. 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头部信息
  1. layoutVersion(int):image layout版本号,0.19版本的hdfs中为-18
  2. namespaceId(int): 命名空间ID,系统初始化时生成,在一个namenode生命周期内保持不变,datanode想namenode注册是返回改id作为 registerId,以后每次datanode与namenode通信时都携带该id,不认识的id的请求将被拒绝。
  3. numberItemOfTree(long): 系统中的文件总数
  4. 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

上一篇:《C++并发编程实战》——第1章 你好,C++并发世界


下一篇:OpenGrok安装配置 - MAC篇