Unraid 的 NFS 共享在 mover 运行后变成 Stale file handle:原因与修复

本文记录一次排查。PVE 通过 NFS 挂载 Unraid 的共享作为备份存储,挂载周期性地变成 Stale file handle。根因是 Unraid 的 shfs 与 mover 的共同行为,与网络和 NFS 服务本身无关。

环境

组件版本或配置
Proxmox VEpve-manager/9.2.11,内核 7.0.14-15-pve
Unraid7.3.2,内核 6.18.38-Unraid,作为 PVE 上的一台 VM 运行
网络PVE 与 Unraid 之间使用一个独立的虚拟网桥。PVE 端是 10.10.10.1,Unraid 端是 10.10.10.2。
共享pve-backup,Primary storage 是 NVMe pool ssd-cache,Secondary storage 是 Array
挂载方式PVE 原生 NFS 存储,vers=4.2,hard,timeo=600,retrans=3
备份任务每周日 07:00 运行 vzdump,带 prune
mover每月 28 日 03:00 运行

PVE 的存储配置如下:

nfs: unraid-backup
	export /mnt/user/pve-backup
	path /mnt/pve/unraid-backup
	server 10.10.10.2
	content backup
	options vers=4.2,hard,timeo=600,retrans=3,_netdev
	prune-backups keep-last=2,keep-weekly=2,keep-monthly=3

Unraid 生成的 /etc/exports 条目如下:

"/mnt/user/pve-backup" -fsid=101,async,no_subtree_check 10.10.10.1(rw,no_root_squash,sync,no_subtree_check)

现象

备份任务在写完归档以后、prune 阶段失败:

INFO: archive file size: 5.29GB
INFO: adding notes to backup
INFO: prune older backups with retention: keep-last=2, keep-monthly=3, keep-weekly=2
ERROR: Backup of VM 202 failed - unable to activate storage 'unraid-backup' - directory '/mnt/pve/unraid-backup' does not exist or is unreachable

此后挂载点一直不可用:

# stat /mnt/pve/unraid-backup
stat: cannot statx '/mnt/pve/unraid-backup': Stale file handle

挂载不会自动恢复。之后每周的备份都在开始时失败,报错为 could not activate storage。

背景:为什么之前从 CIFS 换到 NFS

这个存储最早使用 CIFS 挂载,同样出现周期性的 stale。当时的排查结论如下:Unraid 的 emhttpd 检测到配置变化时会执行 rc.samba reload;在 reload 期间,客户端的 tree reconnect 会被服务端拒绝(reconnect tcon failed rc = -2)。Unraid 的 NFS 服务与 Samba 相互独立,所以当时改用了 NFS。

这个判断本身是正确的。但是它遗漏了一点:Unraid 通过 NFS 导出的是 /mnt/user/... 路径。这个路径属于 FUSE 文件系统 shfs,所以 NFS 仍然受 shfs 行为的影响。

排查过程

1. 排除网络和 NFS 服务

  • ping 10.10.10.2 没有丢包,RTT 约 0.1 ms。
  • Unraid 的 2049 端口可以连接。
  • nfsstat -c 显示 retrans 为 0。

这说明传输层和 RPC 层都正常。

2. 内核日志中的 fileid changed

PVE 的内核日志在备份写入期间出现了以下记录:

Sep 06 07:00:02 pvescheduler: starting new backup job: vzdump 202 ...
Sep 06 07:00:20 kernel: NFS: server 10.10.10.2 error: fileid changed
Sep 06 07:01:09 kernel: NFS: server 10.10.10.2 error: fileid changed
Sep 06 07:01:09 pvestatd: unable to activate storage 'unraid-backup' - directory '/mnt/pve/unraid-backup' does not exist or is unreachable

fileid changed 表示客户端用同一个 file handle 查询时,服务端返回了不同的 inode 号。客户端因此认为这个对象已经不是原来的对象。

对普通文件来说,客户端可以按文件名重新 lookup,然后得到新的 handle。对 NFS 导出的根目录来说,客户端没有文件名可以用来重新 lookup。所以根目录的 fileid 一旦改变,整个挂载就会永久失效,只能重新挂载。

3. 失败的时间规律

把 PVE 的全部 vzdump 任务按时间排列,可以看到一个规律。每月 28 日以后,挂载会失效。这个日期正是 mover 的运行日期。

日期事件结果
06-14、06-21正常备份OK
06-28 03:00mover 运行—
06-28 07:00备份开始时挂载已经失效
07-12重新挂载后的第一次备份写完归档后失败
07-28 03:00mover 运行—
08-23备份写完归档后失败
09-04PVE 重启,重新挂载—
09-06备份写完归档后失败

失败分为两种:

  • mover 运行以后,挂载立即失效。
  • 重新挂载以后,下一次备份的写入过程使挂载失效。

4. 共享分布在多个分支上

在 Unraid 上查看这个共享的实际位置:

# ls -d /mnt/*/pve-backup
/mnt/disk1/pve-backup
/mnt/disk2/pve-backup
/mnt/ssd-cache/pve-backup
/mnt/user/pve-backup
/mnt/user0/pve-backup

/mnt/user/pve-backup 是 shfs 合并 disk1、disk2、ssd-cache 三个分支后得到的视图。

5. shfs 报告的 inode 来自底层分支

Unraid 的全局设置中有 fuse_useino="yes"。在这个设置下,shfs 报告的 inode 号由底层分支的 inode 号推算得到。可以用两个目录的 inode 差值验证这一点:

# stat -c "%i %n" /mnt/user/pve-backup /mnt/user/pve-backup/dump
216454257090494596 /mnt/user/pve-backup
216454258165593218 /mnt/user/pve-backup/dump

# stat -c "%i %n" /mnt/ssd-cache/pve-backup /mnt/ssd-cache/pve-backup/dump
132 /mnt/ssd-cache/pve-backup
1075098754 /mnt/ssd-cache/pve-backup/dump

两组数据的差值都是 1075098622。这说明 /mnt/user/pve-backup 的 inode 等于一个固定偏移量加上 ssd-cache 上的 inode。

另一个共享 media 同时存在于 cache、disk2、ssd-cache 三个分支上,它的数据也符合同样的规律:

216454260311720064 /mnt/user/media
3221225600         /mnt/ssd-cache/media

216454260311720064 - 216454257090494464 = 3221225600。所以当 ssd-cache 分支存在时,shfs 使用这个分支的 inode。在本机上,ssd-cache 是这两个共享的 Primary storage。本文没有进一步验证 shfs 选择分支的完整规则。

由此可以得出结论:当 ssd-cache 上的目录出现或消失时,/mnt/user/pve-backup 的 inode 号会随之改变。

6. 目录的创建时间与备份开始时间相同

# stat -c "%w | %n" /mnt/ssd-cache/pve-backup /mnt/ssd-cache/pve-backup/dump
2026-09-06 07:00:11.627319509 +0900 | /mnt/ssd-cache/pve-backup
2026-09-06 07:00:11.627319509 +0900 | /mnt/ssd-cache/pve-backup/dump

vzdump 的日志显示 Backup started at 2026-09-06 07:00:11。所以这两个目录是在 vzdump 开始写入时创建的。写入前,ssd-cache 上没有这两个目录。

7. mover 会删除 pool 上的共享顶层目录

/usr/local/sbin/mover 中的相关代码如下:

move() {
  find "$1" -depth 2>/dev/null | /usr/libexec/unraid/move $DEBUGGING
  <em># second pass to clean up leftover empty directories</em>
  find "$1" -depth -type d 2>/dev/null | /usr/libexec/unraid/move $DEBUGGING
}
...
    if [[ $shareUseCache = yes && -n $shareCachePool && -d "/mnt/$shareCachePool/$SHARE" ]]; then
      ...
          move "/mnt/$shareCachePool/$SHARE"

$1 是 /mnt/ssd-cache/pve-backup。find -depth 的输出包含 $1 本身,第二遍还会清理空目录。所以 mover 运行后,/mnt/ssd-cache/pve-backup 会被移走。

根因

两个动作都会改变 /mnt/user/pve-backup 的 inode 号:

  1. mover 把 /mnt/ssd-cache/pve-backup 整个移到阵列。ssd-cache 分支消失后,shfs 改用阵列分支的 inode。
  2. 下一次 vzdump 写入时,shfs 按 Primary storage 设置在 ssd-cache 上重新创建这个目录。shfs 又改用 ssd-cache 分支的 inode。

每次变化都会改变 NFS 导出根目录的 fileid。PVE 的 NFS 客户端检测到 fileid changed 以后,挂载永久失效。

因此,只要满足以下三个条件,就会出现这个问题:

  • NFS 导出的路径位于 /mnt/user 下。
  • 这个共享同时使用 pool 和阵列,并且启用了 mover(shareUseCache="yes")。
  • fuse_useino="yes"。

如何判断自己是否受影响

在 Unraid 上运行以下命令。把 <share> 替换为共享名。

<em># 1. 共享是否分布在多个分支上</em>
ls -d /mnt/*/<share>

<em># 2. 共享的 cache 设置</em>
grep -E "^shareUseCache|^shareCachePool" /boot/config/shares/<share>.cfg

<em># 3. shfs 是否使用底层 inode</em>
grep fuse_useino /boot/config/share.cfg

<em># 4. pool 上顶层目录的创建时间</em>
stat -c "%w %n" /mnt/<pool>/<share>

在 NFS 客户端上运行以下命令:

dmesg -T | grep "fileid changed"

如果第 4 步的创建时间接近最近一次写入的时间,并且客户端日志中有 fileid changed,那么你很可能遇到了同一个问题。

修复方案对比

修复的目标是:让 /mnt/user/<share> 的底层分支组合保持不变。

方案做法收益成本
APrimary storage 改为 Array,不使用 pool只改一个共享,不需要脚本,有 parity 保护写入速度受 parity 限制。数据仍然经过 FUSE。
B改为 exclusive share,只使用 poolNFS 直接访问 pool 的文件系统,完全绕过 FUSE需要启用全局设置 shareUserExclusive,这可能影响其他共享。阵列上的旧数据需要迁移或删除。没有 parity 保护。
DSecondary storage 改为 None(only),每月用脚本把文件移到阵列写入速度与 NVMe 相同;旧备份有 parity 保护需要维护一个脚本。如果有人删除 pool 上的目录,或者把设置改回 yes,问题会复发。
D’与 D 相同,但不迁移文件最简单没有 parity 保护

本文采用方案 D。

方案 D 的实现

第 1 步:关闭这个共享的 mover

在 WebUI 中打开 Shares → pve-backup,把 Secondary storage 改为 None。改完后配置文件如下:

shareUseCache="only"
shareCachePool="ssd-cache"
shareCachePool2=""

mover 只处理 yes 和 prefer 两种设置,所以它会跳过 only 的共享。读取不受影响,shfs 仍然会合并阵列上的旧文件,PVE 也仍然可以看到并 prune 这些文件。

修改这个设置之前,要确认 pool 上的共享目录已经存在。如果目录不存在,下一次写入时会创建它,这会触发一次 inode 变化。

第 2 步:用 User Scripts 按月迁移文件

脚本只移动文件,不删除也不移动目录。

#!/bin/bash
<em># Move finished PVE backups from the ssd-cache pool to disk1.</em>
<em># Do not remove the share directories on the pool. Removing them changes the</em>
<em># inode that shfs reports for /mnt/user/pve-backup and breaks the NFS mount on PVE.</em>
set -u
SRC=/mnt/ssd-cache/pve-backup/dump
DST=/mnt/disk1/pve-backup/dump

[ -d "$SRC" ] && [ -d "$DST" ] || { echo "missing $SRC or $DST"; exit 1; }

<em># Skip files changed in the last 120 minutes, so a running backup is not moved.</em>
rc=0
while IFS= read -r -d '' f; do
  mv -n -- "$f" "$DST/"
  if [ -e "$f" ]; then
    echo "not moved: $f"
    rc=1
  fi
done < <(find "$SRC" -maxdepth 1 -type f -mmin +120 -print0)
exit $rc

设计说明:

  • 只移动文件。 不要使用 mv dump ...,不要使用带目录清理的 rsync --remove-source-files,也不要使用 find -delete。
  • mv -n。 目标文件已经存在时,不覆盖目标文件,也保留源文件。脚本会输出 not moved 并返回 1。
  • -mmin +120。 跳过最近 120 分钟内修改过的文件,避免移动正在写入的备份。
  • < <(...)。 如果写成 find ... | while ...,循环会在子 shell 中运行,rc 的值无法传回主 shell。
  • 磁盘路径之间移动。 源和目标都是 /mnt/<disk>/... 路径,不经过 /mnt/user。这样可以避免在同一次复制中混用 user share 路径和 disk share 路径。
  • 跨文件系统的 mv。 跨文件系统时,mv 先复制再删除源文件。直接写入 /mnt/disk1 时,共享的 Minimum free space 设置不生效,所以要自己关注目标磁盘的剩余空间。

调度时间设为 0 2 28 * *。备份在周日 07:00 运行,两者不会重叠。

验证

  1. 修改设置并在 PVE 上重新挂载以后,pvesm status 显示 active。
  2. 手动运行一次 vzdump。写入 5.30GB 用时 68 秒,prune 成功,任务返回 rc=0。
  3. 手动运行脚本,把 09-06 的备份从 ssd-cache 移到 disk1。
  4. 在备份前、备份后、移动后分别检查。PVE 和 Unraid 两端的根目录 inode 都保持 216454257090494596,dump 的 inode 都保持 216454258165593218。
  5. PVE 的 pvesm list 可以列出被移动的文件,文件大小一致。

移动文件以后,PVE 上出现了一条新的 fileid changed。原因是 PVE 缓存了这个文件的旧 handle。客户端按文件名重新 lookup 后,stat 成功返回了新的 inode 号,存储仍然是 active。这条日志证明了一点:普通文件的 inode 变化可以自动恢复,只有导出根目录的 inode 变化会使挂载失效。

脚本的四条执行路径也分别测试过:

情况结果
没有要移动的文件rc=0
源目录或目标目录不存在输出 missing ...,rc=1
有旧文件(包括文件名带空格的文件)和新文件只移动旧文件,rc=0
目标目录已有同名文件不覆盖目标文件,输出 not moved: ...,rc=1

已知限制

  • 数据仍然经过 FUSE。本文只消除了“分支出现或消失”这一个原因,没有排除其他与 FUSE 相关的 handle 问题。
  • 如果有人手动删除 /mnt/ssd-cache/pve-backup,或者把 Secondary storage 改回 Array,问题会复发。
  • 每月迁移文件以后,NFS 客户端可能记录一条 fileid changed。只要存储仍然是 active,就不需要处理。
  • 本文没有测试关闭 fuse_useino 的效果。这个设置是全局的,会影响所有共享。

附:检查其他通过 NFS 导出的共享

本机的 media 共享也通过 NFS 导出,并且分布在三个分支上。如果其中某个分支上的顶层目录被删除或新建,这个共享的 NFS 客户端也可能遇到同样的问题。可以用下面的命令列出所有 NFS 导出的共享,再逐个用上文的方法检查:

grep -oE '^"/mnt/user/[^"]+"' /etc/exports

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注