印度机房用 tar 打包备份,gzip 把 40GB 数据压成了 11GB 的坏包

去年在孟买那台备份机上做过一次全量归档,Ubuntu 20.04,两块 4TB 的 SATA 盘做 mdadm RAID1,/data 目录大约 40GB,主要是日志、上传文件和几个 PostgreSQL 的 base backup。目标是打成 tar.gz 放到 /backup,再从另一条链路同步出去。

当时的命令很朴素:

tar -czf /backup/data-$(date +%F).tar.gz /data 2> /backup/tar.err
echo $?

跑完 tar 返回 0,包大小 11GB,看着挺合理,就同步走了。三天后要恢复一个子目录,解包时才发现问题:

tar -tzf data.tar.gz > /dev/null
gzip: data.tar.gz: unexpected end of file

tar -tzf 只列到一半就报错。回头看 /backup/tar.err,里面有一行 file changed as we read it,被 2> 重定向到了文件里,我压根没看。data 目录里有个正在写的应用日志,tar 读到一半文件变了,行为是不确定的,包可能被截断。

换成这个写法,把退出码和 stderr 一起卡住:

set -o pipefail
tar --warning=no-file-changed -czf /backup/data-$(date +%F).tar.gz /data \
  2> /backup/tar.err
tar_rc=$?
if [ $tar_rc -ne 0 ] && [ $tar_rc -ne 1 ]; then
  echo "tar failed: $tar_rc" >&2
  exit 1
fi
gzip -t /backup/data-$(date +%F).tar.gz || exit 2

tar 对“读到变化的文件”返回的是 1,不是 0,但包本身可能是可用的;真正致命的是 2 以上的错误。所以这里区分对待,最后再用 gzip -t 做一次完整性校验,校验不过就不往下游同步。代价是每次多花几分钟重新读一遍包,40GB 大概多两三分钟,我觉得值。

这个办法在两种情况下会失效。一是文件在打包过程中被删掉,tar 会返回 1 并跳过,包里就少东西,gzip -t 查不出来,只能靠 err 文件里逐行看 Cannot stat。二是 /data 里有 socket 或 FIFO,tar 默认会告警,压缩出来虽然能解,但恢复时会报类型不对。我之前就漏过一个应用的 unix socket,恢复环境里服务起不来,排查了半天。

现在我会在打包前先 lsof +D /data 看一眼有没有进程在写,写着的目录单独处理或者跳过。

你们在归档正在写入的目录时,是停服务、快照,还是接受 tar 的返回值 1 直接放过?

0
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。