rsync 部署避坑:脚本执行位被 git 模式位抹掉,巡检备份双静默失败的排查与根治

26 阅读10分钟

本文首发于 note.sichenai.cc,原文链接:note.sichenai.cc/articles/ex…

先说结论。我的小服务器上有一份 30 分钟一拍的健康巡检和一个每日一拍的数据备份。巡检瞎了约 29 天,备份断了 16 天,而这期间我收到的所有检查结果都是绿的。两次失明是同一个根因:脚本的可执行位从来没进过 git,每次发布都被 rsync 忠实地抹掉。

三条保命经验,给同样用「git + rsync 推代码、cron 挂守护脚本」这套组合的个人开发者:

  1. **同一行报错换着文件名炸第二次,就不是运气差了。**第一次你修个案,第二次该查「谁在背后系统性搞事」。
  2. cron 脚本「部署成功」的判定只有两条:手动试跑一次 exit=0,加日志里出现第一条成功记录。文件在、没崩溃、没人找你,都不算数。
  3. **监控体系里得有一个「检查检查者」的位。**守护任务自己挂掉,是全系统最安静的事故;最便宜的自检是盯守护任务的产物时间戳。

这三条是两行一模一样的报错换来的。下面按实际发生顺序讲。

实测环境:腾讯云轻量应用服务器 2核4G(就是我上一篇《别在服务器上跑 npm install:一个 Node 应用搬进 2核4G 小服务器的三个坑》那台机器),Ubuntu,发布方式为开发机 git 仓 rsync -a 推到服务器,应用本体 systemd 常驻,定时任务三件套(数据抓取、备份、巡检)挂 crontab,各自把输出重定向到日志文件。实测时间 2026-09-20 至 10-20。

第一幕:巡检瞎了 29 天,它一直在报警,只是报给了一个没人看的文件

10-19 上午,因为「公众号订阅内容看不到、刷新慢」,我对服务器做了一轮只读体检:crontab、容器状态、抓取记录、逐源逐日入库量、中间件日志,五路对账。结论是业务链路完全健康——「慢」是上游检测机制自带的延迟,属机制性,不用治。

但翻巡检日志的时候不对了。这个 30 分钟一拍的健康检查,日志里从 09-20(挂上 cron 那天)起,每 30 分钟一条,每条都是同一句话:

/bin/sh: 1: /opt/…/deploy/healthcheck.sh: Permission denied

翻译一下:巡检脚本没有可执行位,cron 每 30 分钟试图启动它一次,每 30 分钟被系统拒绝一次,每 30 分钟往日志里写一句遗言。**它从未成功运行过哪怕一拍。**29 天里它其实一直在报警——报警内容是「我自己坏了」——只是这个警报报给了一个没人看的日志文件。cron 不发邮件(现代系统默认没配邮件投递),不弹窗,不打电话。Permission denied 这类故障天然无声。

更要命的是它瞎掉的方式:我的巡检查的是「应用还活着吗」,而它自己死了,所以这 29 天里如果应用真的宕机,我同样不会知道。防空洞塌了,只是恰好这段时间没有空袭。

当场修法:服务器上 chmod +x,手动跑一遍,exit=0,日志出现第一条成功记录。收工。

这里埋了个雷,当时没响:报错的是巡检脚本,我修的也只有巡检脚本。

第二幕:备份目录停在 10-03,而 backup.log 里躺着 17 条遗言

10-20 凌晨,做一项例行的生产加固:把每日备份的副本再异地拉一份到本地(「备份异地化」)。这类任务的第一步是先看看现有备份长什么样——例行 ls 备份目录,一眼看到不对:

**目录里最新的备份文件,停在 10-03。**16 天没有一次备份成功。

再看备份日志,老朋友了:

/bin/sh: 1: /opt/…/deploy/backup.sh: Permission denied

从 10-03 起,每天一条,连刷 17 条。同一行报错,换了个文件名。而同期所有业务巡检——库的新鲜度、抓取的入库量——全部绿灯。因为我当时的监控只盯业务指标:数据在进、应用在跑,一切看起来都好。备份这个守护任务自己死没死,没有任何指标在看它。

两件事叠在一起才后怕:这 16 天里如果数据库真坏了,我能回滚到的最新版本是 10-03 的——中间 16 天的数据全没了。而我会一直抱着「我有每日备份」的安全感,直到坏掉的那一刻。

第三幕:为什么服务器上的脚本会没有执行位

两个脚本、同一行报错、都在服务器上没有执行位。第二次撞见的时候,就该从「个案」升级到「根因」了。确诊链很短:

这些脚本在开发机写好,git 提交,然后 rsync -a 推上服务器。git 对每个文件只记两样:内容,和一个极简的权限标记——可执行(100755)或不可执行(100644)。当年写脚本的时候没有先 chmod +x 就 add 了,git 从此忠实地记着「这文件不可执行」。

而 rsync 的 -a 是归档模式,里面包含权限同步(-p):**源端是什么权限,目的端就被同步成什么权限。**于是每次发布,rsync 都按 git 里那个 100644,把服务器上的执行位抹掉。10-19 我在服务器上手动 chmod 的那次修复,治的是标——它有效,但只有效到下一次发布为止。手修远端是颗定时炸弹,引线焊在发布流程上。

一句话复盘这个机制:门锁换了芯,但配钥匙的图纸没改,下次按图配出来的还是旧钥匙。

还有个补丁思维的问题值得自首:10-19 修巡检的时候,backup.sh 已经病了 16 天,它的遗言也躺在没人看的日志里。我为什么没发现?因为报错的是巡检,我修的才是巡检。「哪个响修哪个」在单点故障里是好直觉,在系统性故障里是漏网之鱼的生产线。

第四幕:根治,和三道闭环

根治一步,落在源头:

# 在开发机(不是服务器)上:
chmod +x deploy/*.sh && git add deploy/
git ls-files -s deploy/
# 100644 → 100755,git 记住了,rsync -a 从此每次都带对

从此「部署完成」多了一条判定,写进发布流程:**发布后手动试跑一遍守护脚本,看 exit=0。**不是「文件同步过去了」,是「它真的能跑」。

**备份闭环 = 恢复演练,不是「文件拷走了」:**异地副本拉下来之后,PRAGMA integrity_check 验完整性,然后直接用它把应用真起一个实例:落地页 200、无口令访问 401、带管理口令出数据——三验全过,这份备份才算「可恢复」,而不只是「存在」。

告警闭环 = 从真实出口发真实消息:配好微信推送通道后,特意从服务器侧发起一条测试推送(而不是本机 Mac)——将来真实告警从服务器发,本机通不代表云服务器出口通。测试消息实收,链路才算闭环。

最便宜的一道自监控,检查检查者:

ls -1t /opt/backups/ | head -1
# 守护任务的产物时间戳:最新文件是昨天=正常;停在半个月前=断档

备份目录最新文件停在哪天,一眼定生死。这类检查不查业务,专查「守护任务自己还活着吗」。

两枚花絮

**「先清后查」在 WAL 伴生文件上是反向的。**异地拉取脚本里我写了清理 SQLite 的 -shm/-wal 伴生文件,放在完整性校验之前——直觉上先清后查更卫生。结果残留文件永远在,只是时间戳变成了校验时刻。对照实验定罪:macOS 的 sqlite3 打开 WAL 模式的库时会当场重建 -shm/-wal,退出还不删。**清理必须放在最后一个会打开它的操作之后。**排查这类时序问题有个窍门:看残留文件的时间戳指向哪一步。

**「在别人机器上能跑」的隐式依赖。**同一批加固里,一个照抄旧脚本的 import "dotenv/config" 在本地直接炸了 Cannot find module——服务器上它一直是某个包的传递依赖,恰好有;本地瘦身安装后没有。服务器的 node_modules 里藏着一批「恰好有」的包,你的脚本用了它们却不自知,换个环境就现形。

复盘表

谁症状静默时长真因处置
巡检脚本Permission denied 每 30 分钟一条约 29 天(09-20 起)执行位未进 git,rsync -a 每次发布抹掉10-19 服务器 chmod 手修(治标);10-20 本地 chmod+git add 根治
备份脚本Permission denied 每日一条,共 17+ 条16 天(10-03 起)同上,10-19 手修时漏网同上;补恢复演练三验
监控体系业务指标全绿,无任何告警全程只盯业务指标,不盯守护任务自身「检查检查者」:盯守护任务产物时间戳
告警链路(当时不存在)—尚未配置配好后从服务器出口发真实测试消息闭环

我的路径,和你能照抄的路径

  • **两处「幸好」必须交代。**巡检失明的 29 天和备份断档的 16 天里,都没有发生实际业务事故;它们分别被「排查另一个问题时的顺带翻日志」和「执行另一项任务前的例行 ls」撞见——都不是监控体系自己的功劳。换句话说,如果那 16 天里数据库真的坏了,我要到坏的那一刻才会知道备份早已断档。这篇的经验不是「我的监控很灵敏」,恰恰是「我的监控有结构性盲区,这次靠运气补的」,所以才要把盲区本身堵上。
  • **「手修会在下次发布时再被抹掉」是机制推演,不是我亲眼看到的第二次事故。**10-19 的手修恰好发生在当天发布之后,活到了次日确诊;但 rsync -a 按源端权限同步是文档写明的行为,根因不除、复发是必然。
  • **适用边界。**这套病的前提是「git 记权限位 + rsync -a 同步权限位」的发布方式。用 Docker 镜像或 CI 构建产物分发的流程不沾这个坑(各有各的坑)。
  • **数字口径。**29 天/16 天按日志首条报错出现时间倒推(日志留存完整);「git 只记可执行位」「rsync -a 含权限同步」为文档口径,事故链为本人实测。
  • **单机单流程样本。**一台机器、一套发布流程、两次爆发同一个根因——足够定性,不足以统计。

数据来源与视角:本篇为本人运维实录(2026-09-20 至 10-20,同一台 2核4G 轻量服务器),报错原文未润色;「手修必复发」一节为机制推演,已就地标注;「SSH 会话清理」类推断本篇无涉及。文中路径已脱敏(/opt/… 为节选),时点性事实以 2026-10 为准。