图 1:本文封面,两套协议的写入在同一个文件上交汇。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。场景很常见:一台存储服务器上放着同一份数据盘,既用 NFS 挂给 Linux 计算节点,又用 SMB 共享给办公侧访问。问题是:同一个文件被两边同时读写,会出什么事?要答好这题,得先搞清楚两套协议各自怎么管锁、怎么管缓存。
NFS 一侧:锁是租约化的状态,缓存靠委托下放。
NFSv4(RFC 7530)把文件锁收进了协议本体:客户端可以对文件的字节范围加锁(byte-range lock),而且锁是有租约的——服务器为客户端持有的全部状态定义一个统一的租约期,客户端需要周期续租,租约过期不续,锁就可能被收回。这套设计是为了网络分区下的确定性:客户端掉线,锁不会永远挂死在那里。
更有意思的是委托(delegation):服务器可以把一个文件的读委托或写委托下放给某个客户端。拿到写委托的客户端被保证此刻没有其他客户端在读写该文件,于是可以放心地在本地缓存读写操作;一旦有别的客户端要做冲突操作,服务器通过回调路径通知原客户端收回委托。委托的本质,是把缓存一致性做成了协议内的一等公民。
SMB 一侧:锁和缓存更是绑在一起的。
SMB 的对应机制叫机会锁(oplock)。按 SMB2 协议文档,客户端在打开文件的 CREATE 请求里申明想要的 oplock 级别:二级(level II)、独占(exclusive)、批量(batch),新版本方言里还可以申请租约(lease)。机会锁不是客户端对服务器的命令,而是请求,由服务器视情况授予;它存在的意义,就是让客户端可以把数据缓存在本地,减少网络往返。当另一个客户端要打开同一文件时,服务器会「折断」(break)已有的 oplock:持锁客户端必须把缓存里的脏数据刷回服务器、降级自己的缓存级别,对方的打开才被放行。在 SMB 的世界里,「我能缓存什么」完全由「我持有什么锁」决定。
混挂的真正问题:两套机制互相看不见。
现在把两边拼到同一个后端文件系统上。NFS 服务进程维护的是 NFS 的锁表与委托,SMB 服务进程维护的是 SMB 的 oplock 表——它们是两个守护进程里的两套状态,底层文件系统通常并不负责把二者翻译成同一份视图。于是三个盲区依次出现。
第一,锁不互通。Linux 侧通过 NFS 拿到的字节范围锁,挡不住 SMB 侧客户端的写入,反过来也一样。两边都以为「我锁住了」,实际上谁也没锁住谁,两段写入的字节交错落盘,文件内容被撕成两半。
第二,缓存脏读。SMB 侧客户端拿着 oplock 在本地缓存了还没刷回的修改,NFS 侧读到的就是旧数据;反过来,NFS 侧拿到写委托后的本地修改,SMB 侧同样看不见。因为「收回委托」「折断 oplock」这类通知只在各自协议内部流转,跨协议的失效信号根本不存在。
第三,恢复语义断裂。断线重连、租约超时回收、oplock 降级,这些善后流程都是按单协议假设设计的;混挂之下,一边的恢复动作可能正踩着另一边未落盘的数据,破坏被放大。
图 2:双协议混挂架构与锁、缓存语义对比。
工程上怎么落地才不出事。
结论不是「不能混挂」,而是「不能让两边对同一文件同时写」。常见姿势有三种。其一,角色隔离:一侧只读(比如办公侧只看产出的报表文件),写路径只走一种协议。其二,目录切分:NFS 写的目录和 SMB 写的目录物理分开,互不染指。其三,应用层互斥:必须双写时,由上层应用用自己的分布式锁串行化,并让每次写入都直通落盘,绕过双方客户端缓存。评审混挂方案时先问三个问题:锁谁管、缓存谁失效、恢复谁兜底——答不上来的设计就是定时炸弹。
回到面试现场,建议的答题结构:先分别讲清 NFS 的字节范围锁、租约与委托,以及 SMB 的 oplock 级别与折断机制;再指出根因是两套状态互不可见;最后给出锁不互通、缓存脏读、恢复断裂三类后果与对应的工程规避方案。你们的数据盘有没有类似的双协议共享场景?是靠只读隔离还是应用层锁扛住的?欢迎说说你们的做法。