分布式文件系统在设计实现的时候面临的一个首要问题是数据一致性,即需要向用户提供一个合理的数据一致性模型(Consistency Model),这个数据一致性模型成为用户程序和系统之间的一个合约(Contract)。这个合约里的规则保证程序对数据的读写结果是可预期和可理解的。
具体到分布式文件系统的客户端,如果没有缓存,数据一致性和多个进程访问本地文件系统的场景是类似的。和本地文件系统一样,调用程序通常通过文件系统提供的两个层面的文件锁来保证强数据的一致性。
首先是全文件级的锁。如共享模式,当客户端打开一个文件时,可以指定被分配的文件句柄在被关闭以前该文件是否允许被再次打开,如果允许,就可以指定允许的请求读写权限。
其次是文件区间锁(Byte Range Lock)。文件在打开后可以用文件句柄给文件不同区间上锁,不同的文件系统可能提供不同的锁语义,比如Windows文件系统一般提供强制锁(Mandatory Locks),POSIX一般提供建议锁(Advisory Locks)。锁重叠时的处理方式也不同。
分布式文件系统需要向上层应用提供透明的客户端缓存,从而缓解网络延时现象,更好地支持客户端性能水平扩展,同时也降低对文件服务器的访问压力。当考虑客户端缓存的时候,由于在客户端上引入了多个本地数据副本(Replica),就相应地需要提供客户端对数据访问的全局数据一致性。举例来说,客户端C1、C2、C3上的应用程序同时访问一个文件readme.txt,如果readme.txt在C1的本地文件缓存中被改动后,C2和C3的本地缓存长时间还得不到更新,那就意味着对于同样的应用,在分布式多客户端环境运行时的读写一致性模型和在本地或者单客户端环境中的是不一样的,违背了分布式文件系统的标准协议。例如NFS或者SMB的追求目标,即尽量向上层应用程序提供和本地文件系统一致的行为,让程序的运行结果变得可预期和可理解。如果多客户端无法提供和单客户端一致的强读写一致性,那么对于上层应用程序来说是很难接受的。
为了解决分布式文件系统的客户端缓存一致性,在文件存储里主要采用如下几种数据一致性模型。
1.基于时间戳的简单一致性协议
NFSv2/3都是简单的无状态的协议,NFSv2/3服务器不记录和跟踪文件状态。当客户端决定将本地缓存的文件内容更新到服务器时,服务器无法主动去通知其他持有数据副本的客户端。客户端需要自行检查本地缓存的有效性,采用的方法是向服务器查询文件元数据中的最后修改时间(Last Modify Time),看它和本地缓存的文件修改时间是否一致,如果不一致就将本地副本设置为无效状态,在读取时需要去服务器重取。
这种方式是无法提供强数据一致性的。能提供的一致性保证就是Close To Open一致性,指的是应用关闭文件时,NFS客户端保证将本地缓存的文件内容写到服务器端,当同一个文件在这个客户端又被打开的时候,NFS客户端先去做文件修改时间的检查,确保上一次关闭以后对应本地缓存的内容还有效。这种解决方案显然无法处理好多个客户端并发读写访问一个文件的场景(如数据库访问),这只能依赖用户自行关闭所有客户端的缓存。
因为上面这样的协议在服务器是无状态的,所以,失效恢复(Failure Recovery)的方案非常简单,就是当客户端失败或者网络断开时,将客户端缓存中的内容设为完全失效。
2.缓存确认方案
AFS(Andrew File System)等分布式文件系统采用缓存确认(Cache Invalidation)方案来解决一致性问题。基本想法就是,当文件第一次被客户端打开的时候,在服务器注册缓存副本的信息;当客户端关闭文件、将本地缓存的文件内容更新到服务器的时候,服务器会主动去回调(Callback)其他持有相应数据副本的客户端,告知目前缓存的副本会在下次文件被打开的时候失效。由于缓存状态只能在文件打开关闭的时候被更新,因此这样的方案也无法保证数据的强一致性。
在网络临时断开、客户端失败或者重启的时候,服务器可能需要重试回调;当服务器失败的时候,副本状态信息一般会被丢失,比较保守的做法是每个客户端将自己本地的缓存全部设置为失效,访问时重新从服务器读取。
3.基于租约的一致性协议
租约是分布式系统里广泛使用的技术,主要思想就是将访问一个资源的给定权利按照合约的方式提供给某个持有者。这份合约可以在一个指定时间之后过期,或在发生网络断开、系统重启等事件时过期。其实,租约可被看成一个有过期时间的特殊的文件级锁,或者基于服务器文件状态的缓存一致性(Cache Coherence)机制。主要分为以下两大类。
· 读租约(Read Lease):如果客户端拥有文件读租约,那么它可以直接而不用去后端读取该文件的本地缓存,也可以主动地预读文件数据,将数据提前加载到客户端缓存里。如果有其他客户端要申请写租约,服务器会将读租约召回,读租约所在的客户端缓存会失效,从而保证数据的一致性。
· 写租约(Write Lease)整个系统中最多只有一个客户端可以拥有文件的写租约,因此,这个客户端是系统中唯一一个拥有有效文件数据的实体(这时候,服务器的数据不一定有效)。在写租约有效期间,本地缓存可被作为回写缓存(WriteBack Cache)。这样,文件数据既能被读取又能被写入,直到租约失效或者文件被关闭的时候,才会被发送到服务器。
如果客户端没有获得文件的租约或者租约被取消,那就意味着这个文件的数据一致性无法得到保证。一般来说,文件系统租约的相关操作包括下面几种:
· 客户端请求租约(Requesting Leases):客户端在打开一个文件的时候,会向服务器请求一个指定类型的租约,同时也会向服务器请求进行升级或降级自己的租约,或者查询租约状态等操作。
· 服务器授予租约(Granting Leases):服务器根据自己管理的文件租约,决定是否授予客户端相应的租约及其具体类型。例如,客户端请求一个文件的写租约,如果当前该文件已经给出了一个或多个读租约,那么此时服务器会有多种实现方案:可以把该文件的读租约全部召回,再给新客户端授予写租约;也可以先授予该客户端读租约,拿到租约的客户端后续并不会真的有写操作。
· 客户端续租约(Renewing Leases):客户端在租约超期前或者收到服务器的租约超期回复以后,向服务器申请延长当前的租约有效期。
· 客户端终止租约(Deleting Leases):客户端关闭文件或者文件系统卸载的时候,服务器在收到终止租约请求以后会将对应的租约状态移除。
· 服务器修改或取消租约(Breaking Leases或者Revoking Leases):当一个客户端发出的请求和目前服务器上对这个文件的租约聚合状态发生冲突时,和基于回调的缓存失效方案类似,服务器向需要有租约状态冲突的客户端发出修改或者取消租约的请求。根据文件系统对一致性的保证和具体操作的不同,具体协议可能需要服务器阻塞住这个请求的处理,等待客户端对租约状态改变的确认,也可能在发出请求后不需等待,继续完成操作。在前面的例子中,如果一个拿到读租约的客户端发出写请求,那么服务器将收到以后需要向所有的持有该文件读租约的客户端发出解除租约的请求。除了一些特殊情况,具体协议通常会选择让这个写操作不需等待客户端的确认,继续完成。
· 客户端确认租约修改或取消的状态(Acknowledging Lease Breaks ):当客户端收到服务器修改或取消租约的通知以后,可以选择接受服务器的租约安排,也在某些情况下选择进一步降级自己的租约。这时,持有文件写租约的客户端一般需要先将本地缓存的文件更新数据写到服务器,在写全部完成之后向服务器发出确认。收到确认之后,服务器才能继续处理被租约中断阻塞的操作。
当一个客户端文件句柄被关闭的时候,服务器文件句柄也随之关闭,租约就失效了,本地缓存也会相应失效。为此,有两个优化方法:一个方法是在当前客户端文件句柄被关闭以后,先不关闭服务器文件句柄,这样在下一次打开同一个文件时可以继续利用本地已有的缓存,这就是所谓的句柄租约(Handle Lease)。假如一个文件被客户端反复打开和读写,此时可以向该文件提供一个“写+句柄”的租约,这样在文件系统被卸载、租约被服务器取消或者在其他任何由客户端主动决定的情况下就能将本地缓存数据写到服务器,并延时关闭服务器文件句柄,利用缓存处理后续的文件操作请求。其主要流程如下图所示。
延时关闭服务器文件句柄
另一个方法就是将租约的持有者由单个服务器文件句柄扩展到单个客户端,即允许在一个客户端上跨服务器文件句柄共享租约,从而实现跨用户、跨进程的共享文件数据和元数据缓存。举例来说,如果一个文件被同一个客户端上的不同进程同时打开,那么对于服务器来说,就需要为对应的每个服务器文件句柄分配一个独立的租约。这种方式会导致同一个客户端上存在的租约会互相取消,或者需要为同一个文件在同一个客户端上保存多个缓存,最终导致性能的丧失和网络流量的增大。一个解决方法是在服务器为来自同一个客户端的同一个文件上的所有服务器文件句柄提供一个共享租约。当一个文件被某个客户端第一次打开时,客户端去请求一个新的租约,这个租约是独立于服务器文件句柄存在的。之后同一个客户端再去打开这个文件的时候,可以要求把新生成的服务器文件句柄和客户端已经持有的租约连接起来,这样客户端就可以为一个文件的不同句柄提供一个共享缓存。其主要流程如下图所示。
客户端为不同文件句柄提供共享缓存
当上述两种优化方法被放到一起考虑的时候,分布式文件系统可以提供跨文件句柄的共享句柄租约。举例来说,很多用户都是将网站内容存储在一个SMB或者NFS的文件系统上,然后利用多个虚拟机拓展Web服务器,通常每个Web服务器都是多线程的,并反复读取同一组文件。当同时使用共享读和句柄租约的时候,静态Web文件和动态脚本文件在很长时间里都只需要从后端的文件系统读取一次,之后每个客户端上只需缓存每个文件的副本,并用这个副本持续地支持多线程同时读取文件。在用户升级或者更新网站的时候,各个Web服务器持有的被更新的文件的租约会被中断,文件读取操作会被重新发送到服务器。在文件再次被客户端打开时,一个新租约会被重启申请,系统会很快回到原先的稳态,客户端和服务器之间的网络流量一般会恢复到原来很有限的状态。