可恨又可爱的 NFS

最近在给公司生产环境共享文件服务器调整目录布局,之前在开发测试环境整过一次,跑了一个多月没啥问题,以为在生产环境依葫芦画瓢是小菜一碟,没想到栽了好几天费了老劲,然后重新认识了下 NFS,一悲一喜。

下面是我司的文件共享示意,”–>” 表示符号链接。 这套共享文件的目录布局有三个用意:

  1. Kubernetes YAML 里的 volumes 定义可以统一用 /shr/{{ cluster }}//{{ namespace }}/{{ svc }} 的写法,部署时自动对应到 NFS 服务器上各个环境独有的目录;
  2. 服务之间的文件共享通过服务端的 bind mount 实现,比如 /data/k8s-dev/nsA/svc2/images 实际是 /data/k8s-dev/nsA/svc1/images 的 bind mount,所以在 Kubernetes YAML 里只需要挂载“自己”的共享目录,方便配置维护、测试;
  3. 挂载点与物理存储位置分离;

开发环境(每个 cluster/namespace 都是独立一套目录结构):

/shr/
  |-- k8s-dev   --> /data/k8s-dev/
  `-- k8s-qe    --> /data/k8s-qe/

/data/k8s-dev/
   |-- nsA/
   |    |-- svc1/
   |    `-- svc2/
   `-- nsB/

生产环境(注意其实只有一套cluster/namespace 目录结构,其它 cluster、namespace 都是符号链接):

/shr/
  |-- k8s-prod  --> /data/k8s-prod
  |-- k8s-prod2 --> /data/k8s-prod
  `-- k8s-prod3 --> /data/k8s-prod

/data/k8s-prod/
   |-- nsA/
   |    |-- svc1/
   |    `-- svc2/
   `-- nsB  --> nsA

/data/k8s-prod2 -> k8s-prod
/data/k8s-prod3 -> k8s-prod

之前给开发环境配置的 /etc/exports 类似这样(省略了 anonuid 和 anongid 选项):

/shr/k8s-dev/nsA/svc1  -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8
/shr/k8s-dev/nsB/svc2  -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8
/shr/k8s-qe/nsA/svc1   -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8
/shr/k8s-qe/nsB/svc2   -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8

在 K8S 开发集群上用 /shr/CLUSTER/NAMESPACE/SVC 的方式挂载没问题,但是在 K8S 生产集群上挂载却失败,查了好一会,发现原因如下:

  1. 不管是开发环境的NFS server,还是生产环境的 NFS server,使用 mount -t nfs4 SERVER:/shr/CLUSTER/NAMESPACE/SVC 总是报告 “No such file or directory”,从而挂载失败;
  2. Kubernetes 的 NFS volume 会优先尝试 NFSv4 协议,失败则使用 NFSv3 协议。手动验证在开发环境可以使用 mount -t nfs SERVER:/shr/CLUSTER/NAMESPACE/SVC 挂载成功;
  3. 由于生产环境的 NFS server 上防火墙只开放了 NFSv4 所需的 2049 端口,没有开放 NFSv3 所需的动态端口范围(配置比较麻烦),所以生产环境 mount -t nfs 失败;

现象知道了,问题来了:

  • 抛开防火墙问题,为什么 NFSv3 可以挂载成功?
  • 为什么 NFSv4 挂载说找不到文件?
  • 为什么即使在 /etc/exports 里加了选项 refer=/data/CLUSTER/NAMESPACE/SVC@SERVER,NFSv4 挂载依然说找不到文件?

在调查原因之前,先粗糙的讲述下 NFSv3 和 NFSv4 在挂载协议以及服务设计上的不同。

NFSv1, NFSv2, NFSv3 系列都秉承了 Sun Microsystems 公司(有多少人知道 Sun 其实是个缩写?)的 Sun RPC 设计,NFSv3 挂载的过程是这样的:

  1. Sun RPC 的客户端向 IP:RPC_BIND_PORT(UDP/TCP 111) 发送一个 PROGRAM ID,比如 100005 表示 NFSv1~3 的 rpc.mountd 服务,这是 NFS 的 RFC 文档规定好的;
  2. NFS server 一侧运行了 /sbin/rpcbind,称为 portmapper 服务,这个东西有点像 DNS server,它把 PROGRAM ID 翻译成服务真正的 IP 和 PORT(不知道 IP 是否可以是其它机器),每个 RPC service 启动时都要向 rpcbind 注册自己的 PROGRAM ID.
  3. Sun RPC 客户端向 rpc.mountd 服务发一个 MOUNT 请求;
  4. rpc.mountd 根据 /var/lib/nfs/etab 文件里的 export table 判断是否目录存在并且允许挂载;

这套 RPC 协议有两个主要问题:

  • RPC service 的端口是动态的,而且 NFSv3 用的 RPC 服务不只 rpc.mountd,还有 rpc.statd, rpc.gssd, rpc.idmapd, rpc.svcgssd, nlockmgr 等,在 /etc/rpc 文件里可以看到完整列表(不完全是 NFS 服务相关的),用rpcinfo -p命令可以看到当前注册的 RPC 服务。动态端口导致防火墙配置比较头大,需要修改 NFS 各个服务的启动参数,指定端口范围以与防火墙配合,而如果网络链路上不止一个防火墙,那就头大如斗了;
  • NFSv3 的多个挂载之间是独立的,比如导出了 /a/b,导出了 /a/b/c/d,那么 NFS 客户端一侧某个文件打开对话框选择文件时,可以从 /a/b/c/d/e 回退到 /a/b/c/d,却不能从 /a/b/c/d 回退到 /a/b/c 进而退到 /a/b;

NFSv4 是 Sun 公司之后, 第一个被 IETF 主导的 NFS 协议更新版本,吸收了 AFS 和 CIFS 的一些优点,并加入了大量改进,颇有鸟枪换炮的感觉,话说 Windows 高版本自带了 NFS server 和 client 功能,只是默认没安装,估计极少人知道。相比苹果的 AFP 和微软的 SMB/CIFS 文件共享协议,NFS 是当之无愧的跨平台文件共享协议标准。

NFSv4 加入了大量改进,几乎可以说是个全新的 NFS 协议:

  • 抛弃了 Sun RPC 那一大坨,直接用单一端口 2049 通讯(同时支持 TCP 和 UDP,但不建议使用 UDP),从此防火墙配置不再是问题;
  • 引入了 pseudo filesystem 概念,将多个导出融为一体,上面的 /a/b 和 /a/b/c/d 例子中,NFSv4 会填补 /a、/a/b/c 这三个“未导出”的 gap,使得跟 UNIX 文件系统类似,多个挂载融合成单一的文件目录树;
  • NFSv4 采用有状态协议,文件锁实现更加高效且符合 POSIX 语义;
  • 支持多个 NFS 请求打包成一个请求一并发给服务端,提高通讯效率;
  • 支持 client cache,在只有一个客户端访问一个文件时,客户端可以把文件内容缓存起来,当有其它客户端也打开这个文件时,NFS server 会通知前一个客户端。这种设计极大提高了并发访问低时的 NFS文件读写效率;
  • exports 选项 refer= 允许 NFS server 迁移,类似 HTTP 的 redirect 效果;
  • exports 选项 replicas= 允许 NFS client 从一堆候选中选一个 NFS server (文件复制需要服务端自己实现);
  • NFSv4.1 支持 parallel NFS 扩展 (Linux 已经支持);
  • 2016 年 11 月发布的 NFSv4.2 规范支持 Server-Side Copy, Application Input/Output (I/O)
    Advise, Space Reservations, Sparse Files, Application Data Blocks, Labeled NFS;
  • 内置支持 attributes,基于此实现了内置的 quota、ACL,目前 Linux nfs-kernel-server 还不支持 quota 属性,ACL 属性支持是通过POSIX ACL模拟的,不完全;

不管是 NFSv3 还是 NFSv4,都使用 exportfs 命令设置 NFS 导出目录。exportfs 有两种操作模式:(1) 读取 /etc/exports 以及 /etc/exports.d/* (2) 从命令行参数获得导出目录设置。 两种模式下,exportfs 都会通过 /proc/net/rpc/nfsd.export/channel 往 Linux 内核写一份(很像 Plan 9 吧?),并且更新 /var/lib/nfs/etab 文件。

在从文件读取导出目录时,exportfs 源码 https://salsa.debian.org/debian/nfs-utils/blob/daf0838e30f896d02ca51becc99578713c1be4cb/support/nfs/exports.c#L186 会使用 realpath() 对目录做规范化处理,从命令行获取导出目录时则不会调用 realpath()。

rpc.mountd 在判断 MOUNT RPC 请求的 path 时,也会先调用 realpath() 然后跟 /var/lib/nfs/etab 中条目比对: https://salsa.debian.org/debian/nfs-utils/blob/daf0838e30f896d02ca51becc99578713c1be4cb/utils/mountd/mountd.c#L465

而 Linux 内核通过 /proc/net/rpc/nfsd.export/channel 接收导出目录时,会对 path 调用 kern_path() 解析掉符号链接: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/nfsd/export.c?h=v4.18-rc6#n545

总结一下:

  1. exportfs 读取配置文件时,总是对目录规范化;
  2. exportfs 不会对命令行指定的目录做规范化(猜测是实现上的纰漏);
  3. 对 NFSv3,rpc.mountd 在处理 mount request 里的 path 时,会先规范化这个 path 才查找 /var/lib/nfs/etab
  4. 对 NFSv4,mount request 是在内核里处理的,内核记录的 export table 总是使用规范路径。

所以上面三个问题的答案有了:

  • 抛开防火墙问题,为什么 NFSv3 可以挂载成功?
    • 虽然实际导出的是 /data/xxx 而非 /shr/xxx,rpc.mountd 的 /var/lib/nfs/etab 记录的 /data/xxx,但 rpc.mountd 对客户端请求的 /shr/xxx 规范了下,解析符号链接后成了 /data/xxx,所以匹配上了。
  • 为什么 NFSv4 挂载说找不到文件?
    • 实际导出的是 /data/xxx,Linux 内核里记录的是 /data/xxx,客户端请求的 /shr/xxx 不在导出列表里。
  • 为什么即使在 /etc/exports 里加了选项 refer=/data/CLUSTER/NAMESPACE/SVC@SERVER,NFSv4 挂载依然说找不到文件?
    • 跟上面原因一样,导出表里找不到 /shr/xxx 条目,也就无法应用 refer 选项。

OK,用 NFSv3 可以,但是实在不想折腾 NFSv3 的防火墙配置,怎么用 NFSv4 达到这个效果呢?首先,可以用 Linux 的 bind mount 解决,但由于 /data/k8s-prod/NS/SVC 里头也有 bind mount,所以需要 mount –rbind,但这个东西有个麻烦:如果 src mount 发生变更,比如里头多了一个 bind mount,那么 dest mount 是不会自动变更的。

其实用符号链接是可以解决的,配置如下:

/shr         -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8
/shr/k8s-dev -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8
/shr/k8s-qe  -rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8

后两行实际等价于写规范化的路径 /data/k8s-dev 和 /data/k8s-qe,写成 /shr/xxx 只是审美上好看点,用 exportfs 命令可以看到实际的导出表记录的 /data/xxx 路径。

需要第一行的原因是让 /shr/k8s-dev、/shr/k8s-qe 可以被访问到(记住上面的写法实际是导出了 /data/k8s-dev,/data/k8s-qe),客户端访问 /shr/k8s-* 时由于是符号链接,NFS 客户端会重新请求 /data/k8s-*,所以需要后面两行。

然后客户端可以用 NFSv3 或者 NFSv4 挂载了:

mount -t nfs SERVER:/shr/k8s-dev/nsA/svc1  /mnt
mount -t nfs4 SERVER:/shr/k8s-dev/nsA/svc1 /mnt

话说我今儿才反应过来 NFS 挂载时指定的路径可以是 exports 指定路径的子路径,在 NFSv4 下甚至可以挂载根目录,然后就能看到服务端的所有导出目录了😭

这篇又很长,奖励一个技巧给看到这里的同学。 有时候对于下面的目录树,我们希望暴露给 NFSv4 client 的是 /dirA 和 /dirB,隐藏掉 /export 这一级目录。

/export/dirA
/export/dirB

可以用 fsid=0 选项达到这个效果:

/export -fsid=0,rw,sync,no_subtree_check,all_squash,crossmnt 10.0.0.0/8

fsid=0 有特殊含义,表示根目录。

最后留个作业:如果把最上面的 /shr/k8s-prod2 -> /data/k8s-prod 符号链接改成 /shr/k8s-prod2 -> /data/k8s-prod2 -> /data/k8s-prod,为什么 NFSv4 挂载时报告文件找不到呢?

    原文作者:纤夫张
    原文地址: https://zhuanlan.zhihu.com/p/40691466
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞