本文记录一次排查。PVE 通过 NFS 挂载 Unraid 的共享作为备份存储,挂载周期性地变成 Stale file handle。根因是 Unraid 的 shfs 与 mover 的共同行为,与网络和 NFS 服务本身无关。
环境
| 组件 | 版本或配置 |
|---|---|
| Proxmox VE | pve-manager/9.2.11,内核 7.0.14-15-pve |
| Unraid | 7.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:00 | mover 运行 | — |
| 06-28 07:00 | 备份 | 开始时挂载已经失效 |
| 07-12 | 重新挂载后的第一次备份 | 写完归档后失败 |
| 07-28 03:00 | mover 运行 | — |
| 08-23 | 备份 | 写完归档后失败 |
| 09-04 | PVE 重启,重新挂载 | — |
| 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 号:
- mover 把
/mnt/ssd-cache/pve-backup整个移到阵列。ssd-cache分支消失后,shfs改用阵列分支的 inode。 - 下一次 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> 的底层分支组合保持不变。
| 方案 | 做法 | 收益 | 成本 |
|---|---|---|---|
| A | Primary storage 改为 Array,不使用 pool | 只改一个共享,不需要脚本,有 parity 保护 | 写入速度受 parity 限制。数据仍然经过 FUSE。 |
| B | 改为 exclusive share,只使用 pool | NFS 直接访问 pool 的文件系统,完全绕过 FUSE | 需要启用全局设置 shareUserExclusive,这可能影响其他共享。阵列上的旧数据需要迁移或删除。没有 parity 保护。 |
| D | Secondary 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 运行,两者不会重叠。
验证
- 修改设置并在 PVE 上重新挂载以后,
pvesm status显示active。 - 手动运行一次 vzdump。写入 5.30GB 用时 68 秒,prune 成功,任务返回
rc=0。 - 手动运行脚本,把 09-06 的备份从
ssd-cache移到disk1。 - 在备份前、备份后、移动后分别检查。PVE 和 Unraid 两端的根目录 inode 都保持
216454257090494596,dump的 inode 都保持216454258165593218。 - 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