个人技术笔记

2026-10-09 · 部署 部署与运维

把静态站发布做稳:一次"先删后解包"的教训

事故是怎么发生的

发布脚本要干的事很普通:本地打包,scp 上传,服务器解包,重载 nginx,最后做公网验收。问题出在服务器那一段的执行顺序上,写的是「上传 → rm -rf 目标目录 → tar -xzf 包」。

那一次,上传那一步其实失败了。失败归失败,远端脚本照常往下跑:先执行了 rm -rf,而 tar 又找不到包。于是正式站点的公开目录被清空了大约一分钟。

万幸的是脚本在清空之前先打了一份改前备份(tar -czf 备份.tgz -C 目标目录 .),靠这份备份即时恢复了。也就是说,救回这次事故的不是脚本写得多聪明,而是那个看起来多余的备份步骤。

两种发布顺序对比:先删后解包会在上传失败时清空站点;正确做法是先解包到临时目录、校验后再替换
两种顺序的差别:出问题的不是"删不删",而是删除有没有排在"文件包确认到达"之后。

根因是三条叠在一起

单看每一条都不致命,叠起来才出事。

  1. 顺序错了。把"删除"放在了"确认包已到达"之前。正确的形态应该是:解包到临时目录 → 校验关键文件存在 → 再原子替换。删除只能发生在"替换"这一步,不能发生在"下载"这一步之前。
  2. PowerShell 变量撞车。脚本里写了 $HOST = 'deploy@…',而 $HOST 是 PowerShell 的只读自动变量 $Host(变量名大小写不敏感),赋值当场报 VariableNotWritable。
  3. 重命名只改了一半。把 $HOST 批量替换成 $DEST 的时候,scp 那一行写的是 ${HOST}:(带花括号),字面量里并不含 $HOST,于是被漏掉了。那一行因此拿到了一个乱七八糟的主机名,上传失败——这就是上面那次事故的扳机。

第二条和第三条的关系值得单独记一句:变量名写错本身只是让某一行报错,真正造成损失的是"报错之后脚本还在继续跑"。发布脚本里任何一步失败,都不该被当成可以忽略的噪声。

同一次里还踩了一个独立的坑

用 PowerShell 批量改文件时写了 $c.Replace($from, $to, 1)。这个三参数重载并不存在(三参版要的是 StringComparison 枚举,不是 int),调用直接抛异常,写入全部失败。可脚本没有停下来,继续往下跑,最后还照样打印了"已改 ✅"。

这条和上面的发布事故其实是同一个毛病的两种表现:脚本把"我以为做了什么"当成"实际发生了什么"来汇报。一个失败的写入,被一行成功的提示盖了过去。

留下来的三条

  1. 任何"清空 + 重建"的发布,默认形态必须是「解包到临时目录 → 校验 → 替换」。rm -rf 绝不能出现在包还没确认到达之前。
  2. 改前备份是必需品,不是可选项。这一次它就是唯一的救命绳。
  3. 写盘之后必须回读校验,而且校验要独立于写入路径。"脚本说成功了"永远不等于"事实已经改变"。这次正是自己写的独立复核,把那个假成功抓出来的。

顺带一句

$HOST / $HOME / $PID / $INPUT 这类名字,在 PowerShell 里不能拿来当自己的变量。踩过一次就够记一辈子了。

← 回到笔记列表