namenode存储的数据:
主控服务器主要有三类数据:文件系统的目录结构数据,各个文件的分块信息,数据块的位置信息(就数据块放置在哪些数据服务器上...)。在GFS和HDFS的架构中,只有文件的目录结构和分块信息才会被持久化到本地磁盘上,而数据块的位置信息则是通过动态汇总过来的,仅仅存活在内存数据结构中,机器挂了,就灰飞烟灭了。每一个数据服务器启动后,都会向主控服务器发送注册消息,将其上数据块的状况都告知于主控服务器。俗话说,简单就是美,根据DRY原则,保存的冗余信息越少,出现不一致的可能性越低,付出一点点时间的代价,换取了一大把逻辑上的简单性,绝对应该是一个包赚不赔的买卖。。。
Hdfs支持的写操作
在HDFS中仅仅支持追加写,这大大降低了复杂性.
数据读写操作
有的操作本质上还是涉及到了数据服务器,比如文件创建和删除操作。但是,之前提到,主控服务器只于数据服务器是一个等待拉取的地位,它们不会主动联系数据服务器,将指令传输给它们,而是放到相应的数据结构中,等待数据服务器来取。这样的设计,可以减少通信的次数,加快操作的执行速度。。。
在Hadoop中,读写操作是不走RPC机制的,而是另立门户,独立搭了一套通信框架。在数据服务器一端,DataNode类中有一个DataXceiverServer类的实例,它在一个单独的线程等待请求,一旦接到,就启动一个DataXceiver的线程,处理此次请求。一个请求一个线程,对于数据服务器来说,逻辑上很简单。
读操作
文件的读取实在是一个简单的过程。在客户端DFSClient中,有一个
DFSClient.DFSInputStream类。当需要读取一个文件的时候,会生成一个DFSInputStream的实例。它会先调用ClientProtocol定义getBlockLocations接口,提供给NameNode文件路径、读取位置、读取长度信息,从中取得一个LocatedBlocks类的对象,这个对象包含一组LocatedBlock,那里面有所规定位置中包含的所有数据块信息,以及数据块对应的所有数据服务器的位置信息。当读取开始后,DFSInputStream会先尝试从某个数据块对应的一组数据服务器中选出一个,进行连接。这个选取算法,在当下的实现中,非常简单,就是选出第一个未挂的数据服务器,并没有加入客户端与数据服务器相对位置的考量。读取的请求,发送到数据服务器后,自然会有DataXceiver来处理,数据被一个包一个包发送回客户端,等到整个数据块的数据都被读取完了,就会断开此链接,尝试连接下一个数据块对应的数据服务器,整个流程,依次如此反复,直到所有想读的都读取完了为止。。。
写入的时序控制
在HDFS中,并发写入的次序控制,是由主控服务器来把握的。当创建、续写一个文件的时候,该文件的节点类,由INodeFile升级成为INodeFileUnderConstruction,INodeFileUnderConstruction是INodeFile的子类,它起到一个锁的作用。如果当一个客户端想创建或续写的文件是INodeFileUnderConstruction,会引发异常,因为这说明这个此处有爷,请另寻高就,从而保持了并发写入的次序性。同时,INodeFileUnderConstruction有包含了此时正在操作它的客户端的信息以及最后一个数据块的数据服务器信息,当追加写的时候可以更快速的响应。。。
文件写入的流水线
文件写入,就是在这一组数据服务器上构造成数据流的双向流水线。DFSOutputStream,会与序列的第一个数据服务器建立Socket连接,发送请求头,然后等待回应。DataNode同样是建立DataXceiver来处理写消息,DataXceiver会依照包中传过来的其他服务器的信息,建立与下一个服务器的连接,并生成类似的头,发送给它,并等待回包。此流程依次延续,直到最后一级,它发送回包,反向着逐级传递,再次回到客户端。如果一切顺利,那么此时,流水线建立成功,开始正式发送数据。数据是分成一个个数据包发送的,所有写入的内容,被缓存在客户端,当写满64K,会被封装成DFSOutputStream.Packet类实例,放入DFSOutputStream的dataQueue队列。DFSOutputStream.DataStreamer会时刻监听这个队列,一旦不为空,则开始发送,将位于dataQueue队首的包移动到ackQueue队列的队尾,表示已发送但尚未接受回复的包队列。同时启动ResponseProcessor线程监听回包,直到收到相应回包,才将发送包从ackQueue中移除,表示成功。每一个数据服务器的DataXceiver收到了数据包,一边写入到本地文件中去,一边转发给下一级的数据服务器,等待回包,同前面建立流水线的流程。。。
客户端错误回复
作为客户端,大部分时候,牺牲了就牺牲了,没人哀悼,无人同情,只有在在辛勤写入的时候,不幸辞世(机器挂了,或者网络断了,诸如此类...),才会引起些恐慌。因为,此时此刻,在主控服务器上对应的文件,正作为INodeFileUnderConstruction活着,仅仅为占有它的那个客户端服务者,做为一个专一的文件,它不允许别的客户端染指。这样的话,一旦占有它的客户端服务者牺牲了,此客户端会依然占着茅坑不拉屎,让如花似玉INodeFileUnderConstruction孤孤单单守寡终身。这种事情当然无法容忍,因此,必须有办法解决这个问题,办法就是:租约。。。、、
租约,顾名思义,就是当客户端需要占用某文件的时候,与主控服务器签订的一个短期合同。这个合同有一个期限,在这个期限内,客户端可以延长合同期限,一旦超过期限,主控服务器会强行终止此租约,将这个文件的享用权,分配给他人。。。
在打开或创建一个文件,准备追加写之前,会调用LeaseManager的addLease方法,在指定的路径下与此客户端签订一份租约。客户端会启动DFSClient.LeaseChecker线程,定时轮询调用ClientProtocol的renewLease方法,续签租约。在主控服务器一端,有一个LeaseManager.Monitor线程,始终在轮询检查所有租约,查看是否有到期未续的租约。如果一切正常,该客户端完成写操作,会关闭文件,停止租约,一旦有所意外,比如文件被删除了,客户端牺牲了,主控服务器都会剥夺此租约,如此,来避免由于客户端停机带来的资源被长期霸占的问题。。。
datanode心跳机制
为了整个系统的稳定,数据服务器必须时刻向主控服务器汇报,保持主控服务器对其的完全了解,这个机制,就是心跳消息。在HDFS中,主控服务器NameNode实现了DatanodeProtocol接口,数据服务器DataNode会在主循环中,不停的调用该协议中的sendHeartbeat方法,向NameNode汇报状况。在此调用中,DataNode会将其整体运行状况告知NameNode,比如:有多少可用空间、用了多大的空间,等等之类。NameNode会记住此DataNode的运行状况,作为新的数据块分配或是负载均衡的依据。当NameNode处理完成此消息后,会将相关的指令封装成一个DatanodeCommand对象,交还给DataNode,告诉数据服务器什么数据块要删除什么数据块要新增等等之类,数据服务器以此为自己的行动依据。。。
但是,sendHeartbeat并没有提供本地的数据块信息给NameNode,那么主控服务器就无法知道此数据服务器应该分配什么数据块应该删除什么数据块,那么它是如何决定的呢?答案就是DatanodeProtocol定义的另一个方法,blockReport。DataNode也是在主循环中定时调用此方法,只是,其周期通常比调用sendHeartbeat的更长。它会提交本地的所有数据块状况给NameNode,NameNode会和本地保存的数据块信息比较,决定什么该删除什么该新增,并将相关结果缓存在本地对应的数据结构中,等待此服务器再发送sendHeartbeat消息过来的时候,依照这些数据结构中的内容,做出相应的DatanodeCommand指令。
namenode容灾
在HDFS中,运行着一个SecondaryNameNode服务器,它做为主控服务器的替补,隐忍厚积薄发为篡位做好准备,其中,核心内容就是:定期下载并处理日志和镜像。SecondaryNameNode看上去像客户端一样,与NameNode之间,走着NamenodeProtocol协议。它会不停的查看主控服务器上面累计日志的大小,当达到阈值后,调用doCheckpoint函数,此函数的主要步骤包括:
- 首先是调用startCheckpoint做一些本地的初始化工作;
- 然后调用rollEditLog,将NameNode上此时操作的日志文件从edit切到edit.new上来,这个操作瞬间完成,上层写日志的函数完全感觉不到差别;
- 接着,调用downloadCheckpointFiles,将主控服务器上的镜像文件和日志文件都下载到此候补主控服务器上来;
- 并调用doMerge,打开镜像和日志,将日志生成新的镜像,保存覆盖;
- 下一步,调用putFSImage把新的镜像上传回NameNode;
- 再调用rollFsImage,将镜像换成新的,在日志从edit.new改名为edit;
- 最后,调用endCheckpoint做收尾工作。
整个算法涉及到NameNode和SecondaryNameNode两个服务器,最终结果是NameNode和SecondaryNameNode都依照算法进行前的日志生成了镜像。而两个服务器上日志文件的内容,前者是整个算法进行期间所写的日志,后者始终不会有任何日志。当主控服务器牺牲的时候,运行SecondaryNameNode的服务器立刻被扶正,在其上启动主控服务,利用其日志和镜像,恢复文件目录,并逐步接受各数据服务器的注册,最终向外提供稳定的文件服务。。。
数据的正确性保证(不太理解,后续再拜读)
在复杂纷繁的分布式环境中,我们坚定的相信,万事皆有可能。哪怕各个服务器都舒舒服服的活着,也可能有各种各样的情况导致网络传输中的数据丢失或者错误。并且在分布式文件系统中,同一份文件的数据,是存在大量冗余备份的,系统必须要维护所有的数据块内容完全同步,否则,一人一言,不同客户端读同一个文件读出不同数据,用户非得疯了不可。。。
在HDFS中,为了保证数据的正确性和同一份数据的一致性,做了大量的工作。首先,每一个数据块,都有一个版本标识,在Block类中,用一个长整型的数generationStamp来表示版本信息(Block类是所有表示数据块的数据结构的基类),一旦数据块上的数据有所变化,此版本号将向前增加。在主控服务器上,保存有此时每个数据块的版本,一旦出现数据服务器上相关数据块版本与其不一致,将会触发相关的恢复流程。这样的机制保证了各个数据服务器器上的数据块,在基本大方向上都是一致的。但是,由于网络的复杂性,简单的版本信息无法保证具体内容的一致性(因为此版本信息与内容无关,可能会出现版本相同,但内容不同的状况)。因此,为了保证数据内容上的一致,必须要依照内容,作出签名。。。
当客户端向数据服务器追加写入数据包时,每一个数据包的数据,都会切分成512字节大小的段,作为签名验证的基本单位,在HDFS中,把这个数据段称为Chunk,即传输块(注意,在GFS中,Chunk表达的是数据块...)。在每一个数据包中,都包含若干个传输块以及每一个传输块的签名,当下,这个签名是根据Java
SDK提供的CRC算法算得的,其实就是一个奇偶校验。当数据包传输到流水线的最后一级,数据服务器会对其进行验证(想一想,为什么只在最后一级做验证,而不是每级都做...),一旦发现当前的传输块签名与在客户端中的签名不一致,整个数据包的写入被视为无效,Lease
Recover(租约恢复)算法被触发。。。
从基本原理上看,这个算法很简单,就是取所有数据服务器上此数据块的最小长度当作正确内容的长度,将其他数据服务器上此数据块超出此长度的部分切除。从正确性上看,此算法无疑是正确的,因为至少有一个数据服务器会发现此错误,并拒绝写入,那么,如果写入了的,都是正确的;从效率上看,此算法也是高效的,因为它避免了重复的传输和复杂的验证,仅仅是各自删除尾部的一些内容即可。但从具体实现上来看,此算法稍微有些绕,因为,为了降低本已不堪重负的主控服务器的负担,此算法不是由主控服务器这个大脑发起的,而是通过选举一个数据服务器作为Primary,由Primary发起,通过调用与其他各数据服务器间的InterDatanodeProtocol协议,最终完成的。具体的算法流程,参见LeaseManager类上面的注释。需要说明的是此算法的触发时机和发起者。此算法可以由客户端或者是主控服务器发起,当客户端在写入一个数据包失败后,会发起租约恢复。因为,一次写入失败,不论是何种原因,很有可能就会导致流水线上有的服务器写了,有的没写,从而造成不统一。而主控服务器发起的时机,则是在占有租约的客户端超出一定时限没有续签,这说明客户端可能挂了,在临死前可能干过不利于数据块统一的事情,作为监督者,主控服务器需要发起一场恢复运动,确保一切正确。。。
HDFS要点