好记性不如烂笔头27-分布式文件系统的事务控制(5)

来源:互联网 发布:java应用程序的入口 编辑:程序博客网 时间:2024/06/05 09:32


如果说事务控制在数据库中是有时候用到,在普通的文件系统中是偶尔要处理。那么在分布式文件系统中,事务控制是每一个数据操作都要关注的内容。当然,有很多的现成的系统给我们提供了很多的资源。

分布式文件系统是一个快速发展的概念,目前还没有形成类似ORACLE在数据库中的地位,因此,关于这块,参考了一些网络上的内容,泛泛而谈。Hadoop分布式文件系统(HDFS)在JAVA环境中使用相对比较多,用自己的半桶水水平进行解读。

部分文字来自网络,出处也找不到。 :(

在分布式环境中,有太多的意外,数据随时传输错误,服务器时刻可能当即,很多平常称为异常的现象,在这里都需要按照平常事来对待。因此,对于分布式文件系统而言,仅仅是满足了正常状况下文件系统各项服务还不够,还需要保证它在各种意外场景下健康持续的服务。

1、分布式文件系统中服务器的错误和恢复

在分布式环境中,服务器宕机是常见的事情。HDFS有三类服务器,每一类服务器出错了,都有相应的应急策略。

客户端

客户端做为分布式文件系统的使用者,在整个系统中,地位也是最低的;只有当它在写入数据的时候出现异常(机器挂了,或者网络断了,系统挂了...),才会对整个系统造成一定的影响。此刻,这个挂掉的客户端已经锁定了这个文件,让这个文件无法被其他人写入或者维护,这个事情的解决办法是:租约。

租约,顾名思义,就是当客户端需要占用某文件的时候,与主控服务器签订的一个短期合同。这个合同有一个期限,在这个期限内,客户端可以延长合同期限,一旦超过期限,主控服务器会强行终止此租约,将这个文件的享用权,分配给他人。。。

在打开或创建一个文件,准备追加写之前,会调用LeaseManager的addLease方法,在指定的路径下与此客户端签订一份租约。客户端会启动DFSClient.LeaseChecker线程,定时轮询调用ClientProtocol的renewLease方法,续签租约。在主控服务器一端,有一个LeaseManager.Monitor线程,始终在轮询检查所有租约,查看是否有到期未续的租约。如果一切正常,该客户端完成写操作,会关闭文件,停止租约,一旦有所意外,比如文件被删除了,客户端牺牲了,主控服务器都会剥夺此租约,如此,来避免由于客户端停机带来的资源被长期霸占的问题。

数据服务器

在分布式文件系统中,海量的数据服务器随时有可能出现异常。一旦某数据服务器宕机,如果主控服务器不知道这个情况,系统就会给客户端提供虚假的服务。

数据服务器必须时刻向主控服务器汇报,保持主控服务器对其的完全了解,这个机制,就是心跳消息。在HDFS中,主控服务器NameNode实现了DatanodeProtocol接口,数据服务器DataNode会在主循环中,不停的调用该协议中的sendHeartbeat方法,向NameNode汇报状况。在此调用中,DataNode会将其整体运行状况告知NameNode,比如:有多少可用空间、用了多大的空间,等等之类。NameNode会记住此DataNode的运行状况,作为新的数据块分配或是负载均衡的依据。当NameNode处理完成此消息后,会将相关的指令封装成一个DatanodeCommand对象,交还给DataNode,告诉数据服务器什么数据块要删除什么数据块要新增等等之类,数据服务器以此为自己的行动依据。

但是,sendHeartbeat并没有提供本地的数据块信息给NameNode,那么主控服务器就无法知道此数据服务器应该分配什么数据块应该删除什么数据块,那么它是如何决定的呢?答案就是DatanodeProtocol定义的另一个方法,blockReport。DataNode也是在主循环中定时调用此方法,只是,其周期通常比调用sendHeartbeat的更长。它会提交本地的所有数据块状况给NameNode,NameNode会和本地保存的数据块信息比较,决定什么该删除什么该新增,并将相关结果缓存在本地对应的数据结构中,等待此服务器再发送sendHeartbeat消息过来的时候,依照这些数据结构中的内容,做出相应的DatanodeCommand指令。blockReport方法同样也会返回一个DatanodeCommand给DataNode,但通常,只是为空(只有出错的时候不为空),我想,增加缓存,也许是为了确保每个指令都可以重复发送并确定被执行。。。

主控服务器

主控服务器作为整个系统的核心和单点,一旦主控服务器瘫痪,整个分布式文件服务集群将彻底瘫痪罢工。在主控服务器当即后,通过日志进行提拔新的主控服务器并迅速使其进入工作角色

使用日志来进行系统恢复和回滚,是分布式文件系统进行事物控制的重要环节。

在主控服务器上,所有对文件目录操作的关键步骤(具体文件内容所处的数据服务器,是不会被写入日志的,因为这些内容是动态建立的...),都会被写入日志。另外,主控服务器会在某些时刻,将当下的文件目录完整的序列化到本地,这称为镜像。一旦存有镜像,镜像前期所写的日志和其他镜像,都纯属冗余,其历史使命已经完成,可以报废删除了。在主控服务器不幸牺牲,或者是战略性的停机修整结束,并重新启动后,主控服务器会根据最近的镜像 + 镜像之后的所有日志,重建整个文件目录,迅速将服务能力恢复到牺牲前的水准。

对于数据服务器而言,它们会通过一些手段,迅速得主控服务器的更迭消息。它们会立刻向新的主控服务器其发送心跳消息。

在HDFS的实现中,FSEditLog类是整个日志体系的核心,提供了一大堆方便的日志写入API,以及日志的恢复存储等功能。目前,它支持若干种日志类型,都冠以OP_XXX,并提供相关API,具体可以参见这里。为了保证日志的安全性,FSEditLog提供了EditLogFileOutputStream类作为写入的承载类,它会同时开若干个本地文件,然后依次写入,防止日志的损坏导致不可估量的后果。在FSEditLog上面,有一个FSImage类,存储文件镜像并调用FSEditLog对外提供相关的日志功能。FSImage是Storage类的子类,如果对数据块的讲述有所印象的话,你可以回忆起来,凡事从此类派生出来的东西,都具有版本性质,可以进行升级和回滚等等,以此,来实现产生镜像是对原有日志和镜像处理的复杂逻辑。

目前,在HDFS的日志系统中,有些地方与GFS的描述有所不同。在HDFS中,所有日志文件和镜像文件都是本地文件,这就相当于,把日志放在自家的保险箱中,一旦主控服务器挂了,别的后继而上的服务器也无法拿到这些日志和镜像,用于重振雄风。因此,在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的服务器立刻被扶正,在其上启动主控服务,利用其日志和镜像,恢复文件目录,并逐步接受各数据服务器的注册,最终向外提供稳定的文件服务。。。

同样的事情,GFS采用的可能是另外一个策略,就是在写日志的时候,并不局限在本地,而是同时书写网络日志,即在若干个远程服务器上生成同样的日志。然后,在某些时机,主控服务器自己,生成镜像,降低日志规模。当主控服务器牺牲,可以在拥有网络日志的服务器上启动主控服务,升级成为主控服务器。。。

GFS与HDFS的策略相比较,前者是化整为零,后者则是批量处理,通常我们认为,批量处理的平均效率更高一些,且相对而言,可能实现起来容易一些,但是,由于有间歇期,会导致日志的丢失,从而无法100%的将备份主控服务器的状态与主控服务器完全同步。。。

2、分布式文件系统中数据的正确性保证

在复杂纷繁的分布式环境中,也可能有各种各样的情况导致网络传输中的数据丢失或者错误。并且在分布式文件系统中,同一份文件的数据,是存在大量冗余备份的,系统必须要维护所有的数据块内容完全同步,否则,一人一言,不同客户端读同一个文件读出不同数据,用户非得疯了不可。(脏数据)

在HDFS中,为了保证数据的正确性和同一份数据的一致性,做了大量的工作。首先,每一个数据块,都有一个版本标识,在Block类中,用一个长整型的数generationStamp来表示版本信息(Block类是所有表示数据块的数据结构的基类),一旦数据块上的数据有所变化,此版本号将向前增加。在主控服务器上,保存有此时每个数据块的版本,一旦出现数据服务器上相关数据块版本与其不一致,将会触发相关的恢复流程。这样的机制保证了各个数据服务器器上的数据块,在基本大方向上都是一致的。但是,由于网络的复杂性,简单的版本信息无法保证具体内容的一致性(因为此版本信息与内容无关,可能会出现版本相同,但内容不同的状况)。因此,为了保证数据内容上的一致,必须要依照内容,作出签名。

当客户端向数据服务器追加写入数据包时,每一个数据包的数据,都会切分成512字节大小的段,作为签名验证的基本单位,在HDFS中,把这个数据段称为Chunk,即传输块(注意,在GFS中,Chunk表达的是数据块...)。在每一个数据包中,都包含若干个传输块以及每一个传输块的签名,当下,这个签名是根据Java SDK提供的CRC算法算得的,其实就是一个奇偶校验。当数据包传输到流水线的最后一级,数据服务器会对其进行验证(想一想,为什么只在最后一级做验证,而不是每级都做...),一旦发现当前的传输块签名与在客户端中的签名不一致,整个数据包的写入被视为无效,LeaseRecover(租约恢复)算法被触发。(事物回滚)

从基本原理上看,这个算法很简单,就是取所有数据服务器上此数据块的最小长度当作正确内容的长度,将其他数据服务器上此数据块超出此长度的部分切除。从正确性上看,此算法无疑是正确的,因为至少有一个数据服务器会发现此错误,并拒绝写入,那么,如果写入了的,都是正确的;从效率上看,此算法也是高效的,因为它避免了重复的传输和复杂的验证,仅仅是各自删除尾部的一些内容即可。但从具体实现上来看,此算法稍微有些绕,因为,为了降低本已不堪重负的主控服务器的负担,此算法不是由主控服务器这个大脑发起的,而是通过选举一个数据服务器作为Primary,由Primary发起,通过调用与其他各数据服务器间的InterDatanodeProtocol协议,最终完成的。具体的算法流程,参见LeaseManager类上面的注释。需要说明的是此算法的触发时机和发起者。此算法可以由客户端或者是主控服务器发起,当客户端在写入一个数据包失败后,会发起租约恢复。

一次写入失败,不论是何种原因,很有可能就会导致流水线上有的服务器写了,有的没写,从而造成不统一。而主控服务器发起的时机,则是在占有租约的客户端超出一定时限没有续签,这说明客户端可能挂了,在临死前可能干过不利于数据块统一的事情,作为监督者,主控服务器需要利用日志发起一场恢复运动,确保一切正确。(事物回滚)

 

0 0