45GB 照片库迁 NAS——长程备份任务的可靠性工程

45GB 照片库迁 NAS——长程备份任务的可靠性工程

背景

要把 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。没有任何报错、没有通知——这正是长程任务最危险的状态:它死了,但你不知道

根因有两个:

  1. 在后台任务里又用 & 嵌套了一个长进程,子进程随外层 shell 退出被一起带走。
  2. 没有“任务结束要主动通知人”的出口。

可靠性双保险:看门狗 + 每小时巡检

看门狗(直接作为后台任务运行,不再嵌套 &):

  • 用上面的 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 只报告不执行。确认无差异后,才把本机库移入废纸篓(可恢复)而非永久删除——留一个反悔窗口。

一条原则

长程数据迁移任务,可靠性不来自“拷贝命令多正确”,而来自三件事:

  1. 可续传:中断能从断点接着跑,不从头来。
  2. 可观测:进度、存活、体积都有出口可查,死了能被发现。
  3. 自愈 + 外部巡检:看门狗负责自动重试,独立的巡检负责在两边都挂了的时候叫人。

缺一不可。只写 rsync 不写看门狗的人,终会遭遇“以为传完了,其实只传了 2.7G”的清晨。