主题
云挂载为什么不能直写
异地备份做到最后一步,我差点以为备份成功了 —— 文件在、大小看着对、tar 退出码 0。第二天恢复演练才发现:那个包是 0 字节。
这篇讲的就是这类"看起来成功"的失败,以及云挂载(rclone / WebDAV 挂到本地路径)到底能怎么用。
背景
备份架构很常规:
- 本地:每天打包数据库 + 上传目录,保留 14 份
- 异地:每天同步到网盘,保留 7 份
网盘是通过 NAS 的"云存储挂载"功能挂成本地目录的,于是路径看起来跟普通目录没区别:
bash
/vol02/<uid>-<hash>/ # FUSE 挂载点(rclone)那么"把备份直接 tar 到挂载点"看起来完全合理:
bash
tar czf /vol02/<uid>-<hash>/backup-$(date +%F).tar.gz ./data现象
这条命令的表现非常迷惑:
| 写法 | 结果 |
|---|---|
tar czf <挂载点>/x.tar.gz ... | 退出码 0,文件存在,大小 0 字节 |
dd if=/dev/zero of=<挂载点>/x bs=1M count=10 | Bad file descriptor |
echo hello > <挂载点>/x.txt | Bad file descriptor |
cp /tmp/x.tar.gz <挂载点>/ | ✅ 正常,md5 一致 |
同一个目录:cp 能写,重定向不能写,dd 不能写,tar 假装写成功但其实什么都没写。
原因
这是 FUSE(这里底层是 rclone 的 WebDAV 后端)对写入模式的支持不完整造成的:
cp:打开临时文件、顺序写、关闭、重命名 —— 整块顺序写 + rename,rclone 的 VFS 缓存层能完整承接>重定向 /dd:直接O_WRONLY打开目标路径做随机写,部分操作(特别是 seek 和直接写)在 WebDAV 后端上无法映射,直接返回EBADFtar直写:tar 会先写文件、再回填头部(seek 回去改),这种"先写后回填"在流式后端的映射最容易出问题(配合O_WRONLY打开方式,就成了静默失败)
关键在于:tar 的失败是静默的。 它自己不知道写入没落地,退出码照样 0。
正确做法
分两步:先本地打包,再复制。
bash
# 1. 在本地磁盘打包(本地 ext4/btrfs 随便怎么写)
tar czf /tmp/backup-$(date +%F).tar.gz -C /path/to/data .
# 2. 复制到挂载点(cp 走顺序写 + rename,安全)
cp /tmp/backup-$(date +%F).tar.gz /vol02/<uid>-<hash>/backup/
# 3. 校验(关键)
md5sum /tmp/backup-$(date +%F).tar.gz
md5sum /vol02/<uid>-<hash>/backup/backup-$(date +%F).tar.gz
# 4. 清理本地临时文件
rm -f /tmp/backup-$(date +%F).tar.gz代价是需要一份额外临时空间(等于备份包大小),所以 /tmp 或本地盘要留够。如果包很大,可以用管道 tar | tee 之类优化,但别为了省这一步空间把备份搞成静默失败。
三条能救命的习惯
1. 任何跨文件系统/跨协议的写入,落盘后必须校验
bash
# 只信 md5,不信退出码
[ "$(md5sum a | cut -d' ' -f1)" = "$(md5sum b | cut -d' ' -f1)" ] && echo OKcp 的退出码 0 只代表"系统调用没报错",不代表"对端真的存下了"。
2. 备份必须做恢复演练
我现在的规矩是:异地备份每周抽一个包,实际解出来跑一次 sqlite3 integrity_check 或者直接把库导入一遍。
如果没做这一步,这次 0 字节的事故会藏到真正需要恢复的那天才爆 —— 那时候就没有"重做一次备份"的机会了。
3. 用 stat 而不是"看起来在"
bash
stat -c '%s %n' /vol02/<uid>-<hash>/backup/*.tar.gz云挂载的目录列表有时候是缓存/占位信息,ls 显示有文件不代表底层对象存在、也不代表大小对。读到实际大小才算数。
补充:这些限制是通用的
同样的坑我在别的地方也踩到过,本质一样 —— FUSE / 网络文件系统对"随机写"和"先写后回填"支持不完整:
- WebDAV 挂载点写 SQLite 数据库文件 → 直接损坏(SQLite 大量随机写 + fsync)
- 云挂载目录上跑
git clone→ 索引损坏 - 在 SMB / NFS 上跑数据库 → 除非确认支持锁和 fsync,否则当只读用
判断标准很简单:这个写入会不会 seek 回前面改东西? 会,就别在网络挂载点上做。
一句话版本:云挂载能当"仓库",不能当"工作区"。