在存储系统中,NFS(NetworkFileSystem)是一个重要的概念,已经成为兼容POSIX语义的分布式文件系统的基础。它允许在多个主机之间共享一个公共文件系统,并提供数据共享的好处,从而最大限度地减少所需的存储空间。本文将通过分析NFS文件锁状态视图一致性原理,帮助您理解NFS的一致性设计思想。文件锁文件锁是文件系统最基本的特性之一。借助文件锁,应用程序可以控制其他应用程序对文件的并发访问。NFS作为类UNIX系统的标准网络文件系统,在发展过程中逐渐原生支持文件锁(从NFSv4开始)。NFS自1980年代诞生以来已经发布了三个版本:NFSv2、NFSv3和NFSv4。NFSv4最大的变化是“状态”。某些操作需要服务器维护相关状态,例如文件锁。比如客户端申请文件锁,服务端需要维护文件锁的状态。否则,无法检测到与其他客户端的访问冲突。如果是NFSv3,需要借助NLM来实现文件锁定功能,但有时两者配合不好,容易出错。但是NFSv4被设计成有状态协议,可以自己实现文件锁功能,所以不需要NLM协议。应用程序接口应用程序可以通过fcntl()或flock()系统调用来管理NFS文件锁。下面是使用NFSv4挂载NAS时获取文件锁的调用流程:从上图中的调用栈不难看出,NFS文件锁的实现逻辑基本上是复用了VFS层设计和数据结构.通过RPC从服务器成功获取到文件锁后,调用locks_lock_inode_wait()函数将获取到的文件锁传递给VFS层进行管理。VFS层文件锁的设计相关资料很多。在此不再赘述。EOS原理文件锁是一个典型的非幂等操作,文件锁操作的重试和故障转移会导致客户端和服务端的文件锁状态视图不一致。NFSv4利用SeqId机制设计了一个最多可以执行一次的机制。具体方法如下:对于每一个open/lock状态,Client和Server同时独立维护seqid。当Client发起一个会引起状态改变的操作(open/close/lock/unlock/release_lockowner)时会将seqid加1作为参数发送给server。假设客户端发送的seqid为R,服务端维护的seqid为L,则:如果R==L+1,说明合法请求,应该正常处理。如果R==L,表示重试请求,服务器只返回缓存的回复。其他情况为非法请求,严禁访问。Server根据以上规则判断操作是否正常,重试还是非法请求。这种方式可以保证每个文件锁操作在服务器端至多执行一次,解决了RPC重试导致的重复执行问题,但是仅仅这样是不够的。例如LOCK操作发送后,调用线程被一个信号打断,然后服务端成功接受并执行了LOCK操作,这样服务端就记录了客户端持有锁,但客户端并没有维护由于中断而锁定,从而造成客户端和服务端的锁状态视图不一致。因此客户端也需要配合处理异常场景,最终才能保证文件锁视图的一致性。异常处理从上一节的分析可知,客户端需要配合异常场景来保证文件视图的一致性。那么客户设计师做了怎样的协调设计呢?目前客户端主要实现了SunRPC和NFS协议中的两个协议。两个维度相互配合来解决这个问题。下面分别介绍这两个维度的设计如何保证文件锁状态视图的一致性。SunRPC设计SunRPC是Sun专门为远程过程调用而设计的网络通信协议。这里我们将从保证文件锁视图一致性的维度来理解SunRPC实现层的设计理念:(1)客户端使用int32_t类型的xid来标识上层。对于操作者发起的每个远程过程调用过程,每次远程过程调用的多次RPC重试都使用相同的xid标识,保证多次RPC重试返回的任何一次都可以通知上层远程过程调用已经成功。它确保即使远程过程调用的执行时间很长,服务器也能得到结果。这不同于传统的netty/mina/brpc等,要求每个RPC都有一个独立的xid/packetid。(2)服务端设计了DRC(duplicaterequestcache)来缓存最近执行过的RPC结果。当接收到RPC时,会先通过xid获取DRC缓存。如果命中,说明此次RPC为重试操作,可以直接返回缓存的结果。这在一定程度上避免了RPC重试导致的重复执行问题。为了避免xid复用导致DRC缓存返回非预期的结果,开发者通过如下设计进一步有效降低了复用导致错误的概率:当客户端建立新连接时,初始xid使用随机值。服务器端DRC会额外记录请求的校验信息,在缓存命中时同时校验。(3)允许客户端在得到服务器的响应之前无限次重试,以保证调用者能够获得服务器确定性的执行结果。当然,这样的策略会导致调用者在无响应时挂起。(4)NFS允许用户在挂载时通过soft/hard参数指定SunRPC的重试策略。soft模式禁止超时重试,hard模式一直重试。当用户软挂载时,NFS实现不保证客户端和服务器状态视图的一致性。当远程过程调用返回超时时,需要应用程序配合状态的清理和恢复,比如关闭访问错误的文件等。但是,在实际应用中很少有应用程序会配合,所以一般NAS用户使用困难模式安装。总之,SunRPC需要解决的核心问题之一就是远程过程调用的执行时间不可控。协议设计者为此进行自定义设计,尽量避免非幂等操作RPC重试带来的副作用。信号中断应用程序在等待远程过程调用的结果时可以被信号中断。当发生信号中断时,由于没有获取到远程过程调用的执行结果,可能导致客户端和服务端的状态不一致。比如服务器端已经成功执行了加锁操作,但是客户端并不知道这种情况。这需要客户端做额外的工作来将状态恢复到服务器。下面简单分析一下获取文件锁被信号中断后的处理,以说明NFS协议实现层面的一致性设计。通过获取NFSv4文件锁的过程可以看出,NFSv4在获取文件锁时最终会调用_nfs4_do_setlk()函数发起RPC操作,最后调用nfs4_wait_for_completion_rpc_task()等待。以下是相关代码:;if(ret==0){ret=data->rpc_status;if(ret)nfs4_handle_setlk_error(data->server,data->lsp,data->arg.new_lock_owner,ret);}elsedata->cancelled=1;...}通过分析nfs4_wait_for_completion_rpc_task()的实现可以看出,当ret<0时,说明锁的获取过程被一个信号中断了,使用了structnfs4_lockdata的canceled成员记录。继续查看rpc_task完成释放时的回调函数nfs4_lock_release():它检测到信号中断。注意这个时候没有调用nfs_free_seqid()函数释放持有的nfs_seqid,这是为了:保证纠态过程中不会有并发的用户发起的加锁或者释放操作,简化实现。确保hard模式下的UNLOCK操作只有在LOCK操作返回后才会发送,保证获得的锁能够被释放。通过上述方式,客户端可以有效保证信号中断后客户端和服务端锁状态的最终一致性,但也是以损失部分可用性为代价的。总结文件锁是文件系统原生支持的一项基本功能。NAS作为一个共享文件系统,不得不面临客户端和服务端锁状态视图的一致性问题。NFSv4.0在一定程度上解决了这个问题。当然,随着技术进步的脚步不会停止,NFS的更新迭代也不会停止,未来对NFS的期待也会更多。最后,我们相信技术的力量,我们相信有技术力量的人。我们期待存储的未来,我们期待与您共创未来。
