Andrew文件系统
1 引言
Andrew 文件系统是卡内基梅隆大学 (CMU) 于 1980 年代引入的。该项目由CMU著名教授 M. Satyanarayanan(简称“Satya”)领导,其主要目标很简单:扩展性。具体来说,如何设计一个分布式文件系统,使得服务器可以支持尽可能多的客户端?
有趣的是,设计和实现的许多方面都会影响可扩展性。最重要的是客户端和服务器之间的协议设计。例如,在 NFS 中,协议强制客户端定期检查服务器以确定缓存的内容是否已更改;由于每次检查都会占用服务器资源(包括CPU和网络带宽),因此频繁进行这样的检查将限制服务器可以响应的客户端数量,从而限制可扩展性。
AFS 与 NFS 的不同之处还在于,从一开始,合理的用户可见行为就是首要关注的问题。在 NFS 中,缓存一致性很难描述,因为它直接取决于低级实现细节,包括客户端缓存超时间隔。在AFS中,缓存一致性很简单且易于理解:当文件被打开时,客户端通常会从服务器接收到最新的一致副本。
我们将讨论两个版本的 AFS。第一个版本(我们称之为 AFSv1,但实际上最初的系统被称为 ITC 分布式文件系统有一些基本设计,但扩展性不尽如人意,这导致了重新设计和最终协议(我们称之为 AFSv2,或简称 AFS)。
2 AFSv1
2.1 基本介绍
AFS 所有版本的基本原则之一是在访问文件的客户端计算机的本地磁盘上缓存整个文件。当您 open()
文件时,将从服务器获取整个文件(如果存在)并将其存储在本地磁盘上的文件中。后续应用程序的read()
和write()
操作将被重定向到存储文件的本地文件系统;因此,这些操作不需要网络通信并且速度很快。最后,在 close()
时,文件(如果已被修改)被刷新回服务器。请注意与 NFS 的明显对比,NFS 缓存块(不是整个文件,尽管 NFS 当然可以缓存整个文件的每个块)并在客户端内存(而不是本地磁盘)中进行缓存。
让我们进一步了解细节。当客户端应用程序第一次调用 open()
时,AFS 客户端代码(AFS 设计者称之为 Venus)将向服务器发送一条 Fetch 协议消息。 Fetch 协议消息会将所需文件的整个路径名(例如,/home/zfhe/notes.txt
)传递到文件服务器(他们称之为 Vice 的组),然后文件服务器将遍历路径名,找到所需的文件,并将整个文件发回给客户端。然后,客户端代码会将文件缓存在客户端的本地磁盘上(通过将其写入本地磁盘)。正如我们上面所说,后续的 read()
和 write()
系统调用在 AFS 中严格是本地的(不发生与服务器的通信);它们只是重定向到文件的本地副本。由于 read()
和 write()
调用的行为就像对本地文件系统的调用一样,因此一旦访问了一个块,它也可能会缓存在客户端内存中。因此,AFS 还使用客户端内存来缓存其本地磁盘中的块副本。最后,完成后,AFS 客户端检查文件是否已被修改(即,它已被打开用于写入);如果是,它将使用存储协议消息将新版本刷新回服务器,并将整个文件和路径名发送到服务器进行持久存储。
下次访问文件时,AFSv1 的效率会更高。具体来说,客户端代码首先联系服务器(使用 TestAuth 协议消息)以确定文件是否已更改。如果没有,客户端将使用本地缓存的副本,从而通过避免网络传输来提高性能。下面展示了AFSv1中的一些协议消息。请注意,该协议的早期版本仅缓存文件内容;例如,目录仅保存在服务器上。
|
|
2.2 存在的问题
AFS 第一个版本的一些关键问题促使设计人员重新考虑他们的文件系统。为了详细研究这些问题,AFS 的设计者花费了大量时间测量他们现有的原型,以找出问题所在。这样的实验是一件好事,因为测量是理解系统如何工作以及如何改进系统的关键;因此,获得具体、良好的数据是系统建设的必要组成部分。在他们的研究中,作者发现 AFSv1 的两个主要问题:
- 路径遍历成本太高:当执行 Fetch 或 Store 协议请求时,客户端将整个路径名(例如
/home/zfhe/notes.txt
)传递给服务器。服务器为了访问该文件,必须执行完整的路径名遍历,首先在根目录中查找home
,然后在home
中查找zfhe
,依此类推,一直沿着路径遍历,直到最后找到所需的文件。由于许多客户端同时访问服务器,AFS 的设计者发现服务器花费了大量的 CPU 时间只是沿着目录路径查找。 - 客户端发出过多的TestAuth 协议消息:与NFS 及其过多的GETATTR 协议消息非常相似,,AFSv1 也产生了大量流量,用于通过 TestAuth 协议信息检查本地文件(或其状态信息)是否有效。因此,服务器要花费大量时间告诉客户端是否可以使用其缓存的文件副本。大多数情况下,答案是文件没有变化。
AFSv1 实际上还存在两个问题:服务器之间的负载不均衡,并且服务器对每个客户端使用单独的进程,从而导致上下文切换和其他开销。通过引入卷解决了负载不均衡问题,管理员可以跨服务器移动卷以均衡负载; AFSv2 中通过使用线程而不是进程构建服务器来解决上下文切换问题。然而,我们在这里重点关注上面限制系统规模的两个主要协议问题。
上述两个问题限制了AFS的可扩展性;服务器CPU成为系统的瓶颈,每台服务器只能服务20个客户端而不至于过载。服务器接收到太多 TestAuth 消息,并且当它们接收到 Fetch 或 Store 消息时,会花费太多时间遍历目录层次结构。因此,AFS 设计者面临着一个问题:
如何设计可扩展的文件协议 应如何重新设计协议以最大限度地减少服务器交互的数量,即如何减少 TestAuth 消息的数量?此外,他们如何设计协议以使这些服务器交互高效?通过解决这两个问题,新协议将产生更具可扩展性的 AFS 版本。
3 AFSv2
3.1 基本介绍
AFSv2 引入了回调的概念,以减少客户端/服务器交互的次数。回调只是服务器对客户端的一个承诺,即当客户端缓存的文件被修改时,服务器将通知客户端。在系统中添加这种状态后,客户端就不再需要联系服务器来了解缓存文件是否仍然有效。相反,它会假定文件是有效的,直到服务器告诉它否则;请注意轮询与中断之间的类比。
AFSv2 还引入了**文件标识符(FID)**的概念(类似于 NFS 文件句柄),而不是路径名来指定客户端感兴趣的文件。AFS 中的 FID 由一个卷标识符、一个文件标识符和一个 “唯一标识符 “组成(以便在删除文件时重复使用卷和文件标识符)。因此,客户端不再向服务器发送整个路径名,并让服务器遍历路径名以找到所需的文件,而是逐步遍历路径名,缓存结果,并希望减少对服务器的负载。
例如,如果客户端访问文件/home/zfhe/notes.txt
,而 home
是挂载在 /
上的 AFS 目录(即 /
是本地根目录,但 home
及其子目录在 AFS 中),客户端将首先获取 home
的目录内容,将其放入本地磁盘缓存,并在 home
上设置回调。然后,客户机将取回 zfhe
目录,将其放入本地磁盘缓存,并在 zfhe
上设置回调。最后,客户端会获取notes.txt
,将这个常规文件缓存到本地磁盘,并设置回调,最后向调用应用程序返回一个文件描述符。这个过程如下图所示。
不过,与 NFS 的主要区别在于,每次获取目录或文件时,AFS 客户端都会与服务器建立回调,从而确保服务器会通知客户端其缓存状态的变化。这样做的好处显而易见:虽然对/home/zfhe/notes.txt
的首次访问会产生许多客户端-服务器信息(如上所述),但同时也会为所有目录以及notes.txt
文件建立回调,因此后续访问完全是本地操作,根本不需要与服务器交互。因此,在客户端缓存文件的常见情况下,AFS 的行为几乎与本地磁盘文件系统相同。如果访问一个文件不止一次,那么第二次访问的速度应该与本地访问文件的速度一样快。
3.2 缓存一致性
缓存一致性不是万能的 在讨论分布式文件系统时,很多人都会提到文件系统提供的缓存一致性。然而,这种基本一致性并不能解决多个客户端访问文件的所有问题。例如,如果你正在建立一个代码库,有多个客户端执行代码的检入和检出,你就不能简单地依赖底层文件系统来为你完成所有工作;相反,你必须使用显式文件级锁定,以确保在发生这种并发访问时发生 “正确 “的事情。事实上,任何真正关心并发更新的应用程序都会增加额外的机制来处理冲突。基本一致性主要适用于临时使用,也就是说,当用户登录到不同的客户端时,他们希望在客户端上显示其文件的合理版本。如果对这些协议抱有更高的期望,就会让自己陷入失败、失望和充满泪水的沮丧之中。
当我们讨论 NFS 时,我们考虑了缓存一致性的两个方面:更新可见性和缓存陈旧性。
- 对于更新可见性,问题是:服务器何时会使用新版本的文件进行更新?
- 对于缓存陈旧性,问题是:一旦服务器有了新版本,客户端多久才能看到新版本而不是旧的缓存副本?
由于回调和全文件缓存,AFS 提供的缓存一致性很容易描述和理解。有两个重要的情况需要考虑:不同机器上的进程之间的一致性,以及同一机器上的进程之间的一致性。
在不同的计算机之间,AFS 使更新在服务器上可见,并在同一时间(即更新的文件关闭时)使缓存的副本失效。客户端打开一个文件,然后写入(可能重复)。当它最终关闭时,新文件将刷新到服务器(因此可见)。此时,服务器会“中断”任何具有缓存副本的客户端的回调;**中断是通过联系每个客户端并通知它对文件的回调不再有效来完成的。**此步骤确保客户端将不再读取文件的过时副本;这些客户端上的后续打开将需要从服务器重新获取文件的新版本(并且还将用于在文件的新版本上重新建立回调)。
AFS 对同一台机器上的进程之间的这种简单模型进行了例外处理。在这种情况下,对文件的写入对其他本地进程立即可见(即,进程不必等到文件关闭才能查看其最新更新)。这使得使用单台机器的行为完全符合您的预期,因为此行为基于典型的 UNIX 语义。只有当切换到不同的机器时,你才能检测到更通用的AFS一致性机制。
有一个有趣的跨机器案例值得进一步讨论。具体来说,在不同机器上的进程同时修改文件的罕见情况下,AFS 自然会采用所谓的“最后写入者获胜”方法(也许应该称为“最后关闭者获胜”)。具体来说,最后调用 close()
的客户端将最后更新服务器上的整个文件,因此将成为“获胜”文件,即保留在服务器上供其他人查看的文件。结果是一个由一个客户端或另一个客户端完整生成的文件。请注意与 NFS 等基于块的协议的区别:在 NFS 中,当每个客户端更新文件时,各个块的写入可能会被刷新到服务器,因此服务器上的最终文件可能是两个客户端更新的混合体。在许多情况下,这种混合文件输出没有多大意义,即想象一下 JPEG 图像被两个客户端分段修改;由此产生的写入组合不太可能构成有效的 JPEG。
下图中显示了其中一些不同场景的时间线。这些列显示了 $Client_1$ 上的两个进程($P_1$ 和 $P_2$)及其缓存状态、$Client_2$ 上的一个进程 ($P_3$) 及其缓存状态以及服务器 (Server) 的行为,所有这些都在一个名为 $F$ 的文件上进行操作。对于服务器来说,图中只是显示了左边操作完成后的文件内容。
3.3 崩溃恢复
从上面的描述中,你可能会感觉到崩溃恢复比 NFS 更复杂。你是对的。例如,假设服务器 (S) 在短时间内无法与客户端(C1) 取得联系,比如客户端 C1 正在重启。当 C1 不可用时,S 可能已尝试向其发送一条或多条回调召回消息;例如,假设 C1 在本地磁盘上缓存了文件 F,然后 C2(另一个客户端)更新了 F,从而导致 S 向所有缓存该文件的客户端发送消息,要求它们从本地缓存中删除该文件。由于 C1 在重启时可能会错过这些关键信息,因此在重新加入系统时,C1 应将其所有缓存内容视为可疑内容。因此,在下一次访问文件 F 时,C1 应首先询问服务器(通过 TestAuth 协议消息)其缓存的文件 F 副本是否仍然有效;如果有效,C1 可以使用它;如果无效,C1 应从服务器获取更新的版本。
服务器崩溃后的恢复也比较复杂。由此产生的问题是,回调是保存在内存中的;因此,当服务器重启时,它不知道哪台客户机拥有哪些文件。因此,服务器重启后,服务器的每个客户端都必须意识到服务器已经崩溃,并将其所有缓存内容视为可疑内容,并且(如上所述)在使用文件之前重新确定其有效性。
因此,服务器崩溃是一件大事,因为必须确保每个客户端都能及时意识到服务器崩溃,否则客户端就有可能访问过期文件。实现这种恢复的方法有很多,例如,当服务器重新启动并运行时,让服务器向每个客户端发送一条消息(说 “不要相信你的缓存内容!"),或者让客户端定期检查服务器是否还活着(即所谓的 “心跳消息”)。正如你所看到的,建立一个可扩展性更强、更合理的缓存模型是有代价的;在 NFS 中,客户端几乎不会注意到服务器崩溃。
3.4 可扩展性和性能
采用新协议后,对 AFSv2 进行了测量,发现其可扩展性远远超过原始版本。事实上,每台服务器可以支持大约 50 个客户端(而不是 20 个)。另一个好处是,客户端的性能往往非常接近本地性能,因为在普通情况下,所有文件访问都是本地的;文件读取通常会进入本地磁盘缓存(也可能进入本地内存)。只有在客户端创建新文件或写入现有文件时,才需要向服务器发送存储信息,从而用新内容更新文件。
我们还可以通过比较常见的文件系统访问情况和 NFS 来了解 AFS 的性能。下图显示了定性比较的结果。
在图中,我们分析了不同大小文件的典型读写模式。小文件有 $N_s$ 个块;中等文件有 $N_m$ 个块;大文件有 $N_L$ 个块。我们假定,小型和中型文件适合放在客户端内存中;大型文件适合放在本地磁盘上,但不适合放在客户端内存中。
为便于分析,我们还假设,通过网络访问远程服务器的文件块需要 $L_{net}$ 时间单位。访问本地内存需要 $L_{mem}$,访问本地磁盘需要 $L_{disk}$。一般假设是$L_{net} > L_{disk} > L_{mem}$。
最后,我们假设对文件的首次访问不在任何缓存中发生。如果相关缓存有足够的容量容纳文件,我们假设对文件的后续访问(即 “重读”)会在缓存中命中。
图中各列显示了特定操作(如小文件顺序读取)在 NFS 或 AFS 上大致花费的时间。最右边一列显示的是 AFS 与 NFS 的比例。
我们得出以下结论。首先,在许多情况下,每个系统的性能大致相当。例如,在首次读取文件时(如工作负载 1、3、5),从远程服务器获取文件的时间占主导地位,而且在两个系统上的时间相似。在这种情况下,您可能会认为 AFS 的速度会慢一些,因为它必须将文件写入本地磁盘;但是,本地(客户端)文件系统缓存会对这些写入进行缓冲,因此上述成本很可能是隐性的。同样,你可能会认为 AFS 从本地缓存副本读取文件的速度会更慢,这也是因为 AFS 将缓存副本存储在磁盘上。然而,AFS 也能从本地文件系统缓存中获益;AFS 上的读取可能会在客户端内存缓存中进行,性能与 NFS 类似。
其次,在大文件顺序重读(工作负载 6)过程中出现了一个有趣的差异。由于 AFS 有一个很大的本地磁盘缓存,当文件再次被访问时,它会从本地磁盘缓存中访问文件。相比之下,NFS 只能缓存客户端内存中的数据块;因此,如果重新读取大文件(即大于本地内存的文件),NFS 客户端将不得不从远程服务器重新获取整个文件。因此,假设远程访问确实比本地磁盘慢,在这种情况下,AFS 比 NFS 快 $\frac{L_{net}}{L_{disk}}$ 的系数。我们还注意到,在这种情况下,NFS 会增加服务器负载,这也会对扩展性产生影响。
第三,我们注意到,顺序写入(新文件)在两个系统上的执行情况类似(工作负载 8、9)。在这种情况下,AFS 会将文件写入本地缓存副本;当文件关闭时,AFS 客户端会根据协议强制将文件写入服务器。NFS 会在客户端内存中缓冲写入,也许会因为客户端内存压力而强制将某些块写入服务器,但在文件关闭时肯定会将它们写入服务器,以保持 NFS 的关闭时刷新一致性。你可能会认为 AFS 的速度会更慢,因为它会将所有数据写入本地磁盘。但是,你要知道,它是在向本地文件系统写入数据;这些写入的数据首先提交到页面缓存,然后才(在后台)提交到磁盘,因此 AFS 可以利用客户端操作系统内存缓存基础架构的优势来提高性能。
第四,我们注意到 AFS 在顺序文件覆盖(工作负载 10)上的性能更差。到目前为止,我们假定写入的工作负载也在创建新文件;在这种情况下,文件存在,然后被覆盖写入。对于 AFS 来说,重写可能是一种特别糟糕的情况,因为客户端首先会完整地获取旧文件,然后再将其重写。与此相反,NFS 只需覆盖块,从而避免了最初的(无用的)读取。
最后,访问大文件中一小部分数据的工作负载在 NFS 上的表现要比 AFS 好得多(工作负载 7、11)。在这些情况下,AFS 协议会在打开文件时获取整个文件;但不幸的是,只会执行少量的读取或写入操作。更糟糕的是,如果文件被修改,整个文件都会被写回服务器,对性能的影响会加倍。NFS 作为基于块的协议,执行的 I/O 与读取或写入的大小成正比。总之,我们看到 NFS 和 AFS 的假设不同,因此实现的性能结果也不同,这并不奇怪。这些差异是否重要,始终是一个工作负载问题。
工作负载的重要性 评估任何系统的一大挑战是工作负载的选择。由于计算机系统的使用方式多种多样,因此有多种工作负载可供选择。存储系统设计人员应如何确定哪些工作负载是重要的,以便做出合理的设计决策?
AFS 的设计者根据衡量文件系统使用方式的经验,做出了某些工作负载假设;特别是,他们假设大多数文件不经常共享,并且整体上按顺序访问。考虑到这些假设,AFS 设计就非常有意义了。
然而,这些假设并不总是正确的。例如,假设有一个应用程序定期将信息附加到日志中。这些小日志写入会将少量数据添加到现有的大文件中,这对于 AFS 来说是相当有问题的。还存在许多其他困难的工作负载,例如事务数据库中的随机更新。
4 AFS:其他改进
就像我们在引入 Berkeley FFS(添加了符号链接和许多其他功能)时看到的那样,AFS 的设计者在构建系统时抓住了机会添加了许多功能,使系统更易于使用和管理。例如,AFS 为客户端提供了真正的全局命名空间,从而确保所有文件在所有客户端计算机上都以相同的方式命名。相比之下,NFS 允许每个客户端以他们喜欢的任何方式安装 NFS 服务器,因此只有按照约定(以及大量的管理工作),文件才能在客户端之间以类似的方式命名。
AFS 还非常重视安全性,并采用了对用户进行身份验证的机制,并确保如果用户愿意,可以将一组文件保持私有。相比之下,NFS 多年来对安全性的支持相当原始。
AFS 还包括用于灵活的用户管理访问控制的设施。因此,在使用 AFS 时,用户可以很好地控制谁可以访问哪些文件。 NFS 与大多数 UNIX 文件系统一样,对此类共享的支持要少得多。
最后,如前所述,AFS 添加了一些工具,使系统管理员能够更简单地管理服务器。在系统管理方面,AFS 遥遥领先于该领域。