skip to content
tlj 的工程笔记
Table of Contents

素材来自一次限时算力的具身智能比赛,这里只谈备份这件事本身,成绩和模型效果不在本文范围。文中不出现任何具体网盘、账号、主机、路径。

一句话先说结论

「传完了」和「验过了」是两件事;「这个文件还在」和「这份成果是完整的」也是两件事。我在同一周里,把这两件事各搞错了一次。病根是同一个:验证的时候,验错了维度。

一、约束:机器到点就没了

训练跑在一台限时 GPU 容器上,到点自动销毁,不续。对外网络几乎全封,只有少数镜像域可达,成果进出只能靠一台中转机做 tar-over-ssh 的流式推拉。

在这种环境里,备份不是「好习惯」,是唯一保命手段——机器消失是确定会发生的事,不是风险。所以我从一开始就在备,问题出在我以为自己备好了。

二、第一类幻觉:服务端说有,不等于内容对

我最早的校验方式是:上传完,用 WebDAV 的 PROPFIND 拉回远端文件的 getcontentlength,跟本地字节数一比,数字一样就算传完。

这个判据是不成立的,理由不止一条:

  • 尺寸相同不代表逐字节一致。 传输截断后被补齐、编码/换行处理、上传中断留下的部分提交,都可能给出一个「对的长度」。
  • 目录列表可能是缓存的。 你看到的那条记录未必反映对象当前的真实状态,stale 的列表会让你确信一个已经不对的东西是对的。

唯一可信的验证是往返:把文件下载回来,算 md5,和源端逐字节比。上传→下载→md5 对上,才叫验过。看得见、列得出、长度对,都只是「看着有」。

当时我做到的,是本机侧的 md5 硬验,加上远端的服务端尺寸和一份 manifest 做佐证。逐字节的往返回验当天没做。所以我在清单里把那 8 个实验 run 如实标成了「没做下载回验、未硬核对」,而不是「已备份」。

这一步现在回看是整件事里做对得最干净的一处:我没有把一个未验证的状态记成已验证。 真正的 6 件唯一成果各有至少 2 份独立硬验证副本(本机 md5 + 云端往返 md5,分布在两处互不依赖的存储上),是后来才补齐的。中间那段时间里,清单上写的就是「未硬核对」——难看,但准确。清单难看,你才会去补;清单好看,你就不会。

这里还带出一个更阴的洞:某个网盘的命令行工具,在前台交互 shell 里跑得好好的,放进后台 / cron / 无 tty 环境就失败。如果我按最初计划把它排进定时备份,结果会是「我以为在自动备,其实一次都没跑成」——而且不报警,因为我从来没在那个环境下看过它的输出。发现之后我把定时备份换成另一家,它只保留手动首选的位置。

教训:凡是自动化备份,必须在真实执行环境(cron、后台、无 tty)里验证跑得通,而不是在你手动敲命令的那个 shell 里跑得通。

三、第二类幻觉:验错了完整性维度(更贵的那次)

盘紧张,我要删一个已经打包过的归档目录。删之前我做了核对——我确认了里面那个 336 MB 的模型权重文件在别处有副本,判定这个目录是冗余的,放行删除。

我漏掉的,是同目录里一个 1292 字节的归一化统计文件,训练时算出来的 mean/std。推理时它必须和权重在同一个目录,缺了就是直接报文件不存在,模型加载不了。

正式评测前几天,我才发现 5 个任务的这个文件全丢了。 如果找不回来,只剩两条路:重新下载 57 GB 原始数据把归一化重算一遍,或者换一套统计值冒归一化漂移的风险跑评测。任何一条都等于整条腿废掉。

最后是找回来了:4 个从另一个进程当时留在临时目录里的完整副本捞回;剩下 1 个从一份 22 GB 的全量备份里、那个任务对应的 tar 中流式取出(只抽那一个成员,不落地解压整包,因为盘就是紧张才出的事)。5/5 全部找回,逐件校验过。

那 4 份临时副本的来源值得单独记一笔:它是另一个进程当时按整目录拷贝留下的。这个做法当时被我判为低效——拷了一堆用不上的东西,占盘。三天后救命的正是那些「用不上的东西」。挑文件效率高,拷整目录才救命:挑文件的前提是你此刻就知道哪些文件重要,而这次的全部教训就是我不知道。

根因很朴素:枚举内容的时候,我只看了最大最显眼的那个文件。 大文件抢注意力,小文件不抢,但恢复的时候缺一不可。删 bundle 之前该做的是 diff 全部内容清单,而不是抽查招牌大件。337 MB 里那 1292 字节,和那 336 MB 是同等重要的。

四、我抄下来的检查清单

  1. 备份完成的判据是往返 md5,不是服务端返回的尺寸,不是目录里看得见。
  2. 备份必须是在跑的自动化,并且在真实执行环境(cron / 后台 / 无 tty)里验证过,不是靠人想起来手动挑文件。
  3. 写一份「唯一成果」清单,给每一件标明副本数和验证状态。允许「未硬核对」这个状态存在——最怕的不是有东西没验,是把没验的记成已验。
  4. 删除任何 bundle 之前,diff 完整内容清单,不是核对招牌大件。
  5. 把「恢复所需的最小完整集」显式写下来:权重、归一化统计、推理代码、依赖版本。缺一件就等于没有。这份清单要在你还没删任何东西的时候写。
  6. 磁盘紧张时的正确动作是加盘或 offload,不是删成果。 冗余的代价远小于丢失的代价,两者根本不在一个量级上。

五、收尾

这两次的病根是同一个:验证的时候验错了维度。一次是验了尺寸没验内容,一次是验了大件没验全集。两次我都做了核对动作,两次核对都没有覆盖真正会出事的那一面——所以「我核对过了」这句话本身不构成任何保证,得问清楚核对的是哪个维度。

第二件事的代价之所以只是虚惊,是因为发现得早,以及那 4 份临时副本纯属运气。运气不是方法,所以我把上面那六条写下来了。