AIGC标识 零预算做灾备:把服务器备份塞进天翼云盘的三个技术难点

项目速览:中小规模服务器做灾备通常需要购买对象存储,成本往往难以覆盖。本方案把备份直接存入天翼云盘:每 12 小时自动全量备份一次,配套生成还原指南,更换服务器后可据此快速恢复业务,存储成本为零。

项目名称 翼备 Cloud189-Backup(已开源)
起止时间 2026.04 – 2026.12
我的角色 独立完成:方案设计、编码实现、文档与还原指南
技术栈 Python、Linux Shell、AES-256 加密、MySQL、天翼云盘 API、cron 定时任务
解决什么问题 中小规模服务器没有预算做灾备,一旦故障数据全丢
最终结果 12 小时自动全量备份、零存储成本、长期无人值守运行
技术亮点 登录凭证双保险(自动续期 + 备用通道)、备份文件加密且绑定原机器、备份文件命名异常的自愈处理

中小规模的服务器做灾备,第一个卡住人的往往不是技术,是预算。

对象存储按量计费,一年下来几十上百块不算多,但对一个实验室、一个小团队的自建服务器来说,这笔钱要么走不通报销,要么没人愿意为它单独维护一个账单。而另一边,几乎每个人都躺着一个容量用不完的天翼云盘。于是想法很自然:能不能把备份直接塞进云盘,一分钱不花?

我做了一套,叫翼备 Cloud189-Backup(GitHub)。从 4 月上线到现在,踩了三个坑:认证会失效、SDK 会偷偷改文件名、加密会把你自己的服务器排除在外。这篇把这三个坑和最后的解法写清楚。

一、这套东西到底在做什么

先把目标说清楚,后面所有设计都是围绕它来的。

需求:一台生产服务器,换掉或者挂掉之后,能在新机器上尽快把业务跑起来。

这句话拆开,就决定了要备份什么:

备份对象 为什么必须
MySQL 全量数据 业务数据,丢了没法重建
程序目录 代码、上传的附件、运行依赖
系统环境信息 系统版本、已装软件包列表,用于在新机器上复刻环境
关键配置 SSH、Nginx、MySQL、systemd 等,业务能跑起来的骨架

周期是每 12 小时一次全量。全量听起来笨,但对这种规模反而是对的——增量备份的复杂度(基线管理、链式恢复、任何一环损坏整条链报废)远超它省下的那点空间和流量。

流程大概是:

cron(每12h)
  └─> 打包 MySQL 全量 + 程序目录 + 系统信息 + 关键配置
        └─> 配置项单独加密(AES-256,绑定机器标识)
              └─> 上传天翼云盘
                    └─> 生成配套还原指南

注意中间那条"配置项单独加密"。这是整套方案的安全边界,也是第三个坑的来源,后面细说。

二、坑一:令牌会过期,而你不会记得去续

天翼云盘的 web 端接口走的是 OAuth 那一套:access_token + refresh_token。access_token 有有效期,过期之后所有请求返回鉴权失败。

一个每 12 小时跑一次的定时任务,最怕的就是这种"平时都好,某天突然全挂"的设计。更糟的是它挂得很安静——cron 把错误写进日志,没人看日志,等真要用备份的时候才发现最近三个月的备份全是 0 字节。

第一版:只做 refresh,等着它翻车

第一版的想法很直接:access_token 快过期就用 refresh_token 换新的。

问题在于 refresh_token 本身也会过期,而且在某些情况下会被服务端主动作废(异地登录、改密码、长时间不用)。一旦 refresh_token 失效,整套自动续期就死了,而它死的时候没有任何征兆。

这就是单点依赖:整条链路的存活,压在一个你无法观测、无法控制刷新周期的凭证上。

第二版:Cookie 兜底,做成双层

改成两层:

第一层:access_token ──过期──> refresh_token 换新 ──失败──┐
                                                          ▼
第二层:                                    用持久化 Cookie 重新走登录流程
                                                          │
                                                          ▼
                                              拿回新的 token 对,写回本地

关键设计有三点:

1. 判定要发生在请求之前,而不是失败之后。 每次运行先本地校验 access_token 的过期时间,提前续期,而不是等接口返回 401 再去补救。失败重试的成本远高于提前判断。

2. Cookie 要能独立存活。 持久化的 Cookie 存的是登录态,它和 access_token 是两条独立的凭证链。用 Cookie 走一遍登录接口,就能拿回一整套新的 token 对,等于把"凭证彻底失效"这件事降级成"凭证换了一次"。

3. 任何一层成功都要立刻回写。 新拿到的 token 和 Cookie 必须马上写回本地存储,否则下一次运行又从头开始,续期变成每次都续。

有了第二层,这套东西才真正做到了长期无人维护。之前的状态是"大概率能跑",之后是"断了会自己爬起来"。

这里有个反直觉的经验:兜底方案的价值不在于它经常被用到,而在于它把"需要人介入"的概率从每月一次压到每年一次。对一个没有值班的项目来说,这个差别就是"能用"和"不能用"。

三、坑二:SDK 偷偷改了文件名,备份被重复压缩

这个坑最隐蔽,也最典型。

现象

备份文件是 backup-20260420.tar.gz。上传成功,返回 200,云端也看得到文件,一切正常。直到有一天做还原演练,下载下来发现:

backup-20260420.tar.gz.zip

多了一个 .zip。解压一次得到 backup-20260420.tar.gz,再解压才是真正的备份内容。

更糟的是它会累积。因为下一次备份会把这个已经被改名、被压缩过的文件再打进去,于是云端出现 .tar.gz.zip.zip,体积逐次膨胀,而每次上传都"成功"。

定位

排查路径是这样走的:

  1. 先看本地。打包完立刻 ls -lh 和 md5sum 记录,确认本地文件是干净的 .tar.gz,体积正常。排除打包环节。
  2. 再对比云端。用列表接口拉回文件名和大小,跟本地对比——文件名不一致,大小比本地大一圈。说明是上传过程中被改了。
  3. 翻 SDK 的返回。上传接口返回的 JSON 里有个 fileName 字段,值和本地的完全不同。也就是说服务端接受了内容,但按自己的规则重命名了。
  4. 猜规则。规则大概是:服务端按 Content-Type 判断类型,如果拿不到可信的类型,就按扩展名兜底"归档",于是补了一个压缩包扩展名。而 Python 的 HTTP 客户端在流式上传 .tar.gz 时,默认 Content-Type 是 application/octet-stream,服务端认不出来。

一句话总结:上传时没有声明正确的 MIME 类型,服务端按自己的猜测重命名了文件,并把重命名后的名字当成事实。

解法

三个动作,缺一不可:

# 1. 显式声明 Content-Type,别让服务端猜
headers = {"Content-Type": "application/gzip"}

# 2. 上传后用接口返回的 fileName 作为"云端真名"记录下来,
#    而不是想当然地用本地文件名
remote_name = resp["fileName"]

# 3. 还原脚本按记录的真名去找文件,不要按本地名拼路径

第 2、3 点比第 1 点更重要。即使修好了 MIME,也不该让"本地文件名 == 云端文件名"这个假设留在代码里——那是把服务端的实现细节当成契约。把服务端返回的真名存进清单,还原时按清单走,这套逻辑对任何服务端的改名行为都免疫。

顺带一个教训:这类"静默改名"的问题,只有在真的做还原演练的时候才会暴露。上传成功、列表可见、大小非零,三个信号都指向"正常"。备份系统最大的风险从来不是备份失败,而是你以为备份成功了。

四、坑三:加密绑定机器标识,然后被自己的服务器挡住

配置文件里有什么?SSH 私钥和 authorized_keys、Nginx 站点配置、MySQL 的账号密码、systemd unit。这些东西明文躺在云盘里,等于把服务器钥匙挂在公网上。

所以设计上:配置项单独打包,AES-256 加密,密钥由服务器机器标识派生。

这个设计的好处很直接——备份文件即使被下载走,脱离原机器也解不开,因为它不知道那台机器的标识。

翻车现场

某次任务失败,日志里是一行看不懂的报错,指向获取机器标识的那个环节。

根因是:dmidecode 没装。

dmidecode 是从 BIOS/DMI 读硬件信息的工具,很多方案用它拿 product_uuid 作为机器唯一标识。但它不是所有系统的标配——精简安装的 Debian、容器环境、部分云主机镜像,都可能没有。而且它需要 root 权限读 /dev/mem,在受限环境里就算装了也读不到。

于是一个"从来不会出问题"的环节,在一台新机器上第一次运行就断了。而它断的时候,整个加密流程连启动都做不到。

解法:标识来源要有回退链

机器标识不该只有一个来源,应该是按可靠性排序的候选链,取第一个能拿到的:

1. /sys/class/dmi/id/product_uuid     # 内核直接暴露,不需要 dmidecode
2. /etc/machine-id                    # systemd 系统标配,安装时生成
3. dmidecode -s system-uuid           # 兜底,依赖工具存在 + root 权限

/sys/class/dmi/id/product_uuid 是第一优先级,原因很实在:它是内核已经解析好的 DMI 信息,读一个文件就能拿到,不需要额外装任何东西,也不需要 root。相比之下 dmidecode 要重新去读 SMBIOS 表,多一层依赖,就多一个失败点。

这里的原则可以推广:凡是要"探测环境"的地方,优先找内核或系统已经替你准备好的、可以直接读的东西,而不是再调一个外部工具去问一遍。

顺带一提,/etc/machine-id 在系统重装后会变,product_uuid 在换主板后会变。所以还原指南里必须写清楚:如果机器标识变了,需要用备份时的标识重新派生密钥——否则备份文件会变成一堆永远打不开的字节。这个提示不写进文档,半年后没人记得。

五、还原:备份的终点不是"存下来"

一套备份方案是否成立,唯一的检验标准是能不能还原,而且是在一台全新的机器上。

所以每次备份都会同步生成一份还原指南,内容包括:

  • 系统环境复刻:按备份的软件包列表装回依赖
  • MySQL 恢复:建库、导入、账号权限回填的顺序
  • 配置文件回填:SSH、Nginx、systemd unit 各自该放哪、权限和属主是什么
  • 服务启动顺序:先数据、再配置、最后应用,避免服务起来找不到依赖

这份文档是自动生成的,不是手写的。手写的文档会过期,自动生成的文档永远和这次备份的内容对应。

六、这套方案的边界在哪

写这篇不是想说"云盘能替代对象存储"。它的适用边界很清楚:

适合:单机或少量机器的中小规模业务、备份频率不高(小时级以上)、能接受还原耗时在分钟到小时级、预算为零。

不适合:需要秒级 RPO、需要多副本异地容灾、数据量到 TB 级、有合规审计要求。这些场景下云盘的限速、单文件大小限制、API 稳定性都会成为硬伤。

云盘方案的本质是用工程复杂度换成本——认证要自己做双层、文件命名要自己校验、限速要自己处理。如果你的人工时间比那点存储费贵,直接买对象存储更划算。这个判断没有标准答案,取决于你手上到底是什么资源。

最后

三个坑回头看,性质其实不一样:

  • 认证失效是"会必然发生的事",解决方案是设计失败路径,而不是指望它不出问题
  • 文件名被改是"服务端不守你的约定",解决方案是只信服务端返回的事实,不把实现细节当契约
  • dmidecode 缺失是"假设不成立",解决方案是给每个环境假设准备一条回退链

灾备系统有个共同点:它 99% 的时间都在做无用功,而你对它唯一的期待是那 1% 的时刻不要掉链子。所以它值得用最保守的写法——多做一层兜底,多留一条回退,多写一份能自动生成的文档。

仓库已经开源,入口在 ranqian-bit,欢迎提 issue。

posted @ 2026-11-10 20:42  染迁  阅读(6)  评论(0)    收藏  举报
一言(ヒトコト)

一言(ヒトコト)