关键问题:如何设计可扩展的文件协议

如何重新设计协议,使服务器交互次数最少?也就是说,怎样减少 TestAuth 消息的数量?进一步,怎样让这些服务器交互变得高效?同时解决这两个问题,就能得到一个可扩展得多的 AFS。

原文:HOW TO DESIGN A SCALABLE FILE PROTOCOL. How should one redesign the protocol to minimize the number of server interactions, i.e., how could they reduce the number of TestAuth messages? Further, how could they design the protocol to make these server interactions efficient?

Andrew 文件系统(AFS)于 20 世纪 80 年代诞生于卡内基梅隆大学(CMU),由 M. Satyanarayanan("Satya")教授领导。它的首要目标很简单:规模(scale)——怎样设计一个分布式文件系统,让一台服务器支持尽可能多的客户端?

影响可扩展性的因素很多,最重要的是客户端与服务器之间的协议。上一章的 NFS 协议迫使客户端定期向服务器确认缓存内容是否改变;每次检查都消耗服务器资源(CPU、网络带宽),频繁检查就限制了一台服务器能服务的客户端数量。AFS 与 NFS 的另一个不同是:它从一开始就把合理的用户可见行为当作一等公民。NFS 的缓存一致性依赖于客户端缓存超时等底层实现细节,很难描述;AFS 的缓存一致性简单易懂:打开文件时,客户端通常会拿到服务器上最新的一致副本。

核心概念速览

整文件缓存

open 时把整个文件取到客户端本地磁盘;之后的 read/write 都在本地完成,close 时若修改过再整个传回。

Venus / Vice

Venus 是 AFS 的客户端代码,Vice 是服务器端。

TestAuth(AFSv1)

每次打开缓存文件都问服务器"文件变了没"——太多了,成为瓶颈。

回调(callback)

服务器承诺:你缓存的文件被修改时我会通知你。把"轮询"变成"中断"。

文件标识符(FID)

卷 ID + 文件 ID + uniquifier,取代完整路径名;由客户端逐级查找路径并缓存。

关闭时更新 + 中断回调

close 把新版本存到服务器,同时服务器"打断"其他客户端的回调,使其缓存失效。

最后写者胜

两台机器同时修改文件,最后 close 的那个整份文件留在服务器上(其实是"最后关闭者胜")。

有状态的代价

回调保存在服务器内存里;服务器或客户端崩溃后,都要把缓存视为可疑并重新验证。

AFS 第 1 版:整文件缓存

我们讨论两个版本:第一版(称为 AFSv1,原名 ITC 分布式文件系统)已有基本设计,但扩展性不达预期,于是重新设计出最终协议(AFSv2,简称 AFS)。

所有版本 AFS 的基本信条之一是在访问文件的客户端的本地磁盘上做整文件缓存(whole-file caching):

客户端(Venus) 应用 本地磁盘缓存 notes.txt(整个文件) read()/write() 全在本地,无网络通信 服务器(Vice) /home/remzi/notes.txt open():Fetch 整个文件 close():若修改过,Store 整个文件
图 50-A:整文件缓存。注意与 NFS 的对比:NFS 缓存的是块(虽然也可以缓存一个文件的所有块),而且缓存在客户端内存而不是本地磁盘。

应用第一次调用 open() 时,AFS 客户端代码(设计者称之为 Venus)向服务器发 Fetch 协议消息,传递目标文件的完整路径名(如 /home/remzi/notes.txt)。服务器(称为 Vice)遍历路径名,找到文件,把整个文件发回客户端;客户端把它存进本地磁盘上的缓存。之后应用的 read()/write() 被重定向到本地文件系统中的那份副本,完全不需要网络通信,很快。由于这些调用就像访问本地文件系统一样,被访问过的块还可能被缓存在客户端内存里,所以 AFS 也利用客户端内存缓存本地磁盘上的块。最后 close() 时,如果文件被修改过(被以写方式打开),客户端用 Store 消息把整个新版本和路径名发回服务器永久保存。

下次访问该文件时,AFSv1 会更高效:客户端先用 TestAuth 消息问服务器文件是否变了;没变就直接用本地缓存副本,省去网络传输。注意这个早期版本只缓存文件内容,目录只保存在服务器上。

AFSv1 协议消息作用
TestAuth测试文件是否改变(用于验证缓存项)
GetFileStat获取文件的 stat 信息
Fetch获取文件内容
Store把这个文件存到服务器上
SetFileStat设置文件的 stat 信息
ListDir列出目录内容

表:原书 Figure 50.1,AFSv1 协议要点。

第 1 版的问题

为了找出问题所在,AFS 的设计者花了大量时间测量他们的原型。这种实验是好事:测量是理解系统如何工作、如何改进的关键。他们发现了两个主要问题:

另外还有两个问题:服务器之间负载不均衡——通过引入卷(volume)解决,管理员可以在服务器之间移动卷来平衡负载;服务器为每个客户端使用一个独立进程,导致上下文切换等开销——AFSv2 改用线程构建服务器来解决。本章聚焦前两个限制可扩展性的协议问题。它们让服务器 CPU 成为系统瓶颈:每台服务器只能服务 20 个客户端,再多就过载。

David Patterson(RISC 和 RAID 的知名学者)总是鼓励学生:在构建新系统解决某个问题之前,先测量现有系统、证明问题确实存在。用实验证据而不是直觉,能把系统构建变成更科学的事业;还有一个附带好处:它迫使你在新系统完成之前就想清楚如何测量。等你真正做出新系统时,你既有证据表明自己在解决真实问题,又已经有了现成的方法证明新系统确实优于现状。

AFS 第 2 版:回调与 FID

AFSv2 引入了回调(callback)来减少客户端与服务器的交互。回调就是服务器对客户端的一个承诺:当客户端缓存的文件被修改时,服务器会通知它。有了这份(保存在服务器上的)状态,客户端不再需要联系服务器确认缓存文件是否有效,而是假设它有效,直到服务器告诉它不是。这正好对应轮询(polling)与中断(interrupt)的区别(回想第 36 章 I/O 设备)。

AFSv1 / NFS:轮询 客户端服务器 变了吗?没变 变了吗?没变 变了吗?没变 变了吗?没变 AFSv2:回调(类似中断) 客户端服务器 Fetch 文件 + 登记回调 记下:C1 缓存了 F 打开、读、写、再打开…… 全都在本地,一条消息都不发 别人更新了 F 打断回调:你的 F 失效了
图 50-B:轮询 vs 回调。服务器为此要记住"谁缓存了什么"——AFS 的服务器是有状态的。

AFSv2 还引入了文件标识符(file identifier,FID)(类似 NFS 的文件句柄),取代路径名来指定客户端感兴趣的文件。AFS 的 FID 由卷标识符、文件标识符和一个"uniquifier"组成(uniquifier 让文件删除后卷 ID 和文件 ID 可以被重用)。于是,客户端不再把整条路径交给服务器去走,而是自己逐级遍历路径,缓存每一步的结果,从而减轻服务器负担。

例如客户端访问 /home/remzi/notes.txt,其中 home 是挂载在 / 上的 AFS 目录(/ 是本地根目录,home 及其子项在 AFS 中)。客户端先 Fetch home 的目录内容放进本地磁盘缓存并在 home 上建立回调;再 Fetch 目录 remzi,缓存并建立回调;最后 Fetch 普通文件 notes.txt,缓存到本地磁盘、建立回调,然后把文件描述符返回给应用。下面的模拟器复刻原书 Figure 50.2(从 home 已缓存开始),并可切换到 AFSv1 对比:

模拟器

打开 /home/remzi/notes.txt:AFSv1 vs AFSv2

客户端 C1 两次打开、读取、关闭同一个文件。数一数每个版本发了多少条消息、服务器做了多少次路径查找;再让别人在两次打开之间修改 notes.txt(服务器会打断回调),看第二次打开要重新取什么。

与 NFS 的关键区别在于:每次取目录或文件时,AFS 客户端都会与服务器建立回调,确保服务器在其缓存状态改变时通知它。第一次访问 /home/remzi/notes.txt 会产生多条消息,但也为所有目录和 notes.txt 建立了回调,之后的访问完全在本地进行,不需要任何服务器交互。在文件已缓存在客户端这一常见情况下,AFS 的表现几乎和本地磁盘文件系统一样:第二次访问一个文件应该和访问本地文件一样快。

FID = 卷标识符 文件标识符 uniquifier 文件删除后允许重用前两者 对比 NFS 文件句柄:卷 ID + inode 号 + 世代号
图 50-C:AFS 的文件标识符。

缓存一致性

讨论 NFS 时我们考虑过两方面:更新可见性(服务器何时拿到新版本?)和缓存陈旧(服务器有了新版本后,多久客户端才能看到新版本而不是旧的缓存副本?)。有了回调和整文件缓存,AFS 的一致性很容易描述,分两种情况:

还有一个值得讨论的跨机器情况:在罕见的情况下,不同机器上的进程同时修改一个文件,AFS 自然地采用最后写者胜(last writer wins)的方法(也许应该叫"最后关闭者胜"):谁最后调用 close(),谁就最后把整个文件更新到服务器上,成为"胜出"的文件。结果是一个完全由某一个客户端生成的文件。对比 NFS 这样基于块的协议:各个客户端更新文件时,单个块的写可能陆续被刷到服务器,最终服务器上的文件可能是两个客户端更新的混合。很多情况下混合的文件毫无意义——想象一张 JPEG 图片被两个客户端分块修改,混合的结果多半不是合法的 JPEG。

AFS:整文件,最后关闭者胜 C1 写的版本 C2 写的版本 服务器最终 C1 最后 close 完整来自某一个客户端 NFS:按块刷写,可能混合 C1 写的版本 C2 写的版本 服务器最终 块交错刷到服务器 两者混合(比如坏掉的 JPEG)
图 50-D:并发修改时 AFS 与 NFS 的不同结局。

下面的模拟器默认载入原书 Figure 50.3 的完整时间线:Client1 上有进程 P1、P2,Client2 上有进程 P3,所有操作都针对文件 F。服务器一列显示该行操作完成后服务器上的文件内容;被删除线划掉的缓存表示回调已被打断(失效)。你可以编辑操作序列,也可以切换到 NFS 语义,用同一串操作做对比。

模拟器

AFS 缓存一致性时间线(Figure 50.3)

每一步显示两个客户端的缓存内容、服务器上的文件、每次 read 读到的值和发出的协议消息。试试:本地进程能立刻看到写;远端要等 close;两个客户端同时写时谁胜出;客户端宕机错过回调、服务器崩溃丢失回调时会怎样。
预设:
语法:P1 open · P1 write A · P1 read · P1 close(P1、P2 在 Client1,P3、P4 在 Client2)· C1 down / C1 up · S crash · wait 3。每个操作耗时 1 秒。

逐行读 Figure 50.3,理解每次 read 为什么返回那个值:

讨论分布式文件系统时,人们常大谈它提供的缓存一致性。但这种基础一致性并不能解决多客户端访问文件的所有问题。比如你在构建一个代码仓库,多个客户端同时签入、签出代码,就不能指望底层文件系统替你做完所有事,而必须使用显式的文件级锁,确保并发访问时发生"正确"的事。事实上,任何真正在意并发更新的应用都会加上额外机制来处理冲突。本章和上一章描述的基础一致性主要适用于日常使用——用户换一台客户端登录时,希望看到自己文件的某个合理版本。对这些协议期望更多,只会让自己失望、沮丧、泪流满面。

交错枚举:两个客户端的所有执行顺序

类似 AFS 模拟器 afs.py 作业的第 4–6 题:两个客户端各执行三步,一共有 C(6,3) = 20 种交错顺序。调度串中 0 表示让 Client1(P1)执行下一步、1 表示让 Client2(P3)执行下一步(与 afs.py -S 的记法相同)。先预测,再点"枚举"。点击任意一行,可以把它载入上面的时间线模拟器逐步查看。

模拟器

交错枚举器

工作负载 1:Client1 执行 open → write 1 → close,Client2 执行 open → read → close,文件初值 0,统计 Client2 读到的值。工作负载 2:两个客户端都 open → write → close(分别写 X、Y),统计服务器上的最终内容。

崩溃恢复

从上面的描述你可能感觉到,AFS 的崩溃恢复比 NFS 复杂。没错。

客户端重启:可能错过了回调中断 ① C1 缓存了 F(持有回调) ② C1 正在重启,联系不上 ③ C2 更新 F,服务器向 C1 发回调中断…… 消息没送到 ④ C1 重新加入:把全部缓存视为可疑 ⑤ 下次访问 F 先发 TestAuth: 仍有效就用,否则重新取新版本 服务器重启:回调表在内存里丢了 ① 服务器记着"C1、C2 缓存了 F" ② 服务器崩溃重启:不知道谁缓存了什么 ③ 每个客户端必须及时意识到服务器崩溃过, 把缓存视为可疑、使用前重新验证 通知方式:服务器重启后给每个客户端发消息 ("别相信你的缓存!"),或客户端定期 发心跳(heartbeat)检查服务器是否还活着
图 50-E:回调是服务器端状态,崩溃恢复因此变得复杂。相比之下,NFS 客户端几乎察觉不到服务器崩溃。

设想服务器 S 有一小段时间联系不上客户端 C1(比如 C1 正在重启)。这期间 S 可能向 C1 发过一条或多条回调撤销消息:假设 C1 本地磁盘缓存了文件 F,然后 C2 更新了 F,于是 S 向所有缓存了 F 的客户端发消息,让它们从本地缓存中删掉 F。C1 在重启时可能错过这些关键消息,所以重新加入系统时,C1 应把所有缓存内容视为可疑:下次访问 F 时,先用 TestAuth 问服务器缓存的副本是否仍然有效;有效就用,否则从服务器取新版本。

服务器崩溃后的恢复也更复杂:回调保存在内存里,服务器重启后不知道哪台客户端缓存了哪些文件。因此服务器重启后,每个客户端都必须意识到服务器崩溃过,并把缓存内容视为可疑、在使用前重新确认有效性。服务器崩溃是件大事:必须确保每个客户端及时知道这次崩溃,否则就有客户端访问陈旧文件的风险。实现方式很多,比如服务器恢复后给每个客户端发消息("别相信你的缓存内容!"),或者客户端定期检查服务器是否活着(心跳消息)。可以看到,构建更可扩展、更合理的缓存模型是有代价的;而用 NFS 时,客户端几乎注意不到服务器崩溃。在上面的时间线模拟器里,点"客户端宕机错过回调"或"服务器崩溃丢失回调",再取消"恢复后把缓存视为可疑",就能看到后果。

AFSv2 的规模与性能

新协议到位后,测量发现 AFSv2 的可扩展性大大提高:每台服务器可以支持约 50 个客户端(而不是 20 个)。另一个好处是客户端性能常常接近本地性能:常见情况下所有文件访问都是本地的,读文件通常访问本地磁盘缓存(也可能是本地内存)。只有客户端创建新文件或写已有文件时,才需要向服务器发 Store 消息。

原书 Figure 50.4 用简单的定性分析比较了常见访问场景下 AFS 与 NFS 的性能。小文件有 Ns 个块,中等文件 Nm 个块,大文件 NL 个块;假设小、中等文件能放进客户端内存,大文件能放进本地磁盘但放不进内存。通过网络访问远程服务器的一个块需要 Lnet,访问本地内存需要 Lmem,访问本地磁盘需要 Ldisk,一般假设 Lnet > Ldisk > Lmem。第一次访问文件不命中任何缓存;之后的访问("重读")如果相应缓存装得下,就会命中。改改参数,看看比值:

计算器

AFS vs NFS 工作负载对比(Figure 50.4)

表中"公式"两列是原书的结果,"数值"列按你输入的参数计算;最右列是 AFS/NFS 的比值(< 1 表示 AFS 更快,标绿;> 1 表示 AFS 更慢,标红)。

几点观察(原书):

  1. 很多情况下两者性能大致相当。例如第一次读文件(工作负载 1、3、5),从远程服务器取文件的时间占主导,两个系统差不多。你可能觉得 AFS 要把文件写到本地磁盘会更慢,但这些写会被本地文件系统缓存吸收;同样,从本地磁盘缓存副本读看似更慢,但 AFS 同样受益于本地文件系统的内存缓存,读很可能命中客户端内存,性能与 NFS 相近。
  2. 大文件顺序重读(工作负载 6)差异明显:AFS 有很大的本地磁盘缓存,再次访问时从那里读;NFS 只能在客户端内存里缓存块,比内存大的文件重读时必须从服务器重新取整个文件。所以 AFS 快 Lnet/Ldisk 倍(假设远程访问确实比本地磁盘慢)。而且 NFS 在这种情况下增加了服务器负载,也影响扩展性。
  3. 新文件的顺序写(工作负载 8、9)两者相近:AFS 写到本地缓存副本,关闭时把数据推给服务器;NFS 把写缓冲在客户端内存中,可能因内存压力提前把一些块写到服务器,但关闭时一定会写,以保持 flush-on-close 一致性。AFS 写本地文件系统,同样先进入页缓存、稍后才在后台写盘,从而受益于客户端操作系统的内存缓存。
  4. 顺序覆写(工作负载 10)AFS 更差:前面的写负载都是创建新文件;这里文件已存在并被覆写。AFS 客户端先把旧文件整个取回来,然后又整个覆盖掉;NFS 只是覆写块,省掉了开头那次(无用的)读。(假设 NFS 的写都是块大小且块对齐的,文件也不是用 O_TRUNC 打开的——否则 AFS 的 open 不会去取即将被截断的文件内容。)
  5. 只访问大文件中一小部分数据(工作负载 7、11)NFS 好得多:AFS 在打开时取回整个文件,却只做了一次小读或小写;如果文件被修改,还要把整个文件写回服务器,性能影响翻倍。NFS 作为基于块的协议,I/O 量与读写大小成正比。

总的来说,NFS 和 AFS 做了不同的假设,自然得到不同的性能结果。这些差异重不重要,一如既往,取决于工作负载。

评估任何系统的一个难题是选择工作负载。计算机系统的用法千差万别,存储系统设计者该如何判断哪些工作负载重要?AFS 的设计者根据他们测量文件系统使用情况的经验做了一些假设:大多数文件不常被共享,并且会被从头到尾顺序访问。在这些假设下,AFS 的设计非常合理。但假设并不总成立:比如一个应用周期性地往日志里追加少量信息,这种向大文件追加小数据的写对 AFS 很不友好;事务数据库中的随机更新也是难题。

AFS 的其他改进

就像 Berkeley FFS 趁机加入了符号链接等新特性,AFS 的设计者也借机加入了许多让系统更好用、更好管理的特性:

真正的全局命名空间

所有文件在所有客户端上的名字都一样。NFS 允许每个客户端随意挂载服务器,只能靠约定和大量管理工作让各处命名一致。

认真对待安全

内置用户认证机制,能保证文件在用户需要时保持私密。NFS 在很多年里对安全的支持都很原始。

灵活的访问控制

用户可管理的访问控制列表(ACL),精确控制谁能访问哪些文件;NFS 和大多数 UNIX 文件系统对这类共享支持较少。

简化服务器管理

提供让管理员更容易管理服务器的工具(如在服务器间迁移卷)。在系统管理方面,AFS 领先业界好几光年。

先预测再验证

在"打开 notes.txt"模拟器中,打开次数设为 3,比较 AFSv1 与 AFSv2 的消息总数和服务器路径查找次数。
看答案
AFSv1:第 1 次 Fetch(服务器走 3 级路径),之后每次 open 都发 TestAuth(服务器每次再走 3 级路径),共 3 条消息、9 次路径查找。AFSv2:第 1 次发 2 条 Fetch(remzi、notes.txt,服务器各查 1 次),之后回调都有效,一条消息都不发:共 2 条消息、2 次查找。两者差距随打开次数线性增长——这就是 AFSv2 能把每台服务器的客户端数从 20 提到 50 的原因。
AFSv2 中选"别人修改了 notes.txt"。预测第 2 次打开会发几条消息?如果改成"别人修改了目录 remzi"呢?
看答案
notes.txt 被修改:服务器打断了 C1 对 notes.txt 的回调,home 和 remzi 的回调仍有效,所以只重新 Fetch notes.txt,1 条消息。remzi 被修改:只重新 Fetch 目录 remzi(从中重新找到 notes.txt 的 FID),notes.txt 本身的回调仍有效,直接打开本地副本,也是 1 条消息。
载入原书 Figure 50.3,在第 12 行之前暂停:预测 P3 的 read 会读到什么?第 15 行 P1 close 之后,Client2 的缓存发生了什么?
看答案
P3 读到 A:P1 写的 B 只在 Client1 的缓存里,服务器还是 A,P3 打开时取回的是 A。第 15 行 P1 close 把 B 存到服务器,服务器打断 Client2 的回调,Client2 缓存中的 A 被划掉(失效);P3 下次 open 时重新取回 B。
把 Figure 50.3 切换成 NFS 语义,T 分别设为 3、4、5。预测第 17 行(t=16)和第 26 行(t=25)的 P3 read 各读到什么?与 AFS 相同吗?
看答案
T=3:两次都与 AFS 相同(B、D)——P3 在 t=22 关闭时 WRITE 的回复刷新了属性缓存,t=25 read 时恰好过了 3 秒,GETATTR 发现 P1 在 t=23 刷写的新版本。T=4:第 26 行读到 C(陈旧读):属性缓存还没过期,客户端直接用缓存里的 C。T=5:连第 17 行也读到 A(陈旧读):P3 在 t=12 的 READ 刷新了属性缓存,t=16 还没过期。同一串操作,结果取决于超时参数和时间间隔——这就是"实现细节定义语义"。AFS 靠回调在 P1 close 的那一刻就让 Client2 的缓存失效,不依赖任何超时。
在交错枚举器里选工作负载 1。预测:AFS 下 20 种交错中 Client2 有几种读到 1?NFS(T=0)呢?
看答案
AFS 下只有 1 种(调度 000111:Client1 完全关闭之后 Client2 才打开)。因为 AFS 的读取看的是 open 时拿到的版本,只有 Client1 的 close 早于 Client2 的 open 才能读到 1。NFS 在 read 时检查(Client2 在 read 之前没有缓存数据,READ 总是取服务器当前内容),只要 Client1 的 close 早于 Client2 的 read 就能读到 1,共 4 种:000111、001011、010011、100011。
工作负载 2(两个客户端都写):预测 AFS 下服务器最终内容由什么决定?
看答案
只由两个 close 的先后决定:谁最后 close,服务器上就是谁写的整份文件(最后写者胜/最后关闭者胜)。open 和 write 的先后都不重要。枚举结果中 X、Y 各占 10 种:最后一步是 Client1 执行(调度以 0 结尾)时 X 胜,否则 Y 胜。

动手实验

afs_sim.c 是一个与本页时间线模拟器逻辑相同的小型 C 模拟器:它逐行执行 Figure 50.3 的操作并打印表格,然后枚举两个作业工作负载的全部 20 种交错。

cd labs/50-afs && make && ./afs_sim

示例输出(本机真实运行结果;程序是确定性的,你的输出应当相同):

=== Figure 50.3: AFS cache consistency timeline (~X = callback broken) ===
#   P1            P2            C1     P3            C2     Server
1   open(F)                     -                    -      -     
2   write(A)                    A                    -      -     
3   close()                     A                    -      A     
4                 open(F)       A                    -      A     
5                 read() -> A   A                    -      A     
6                 close()       A                    -      A     
7   open(F)                     A                    -      A     
8   write(B)                    B                    -      A     
9                 open(F)       B                    -      A     
10                read() -> B   B                    -      A     
11                close()       B                    -      A     
12                              B      open(F)       A      A     
13                              B      read() -> A   A      A     
14                              B      close()       A      A     
15  close()                     B                    ~A     B     
16                              B      open(F)       B      B     
17                              B      read() -> B   B      B     
18                              B      close()       B      B     
19                              B      open(F)       B      B     
20  open(F)                     B                    B      B     
21  write(D)                    D                    B      B     
22                              D      write(C)      C      B     
23                              D      close()       C      C     
24  close()                     D                    ~C     D     
25                              D      open(F)       D      D     
26                              D      read() -> D   D      D     
27                              D      close()       D      D     
messages: Fetch=4 Store=4 callback-breaks=3 TestAuth=0

=== Workload 1: client0 = open,write 1,close   client1 = open,read,close  (file starts as 0) ===
schedule  AFS  NFS(T=0)
000111    1    1
001011    0    1
001101    0    0
001110    0    0
010011    0    1
...
summary (20 interleavings):  AFS 0 x19  AFS 1 x1  NFS 0 x16  NFS 1 x4

=== Workload 2: client0 = open,write X,close   client1 = open,write Y,close  (file starts as 0) ===
schedule  AFS  NFS(T=0)
000111    Y    Y
001011    Y    Y
001101    Y    Y
001110    X    X
010011    Y    Y
...
summary (20 interleavings):  AFS X x10  AFS Y x10  NFS X x10  NFS Y x10

表中 ~A 表示该缓存的回调已被打断(原书中划掉的 A)。可以尝试修改程序开头的操作数组,或把 SUSPECT_ON_RECOVER 设为 0 并加入 C1 down / C1 up 操作,观察错过回调的后果。详见 labs/50-afs/README.md。

自测题

AFS 设计的首要目标是什么?
"the main goal of this project was simple: scale"。简单快速的崩溃恢复是 NFS 的目标;AFS 恰恰为了扩展性牺牲了崩溃恢复的简单性。
AFSv1 的两个主要协议问题是?
这两点让服务器 CPU 成为瓶颈,每台服务器只能服务 20 个客户端。负载不均(用卷解决)和每客户端一个进程(改用线程)是另外两个问题。
回调(callback)的本质是?
C 是轮询(AFSv1 的 TestAuth、NFS 的 GETATTR)。回调让客户端假定缓存有效,直到服务器说它无效。
Client1 上的 P1 打开 F 并写入 B(尚未关闭)。以下哪个进程此时读 F 能看到 B?
同一台机器上的进程立即看到写(UNIX 语义);其他机器要等 P1 close 把文件存到服务器后,重新 open 才能看到(Figure 50.3 第 10、13 行)。
两台机器上的进程同时修改 F,AFS 服务器上最终是?
最后写者胜(更准确说是最后关闭者胜),而且整个文件来自同一个客户端;块级混合是 NFS 可能出现的情况。
AFS 客户端重启后重新加入系统,应该怎么做?
重启期间可能错过了回调中断消息。原书:C1 should treat all of its cache contents as suspect,下次访问时先 TestAuth,有效就用,否则取新版本。
按照 Figure 50.4,哪种工作负载 AFS 明显比 NFS 快?
工作负载 6:AFS 从本地磁盘缓存读(NL·Ldisk),NFS 要重新从服务器取(NL·Lnet)。B、D 都是 AFS 更慢(比值 NL 和 2);A 两者相同。
为什么"周期性地向一个大日志文件追加少量数据"对 AFS 很不友好?
AFS 基于"文件很少共享、整体顺序访问"的工作负载假设设计;小的追加写违背了这个假设(参见工作负载 11:比值 2·NL)。

常见误区与小结

本章小结