LWN: NFS: 新千年的进展!
关注了就能看到更多这么棒的文章哦~
NFS: the new millennium
June 24, 2022
This article was contributed by Neil Brown
DeepL assisted translation
https://lwn.net/Articles/898262/
网络文件系统(NFS)协议已经伴随我们近 40 年了。虽然最初被定义为一个无状态(stateless)协议,但 NFS 的具体实现之中一直是需要管理状态的,而且在历次修订中越来越多地把这些状态管理的需求加入了协议。本系列的第一部分文章中讨论了 NFS 的早期情况,重点放在了状态管理方面。这篇文章通过对 NFS 自本世纪初以来的演变进行分析来讲完这个故事。
NFS 早期是由 Sun Microsystems 控制的,它是 NFS 协议的发起者,也是规范作者,以及具体代码实现的作者。随着新千年的到来,人们对 NFS 的兴趣增加,就出现了独立的实现版本。尤其是我所关注的 Linux 内核中这个实现(尤其是 server 端的实现)以及由 Network Appliance(NetApp)生产和销售的 Filer 设备。社区对 NFS 的兴趣增加,进而希望在该协议的后续发展中拥有更多的发言权。我不知道发生了什么谈判,但确实取得了进展,有一个明确的记录就在 RFC 2339 中,其中 Sun Microsystems 同意将有关 NFS 第四版(及以后)开发的某些权利转让给互联网协会,只要这部分的开发能在 24 个月内(即在 2000 年初)达到 "Proposed Standard" 状态。这个最后期限过去之后还得到了延长。我们在 2000 年底通过 RFC 3010 获得了一个 "Proposed Standard",2003 年 4 月在 RFC 3530 中进行了修订,2015 年 3 月又在 RFC 7530 中进行了修订。
负责这项开发工作的 IETF 工作组主要由 Sun 公司和 NetApp 公司所驱动;它有两个联合主席,每个公司一个,RFC 上所列的大多数作者都来自这些公司。我对这些讨论的记忆是,有一个很长的需求清单,但没有一个连贯的整体的共同愿景。即将到来的(同时也是在不断变化的)最后期限促使人们渴望得到一些成果,即使它并不完美。因此,NFSv4 (尤其是现在被称为 NFSv4.0 的这个第一版)给我的感觉就像是被粘在一起的有用的碎片,而不是精心编织出来纺织作品。我在 NFSv2 中看到的优雅都已经消失。
NFSv4 将我们之前看到的所有各种协议纳入合并成了一个单一的协议,并有一个单一的规范。虽然 "tools" 的方式非常强大,并且非常适合建立原型系统,但通常会有这样的时候:整合起来能提供的好处超过了分成多个独立组件所带来的敏捷性(agility)。对于 NFS 来说,第四版就是这样的一个时刻。对访问控制列表(ACL)、配额跟踪(quota-tracking)、安全协商(security negotiation)、命名空间(namespace)管理、字节范围锁定(byte-range locking)和状态管理的支持都被整合在一起了,并且整合之后的形式往往与它们最初的独立版本完全不一样。在所有这些变动中,我只想重点谈一下对共享状态管理有影响的两个领域。
The change attribute and delegations for cache consistency
正如我们已经看到的,时间戳这种方式用来跟踪文件内容在服务器上的变动并不是一个理想的方式。即使客户端知道时间戳是以某种高精度报告的,它也不能知道在这个时间单位内可以处理多少个 write request。因此,时间戳充其量只是一个有用的提示信息。NFSv4 的设计者希望能有更好的方式,所以他们引入了 "change" 这个属性,有时也称为 "changeid"。这是一个 64 位的数字,每当对象(如文件或目录)以任何方式改变时,必须增加。
这个 changeid 是协议中强制实现的,因此,有好几年,Linux NFS server 都是不符合标准的,因为没有一个 Linux 文件系统可以提供一个符合要求的 changeid。这个问题在 Linux 2.6.31 中得到了解决,但只适用于 ext4,而 XFS 是在 v3.11 中得到了解决。对于不提供 i_version 的文件系统,Linux NFS 服务器会退而求其次,使用 inode 的 change time 来代替,这可能无法保证同样的效果。
NFSv3 引入的 wcc(弱缓存一致性, weak cache consistency)属性信息在 NFSv4 中被保留了下来,但只针对目录,不过它也会使用 changeid 而不是改变时间,奇怪的是,并未在 SETATTR 操作中支持。Wcc 属性不提供给文件。对于 WRITE 请求,仍有可能获得 "before"和 "after" 这两种属性,因为每个 NFSv4 的请求都是一个由基本操作序列组成的一系列复合动作,这其中可能包含 GETATTR、WRITE、GETATTR 这样的序列。然而,这些并不保证以原子方式执行,所以其他一些客户端也可以在两个 GETATTR 调用之间来执行 WRITE。如果 "before"和 "after"的 changeids 之间的差异正好是 1,那么应该可以放心确定并没有中间的干扰,但协议规范并没有明确规定这一点。相反,NFSv4 提供了 delegation (委托,或者授权)。
授权(delegation,或更具体地说是"read delegation",也就是读取授权)是服务器向客户作出的承诺,即其他客户端(或服务端上的本地应用程序)不能向该文件写入。在会发生冲突的那种新的请求被允许完成之前,server 会主动收回该授权。当客户端持有一个授权时,就可以确定服务器上的所有修改都是由它自己做出的,所以它甚至不需要检查 changeid 来确保其 cache 是仍旧准确的。这就提供了一个强大的缓存一致性的保证。
所以,只要服务器在客户打开一个没有人写的文件时提供一个读取授权,就很容易使用 cache 了。服务器到底什么时候应该这样做,尚不完全清晰。提供授权是有代价的,因为当文件被打开进行写入访问时需要收回这些授权,而这可能会导致 open request 被延误。
请注意,如果没有其他客户或应用程序打开这个文件的话(无论读或写),服务器也可以提供一个 "write delegation"。我不清楚这到底有多大用处。理论上来说最明显的好处是,在文件关闭之前进行的 write 操作都不需要被 flush 出去,而且 byte-range lock 也都不需要通知给服务器端了。这些好处是否实用就不是很清楚了。Linux 内核的 NFS 服务器从不提供 write delegations。
clientids, stateids, seqids, and client state management
如前所述,NFSv4 集成了 byte-range lock,因此需要服务器来跟踪每个客户端所持有的所有锁的状态;服务器端还需要知道客户端何时重启。这个功能在 NFSv4 中是全新设计的(在 NFSv4.1 中还有所改进)。
在可用性方面最大的区别就是,不需要客户端报告说它被重启了(这正是 STATMON 协议的做法),而是需要 NFSv4 客户端定期报告它没有被重启。如果服务器在 "lease time (租期)"(在 Linux 中通常为 90 秒)内没有收到客户端的消息,它就可以假定该客户端已经消失了,并且不得阻止另一个客户端来执行会被第一个客户端持有的状态所阻止的那些访问了。因此,就算没有什么 task 的客户端也至少需要时不时地发送一个 "我还在这里" 的消息(就是 NFSv4.0 中的 RENEW),这样,即使有可能有网络延迟,服务端也不应该在 90 秒(或配置中指定的时间内)内看不到任何信息。
这意味着,如果一台机器在没有重启的情况下崩溃的话,锁定的文件不会无限期地被锁定。相反,这意味着,如果路由器故障或线路问题导致网络通信中断过久(称为 "network partition"),那么即使客户端仍在运行,lock 也是相当于丢失了,当网络修复后,客户端就无法继续运行。在 Linux 上,客户端应用程序如果试图访问一个它持有 lock 但是后来丢失了的文件描述符,就会收到一个 EIO 错误。
所有这些都可以相对比较简单地实现。例如,每个 request 都可以包含一个客户端启动时的时间戳。服务器端会根据客户端的 IP 地址来记住这个时间戳,如果它改变了,或者至少在租期内没有看到任何信息,服务器就可以丢弃客户端之前所拥有的任何状态。相应地,服务器的每个回复都可以包含一个时间戳,这样服务器的重启就可以被客户端检测到。然而,这是太简化的处理了。NFSv4 的设计者在 NFSv3 和网络锁管理器方面有相当多的经验,因此他们认为自己有足够的理由来增加一些复杂支持。
Clientids and the client identifier
依靠客户端的 IP 地址,这并不是一个真正的好主意。一方面,这是因为在用户空间运行多个客户端,或使用网络地址转换(NAT),都会出现几个客户端拥有相同的 IP 地址的情况,另一方面也是因为移动设备都会出现 IP 地址变动。后者在 NFSv4.0 的开发过程中并不是一个大问题(尽管 4.1 处理得更好),但用户空间的客户端和 NAT 的问题肯定是个麻烦。不同的客户端可以通过他们的端口号来识别,但是如果一个通过 NAT 网关连接的客户端的连接中断并且不得不重新建立连接的时候,新的连接可以使用不同的端口,因此看起来是一个全新的客户端。因此,NFSv4 要求每个客户端生成一个普遍唯一的客户端标识符(大小可以达到 1KB),将其与 instance identifier(如启动时间戳)相结合,并通过 SETCLIENTID 请求将两者提交给服务器。服务器会回复一个 64 位的 clientid 号码,然后可以在任何需要表明自己身份的请求中使用。
这个客户端标识符(client identifier)是最近在 Linux 存储、文件系统和内存管理峰会(LFSMM)上进行过讨论的。默认情况下,Linux 使用主机名来作为唯一性的主要依据。在主机合理配置了的情况下,这在私有网络中应该足够了。然而,在那些没有配置唯一的主机名而是创建了一个新的网络命名空间的从而得到一个独立的 NFS 客户端实例的容器中,就有问题了。在不同的管理域(administrative domain)的客户端(也就是可能有重复的主机名)访问共享服务器的情况下,也可能出现问题。
stateids and seqid - per-file state
这种简单方案的另一个缺点是,它将所有的状态收集在一起,在空间和时间上都没有明确的区分。
空间上的区别意味着每个文件的状态都可以单独管理。具体来说,如果服务器在租期内没有收到客户端的消息,就必须丢弃所有发生了冲突的请求相关的状态,但它不需要丢弃那些没有争议的状态。因此,当客户端重新建立连接时,它可能会失去对一些文件的访问权,而不是所有文件。这就要求能够识别不同的状态元素,以便服务器端能够告诉客户端哪些已经丢失,哪些仍然有效。Linux NFS 服务器直到最近合并了 "Courteous Server" 功能后,才得以实现其全部好处。
这种更细粒度的状态管理在很大程度上是由 NFSv4 的 OPEN request 来实现的。OPEN 的存在与否,正是与 NFSv3 的一个很大不同,它只有在服务器能够跟踪客户端的状态(具体来说是它打开了哪些文件)时才有可能。一个 OPEN request 可以指出是需要 READ 还是 WRITE 访问,或者两者都需要,并且它可以要求服务器拒绝所有其他客户的 READ 或 WRITE 访问。这种拒绝是 POSIX 接口的大忌,但对于与其他 API 的互操作性来说是有必要。Linux 服务器支持 NFSv4 客户端之间的这种拒绝机制,但不允许服务器上的本地访问或 NFSv3 的访问被阻止;此外,本地已经 open 的文件不会导致有冲突的 NFSv4 OPEN 失败。
NFSv4 的 OPEN request 还指出了一个 "open owner",它是由一个 active clientid 和一个任意的标签来组成的。Linux 客户端为每个用户都生成一个不同的标签,因此,如果一个用户同一时刻多次打开一个文件的话,服务器将只看到该文件被打开一次。OPEN 请求返回一个 "stateid" 来代表打开的文件,应该在后续所有 READ/WRITE 请求中使用。每个这种 stateid 都是不一样的,并且可以被服务器废止(在特殊情况下才会发生)而不影响任何其他的状态。
后续的 OPEN 或 OPEN_DOWNGRADE 请求可以改变与该文件相关的访问标志和 DENY 标志(对于相关的 open owner)。这些请求中每一个都会产生一个新的 stateid,尽管它并不完全是全新的。每个 stateid 有两个部分:"seqid",在每次变动的时候都会增加,还有一个 "other" 部分就不会增加。这允许客户端明确地确定第二个 OPEN 请求是否打开了与第一个 OPEN 请求所打开的相同文件,因为 stateid 的 "other" 部分会匹配上。它还可以清楚地表明在服务端进行的各种改动的顺序,所以客户端可以确保它与服务器端保持同步。因此,"seqid" 给了状态一个时间维度。
当一个 CLOSE 请求和一个 OPEN 请求在同一时间被发送时,这就很重要了。由于 hard link 的存在,客户端可能不知道它是在同一个文件上进行操作,所以这不能被视为客户端的不正确的行为。如果服务器先执行 CLOSE,那么 OPEN 就可能成功,一切都没有问题。如果服务器先执行 OPEN,它将增加该文件状态的 seqid,当它看到 CLOSE 时,就会拒绝,因为 seqid 是旧的申请。在这两种情况下,文件都会保持打开状态,这也是客户想要的(因为它打开了两次文件,但只关闭了一次)。
对于 LOCK 和 UNLOCK 请求,都有类似的 stateid,也包括 seqids,也都有一个相应的 "lock owner"。对于 POSIX lock 来说,lock owner 对应于一个进程,对于 OFD lock 来说,则对应于一个打开状态的文件描述符。
NFSv4.1 - a step forward
NFSv4 工作组花了一些功夫来考虑未来的版本,并描述了可能发生的各种改动。这不是徒劳无功的,在 2010 年 1 月,NFSv4.1 在 RFC 5661 中被定义了下来(大约 10 年后在 RFC 8881 中进行了更新)。
V4.1 包含了许多小改进,这些改进是基于几年来对一个几乎是全新的协议的经验。许多人对 "dot-zero" 版本总是持怀疑态度,对于 NFSv4 的情况来说,这种观点是有一定道理的。NFSv4.0 确实可以工作,而且相当好了,但 4.1 工作得更好。大多数的小细节在本文里都没有提到,但是应该提一下把 UDP 排除在支持的传输层协议之外的决定,因为它是用户可见的的一个变动。UDP 没有拥塞控制,所以 NFS 在一般情况下不能很好地工作。当然,只要其他一些管理拥堵的协议如 QUIC 被放在中间 layer,那么仍然可以使用 UDP。V4.1 还允许服务器端告诉客户端可以安全地对仍在 open 的文件进行 unlink 的操作,这样就可以避免之前的笨拙的操作(在删除时重命名)了。
NFSv4.1 中用户可见的最大变动可能是增加了 "pNFS" —— parallel NFS。这似乎是一个营销术语(marketing term),因为它很容易说,但却只是很粗略地提到了一下这里最重要的变动。有了 NFSv4.1,就有可能将 I/O 请求(READ 和 WRITE)offload 到其他协议上,这些协议很可能通过不同的媒介来与不同的服务器通信。这允许单个 NFS 客户端与 cluster filesystem 进行通信,而不必通过单个 IP 地址来发送所有的请求。这当然是一种并行性,但说早期的 NFS 不允许任何并行性的话,这是并不公平的。哪怕是在 NFSv2 中也可以有多个未完成的 request,服务器端都可以同时并行处理。
这些 offload 协议,以及它们如何整合,已经在不同的 RFC 中有描述了。例如支持通过 iSCSI 运行的 block-access 协议(RFC 5663)或 OSD 对象存储协议(RFC 5664),或者使用 NFSv3 或更高版本(RFC 8435)在数据访问时采用基于 "flexible file" 的方式。这里之所以提到,主要是因为有一种新的状态需要被管理了,那就是一种名为 "布局(layouts)" 的对象。
一个布局描述了如何使用其他协议来访问一个文件中的一部分。每个布局都有一个 stateid,可以分配,然后被丢弃,所以服务器端总是知道哪些布局可能还在使用的。例如如果服务器需要将一个文件迁移到一个另一个存储位置,那么这一点就很重要了——当客户端发现它知道可以用什么 block location 来访问该文件时,它可能就不需要做文件迁移了。
Sessions and a reliable DRC
从我们管理状态的角度来看,NFSv4.1 中最大的变动是该协议最终允许一个完全可靠的重复请求缓存(duplicate request cache)。正如在第一部分中所描述的,当一个请求或回复可能已经丢失了,因此客户端不得不重新发送请求的这种罕见情况下,这是很有必要的。在包括 NFSv4.0 在内的版本中,服务器端只是尽力猜测哪些请求和回复可能是值得记住一段时间的。在 NFSv4.1 中,它可以明确知道。
每一个 NFSv4.1 会话都是一个新的 element of state,它是通过 CREATE_SESSION 请求来从全局 clientid 中 fork 出来的。给出一个 clientid 和一个 sequence number,就可以分配出一个新的 sessionid,它有一系列与之相关的各种属性,其中就包括最大并发请求数。服务器端将在其 duplicate request cache 中分配这么多的 "slot",而客户端将为每个请求分配一个小于这个数字的 slot。客户端承诺在它看到对上一个带有该 slot 号的请求的回复之前,永远不会重复使用这个 slot 号,并且服务器承诺记住每个 slot 中对最近一个请求的回复,只要客户端提出了这个要求。
客户端甚至可以要求服务器端将缓存的回复都存储在 stable storage 中,这样就能做到在重启后仍然存在。如果服务器实现了这个功能并同意提供这个功能,那么结果就是最接近完美的 exactly-once 的语义了。
从表面上看,这里有一点不平衡。最常见的请求(READ、WRITE、GETATTR、ACCESS、LOOKUP)是幂等的,不需要被缓存,但服务器必须为每个 slot 都保留 cache 空间,因此要么浪费 cache 空间,要么会在不必要的情况下限制并发。这在实践中不是一个问题,因为协议允许客户端用不同的参数来创建多个会话。它可以创建一个具有大 slot 数和最大 cache size 为零的 session,并将其用于所有空闲请求。它还可以创建一个 slot 数较少、但是最大 cache size 的 session,并将其用于不能重复的请求。
Directory delegations
NFSv4.1 中的第三种新状态(前两种是 layout 和 session)是 directory delegation 也就是目录授权。在 NFSv4.0 中,可以 open 文件,但与目录相关的所有操作仍然与 NFSv3 中一样,每个操作都是 discrete 的,没有持续保持的状态。在 v4.1 中,我们得到的东西有点像用 GET_DIR_DELEGATION 打开一个目录。这个请求不包含一个明确的 clientid;相反,它使用与该请求所在的 session 相关的 clientid。授权本质上是一个 standing request,也就是说客户端被告知其他客户端对目录所做的任何更改。根据协商的具体内容,这可能涉及到服务器需要说 "有些东西已经改变了,你不再拥有授权",或者它可能提供更精细的细节,比如具体来说什么东西已经发生了改变。这就支持了更强的文件名缓存一致性(file-name cache coherence),甚至允许客户端应用程序来接收变动通知。这个功能在 Linux 中没有实现,无论是服务器端还是客户端都是这样。
NFSv4.2 - meeting customer needs
NFS 的最新版本是 v4.2,在 RFC 7862(2016 年 11 月)中描述的。该文件描述了这次修订的目标是 "采用常见的本地文件系统功能但是在早期版本的 NFS 提供,并可以远程提供出去。"
与 NFSv4.1 主要以更有效或更可靠的方式提供现有功能相比,v4.2 真正提供了一些新功能。至少对 NFS 来说是新的功能。这些功能包括支持 lseek()的 SEEK_DATA 和 SEEK_HOLE 功能,可以高效管理 sparse file,支持 posix_fallocate()来明确地分配和删除文件中的空间,支持 posix_fadvise(),因此客户可以告诉服务器要针对什么样的 I/O 访问模式来进行优化,以及支持 reflinks,用来在文件之间共享内容而不需要复制。这些都没有给协议增加任何新的状态,所以它们并不属于我们这次讨论的重点领域。
还有一个新的功能,确实涉及到一种新的状态,那就是服务器端复制(server-side copy)。这个功能可以支持 copy_file_range(),可以在一个服务器上的两个文件之间复制,或者(如果服务器配合的话)在不同服务器上的两个文件之间进行复制。还有与其密切相关的功能(使用 WRITE_SAME 操作)可以使用一个特定的模式来在必要时重复初始化一个文件。
当客户端发送一个 COPY request 时,服务器端可以选择在回复前执行完整的拷贝,或者异步地把这个操作调度起来然后就立即返回。在后一种情况下,会给客户端返回一个表示了服务器上正在进行的操作的 stateid。客户端可以使用这个 stateid 来查询状态(最重要的就是显示进度条)或取消 copy。服务器可以使用这个 stateid 来通知客户端此操作已经完成或失败。
The future for NFS
到目前为止,在可预见的未来,还没有看到关于 NFSv4.3 的线索,而且可能永远也不会有这样的版本了。v4.2 规范与之前的规范不同,它不是一个完整的规范,而是参考了 v4.1 规范并增加了一些扩展。未来继续增加 extension 的形式已经在 RFC 8178(2017 年 7 月)中添加了扩展,允许进一步增加增量改动,而不要求每次都改变次要版本号。这一点已经在 RFC 8275 中很好地应用了起来,这一版允许在文件创建过程中向服务器发送 POSIX "umask",而 RFC 8276 则增加了对扩展属性(extended attributes)的支持。
当然,围绕 NFS 仍在持续进行开发工作。其中一个更有趣的领域就是关于 NFS 协议如何在 TCP 或 RDMA 之外的地方来有效地进行传输,这两个协议是目前使用到的主要的两个协议。
NFS draft-cel-nfsv4-rpc-tls-pseudoflavors-02 探讨了在 TLS 上使用 ONC-RPC(NFS 使用的底层 RPC 层),具体是探讨了 TLS 提供的 authentication 如何与 NFS 的 authentication 要求来交互操作。然后,draft-cel-nfsv4-rpc-over-quicv1-00 在此基础上探讨了如何在 QUIC 协议上使用 NFS。草案名称中的 "cel" 是指 Chuck Lever,他是 Linux NFS server 的现任维护者。
其他 draft 以及所有的 RFC 都可以在 IETF NFSv4 working group 的网页上找到。
虽然 NFS 和 Linux 一样,看起来都还没有开发完成,但它似乎已经接受了成为一个有状态协议,所有的状态都在一个一致性非常好的模型里面管理起来。在 NFSv4.2 中,一种全新的 state (服务器上的异步复制)被毫不费力地纳入了模型中,这就证明了这一点。因此,未来的改进可能会集中在其他地方,也许是在近期的提高安全性以及支持新的文件系统功能的举措之后。谁知道呢,也许有一天它甚至会与 Btrfs 和平相处。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~