写了 1400 行部署脚本,被一行 scp 干废了

4 阅读4分钟

事故

周五下午,线上服务挂了。

原因:一个 36KB 的旧版 server.js 覆盖了服务器上 76KB 的新版。部署的人没有拉取最新代码,直接把本地的旧文件传了上去。

更惊险的一次:一个只有 1 条测试数据的本地数据库,差点覆盖了生产环境 125 个用户的数据。发现时文件已经在传输队列里了。

根因:本地保存着过时的代码副本,部署时没有和服务器做比对,直接覆盖。

这不是个例。在没有 Git 仓库、没有 CI/CD 的项目里,用 scp / rsync 手动部署,这种事迟早会发生。

我的"完美方案"

我觉得问题出在"没有规范的部署流程"。于是研究了 Capistrano、Shipit、Deployer 这些成熟工具,花了两天写了一套部署系统:

1400 行 Shell,8 个文件。

  • Capistrano 风格的 releases/current/shared 目录结构
  • rsync --checksum 漂移检测
  • ln -sfn + mv 原子切换
  • PM2 日志 → PM2 状态 → HTTP 请求的 3 层健康检查
  • 健康检查失败自动回滚
  • flock 并发锁
  • *.db / uploads/ 全局排除

然后做了 10 轮代码审查,修了 79 个 bug

没错,一个部署脚本,79 个 bug。

实测打脸

信心满满地上线:

  • 第 1 次:失败。zsh 的 trap EXIT 在函数返回时就触发,SSH 连接被中途杀死。
  • 第 2 次:失败。符号链接指向了自己,形成循环,数据库丢了。
  • 第 3 次:终于成功。之后几次也正常。

我以为搞定了。

然后,就在同一天,另一个同事在处理一个紧急 bug。他的操作:

  1. 打开本地的 server.js(旧版本)
  2. 改了几行
  3. scp 直接传上去
  4. 线上新版本被覆盖

1400 行部署系统,完美绕过。

因为他压根没用。赶时间,直接 scp 了。

醒悟

我回头看了看团队规范里写的:

必须使用 deploy.sh 部署,禁止手动 scp!

他读过这条规则。但当他需要"快速修一个 bug"的时候,选择了最短路径。

任何依赖操作者遵守规则的方案,都会在某个时刻被绕过。

不是意愿问题,是人性。写在文档里的规则 = 写在路口的"禁止闯红灯",没有红绿灯和摄像头,迟早有人闯。

重新想

覆盖事故需要 3 个条件同时成立:

  1. 操作者手里有旧代码
  2. 服务器接受了上传
  3. 覆盖不可恢复

前两个没法根除——总得有人能部署代码。但第 3 个可以干掉。

让每一次文件变化都被记录。覆盖了不怕,随时恢复。

这不是"防止错误",是让错误变得无害

最终方案:

#!/bin/bash
# /opt/scripts/auto-version.sh — cron 每分钟跑一次
for dir in /opt/apps/*/; do
  cd "$dir" || continue
  if [ ! -d .git ]; then
    git init -q
    printf 'node_modules/\n*.db\n*.log\nuploads/\ndata/' > .gitignore
    git add -A && git commit -m "init" -q
  fi
  git add -A
  git diff --cached --quiet || git commit -m "auto: $(date +%Y%m%d_%H%M)" -q
done

出事了:

cd /opt/apps/my-project
git log --oneline           # 看历史
git checkout HEAD~1 -- .    # 一键恢复

两个方案的本质区别

1400 行系统的思路:阻止错误发生。 20 行 cron 的思路:让错误发生了也没事

前者需要每个人配合,一个人不配合就全白搭。 后者跑在服务器上,不需要任何人配合。

1400 行20 行
思路阻止错误错误无害
依赖操作者✅ 绕过就废❌ 服务器端自动
Bug79 个0
维护
磁盘~5 MB/项目

四条教训

1. 解决方案比问题大,就是过度工程化。

原始问题用一行 rsync 就能缓解。我搞出了 Capistrano 风格的完整系统,复杂度是问题本身的 100 倍。

2. 追求"事故无害"比"零事故"现实。

零事故要求完美预防——几乎不可能。Netflix 的 Chaos Engineering 就是这个思路:假设一切都会出错,确保出错了能快速恢复。

3. 安全措施放服务器端,别指望客户端。

任何依赖客户端配合的安全机制都是纸老虎。防火墙装在服务器上,不是装在用户电脑上。

4. 10 轮审查不如 1 次实测。

79 个 bug 被审查发现了,但真正上线时 3 个致命 bug 全是审查没覆盖的。真实环境的 1 次反馈 > 代码层面的 10 次检查。

谁需要这个

  • scp / rsync 手动部署的小团队
  • 外包/实习生直接改线上代码
  • AI Agent(Cursor、Claude Code 等)操作服务器
  • 任何没有 Git + CI/CD 的项目

已经有完善的 Git 工作流和 CI/CD?那你不需要这篇文章。


完整脚本(含 .gitignore 优化、并发锁、一键安装/卸载):[Gist 链接]