背景
要把 Mac 照片 App 的库(~/Pictures/Photos Library.photoslibrary,45GB、57,989 个文件、其中 originals 里有 10,339 个原始媒体)整体搬迁到 NAS 的 SMB 共享。目标是保留整个 .photoslibrary 包(相册、编辑、实况照片、视频都带着),而不是散文件导出。
方案确定后,真正难的不是“拷贝”这件事,而是它要跑很久,而久跑的任务最容易静默失败。
坑 1:rsync -aE 在 SMB 上慢到怀疑人生
第一版用 rsync -aE(-E 保留扩展属性 / xattr)。在本地磁盘上没问题,但在 SMB 上,每个文件的 xattr 都要多次网络往返,5 万多个小文件直接把速度拖垮。
修复:去掉 -E,改用断点续传参数:
rsync -a --partial --size-only /src/ /dst/
--size-only:只按文件大小判断是否已传,已传的跳过,天然支持断点续传。--partial:中断的传输保留部分文件,下次接着传。- 去掉
-E后 xattr 不拷;照片库在目标机器上打开不需要这些 xattr,可接受。
坑 2:macOS 没有 GNU timeout
想给每轮 rsync 加超时保护,习惯性地写 timeout 1200 rsync ...,结果 macOS 上直接 command not found——BSD 用户态没有 GNU timeout。
修复:用 bash 原生方式轮询子进程 + 超时强杀,不依赖任何外部命令:
# 启动 rsync 后台,拿到 PID
rsync -a --partial --size-only "$SRC" "$DST" &
PID=$!
# 每 5s 探一次,超过 900s 强杀
START=$(date +%s)
while kill -0 "$PID" 2>/dev/null; do
NOW=$(date +%s)
if [ $((NOW - START)) -gt 900 ]; then
kill -9 "$PID"
break
fi
sleep 5
done
wait "$PID"
kill -0 只检测进程是否存活、不发信号,跨平台可用。
坑 3:后台任务静默退出(最致命)
最初的封装脚本跑在后台任务里,大约 16 分钟后进程悄无声息消失,日志停在“第 1 次 rsync 尝试”之后,副本只传了 2.7GB / 45GB。没有任何报错、没有通知——这正是长程任务最危险的状态:它死了,但你不知道。
根因有两个:
- 在后台任务里又用
&嵌套了一个长进程,子进程随外层 shell 退出被一起带走。 - 没有“任务结束要主动通知人”的出口。
可靠性双保险:看门狗 + 每小时巡检
看门狗(直接作为后台任务运行,不再嵌套 &):
- 用上面的
kill -0轮询 + 900s 超时,超时强杀重来; - 断点续传(
-a --partial --size-only),已传不重传; - 副本体积逼近源库时,最后一轮用纯
-a精确同步收尾; - 退出即留标记:完成写
.done、失败或 NAS 掉线写.fail,后台任务据此通知人主动接管。
巡检兜底(独立 automation,每小时跑一次,只读):
- 检查
.done/.fail标记是否存在; - 检查看门狗进程是否还活着;
- 检查副本体积是否在增长。
任一异常立即发通知,只读、绝不自动改照片文件——巡检的职责是“发现问题叫人”,不是“自己动手”。
收尾:先校验,再清理
拷贝完成后,用内容级校验确认两端一致,再动本机库:
rsync -a --checksum --dry-run "$SRC/" "$DST/"
--checksum 是内容级比对(不含 xattr,与 -a 拷贝口径一致),--dry-run 只报告不执行。确认无差异后,才把本机库移入废纸篓(可恢复)而非永久删除——留一个反悔窗口。
一条原则
长程数据迁移任务,可靠性不来自“拷贝命令多正确”,而来自三件事:
- 可续传:中断能从断点接着跑,不从头来。
- 可观测:进度、存活、体积都有出口可查,死了能被发现。
- 自愈 + 外部巡检:看门狗负责自动重试,独立的巡检负责在两边都挂了的时候叫人。
缺一不可。只写 rsync 不写看门狗的人,终会遭遇“以为传完了,其实只传了 2.7G”的清晨。