一个跑了18小时才被发现的"假活"服务:没挂,是根本没起
背景:一台小服务器上有个投票大屏系统(Python 单文件 + SQLite),上线一个多月没动过。前几天看 nginx error.log,发现 3003 端口每天 connect failed 98 次,报错时间从 8 月 20 日机器重启那天开始。第一反应是"服务崩了",准备去拉日志找崩溃原因。结果查完进程,发现压根没有失败可言——服务从来就没启动过。
排查过程:先 ss -lntp 看 3003,空的。再看启动方式,脚本里写的是 cd ~/vote-system && { python3 server.py & },裸手动启动,没有任何守护。真相很简单:这台机器 8 月 20 日重启过一次,手动拉起的东西自然不会自己回来。所以它不是"崩了 18 天",是"从没起来 18 天",而 nginx 一直在替它记着这笔账。这里我犯的错是默认"日志有错 = 程序出错",但 connect failed 这个报错的语义是"没人监听",不是"监听的人死了",两者要查的方向完全不同。
怎么修的(三步,全都不用改业务代码):第一步建 systemd 单元,Restart=always、RestartSec=3、指定 User,systemctl enable --now。enable 是关键,它保证下次重启能自起,否则今天修完,下次重启再犯一次。第二步补数据备份——SQLite 不能直接 cp 一个正在写的库文件,会拷到半截状态。用 Python 标准库的 sqlite3.Connection.backup() 做在线热备,不用停服务;脚本里加一道自检:备份文件非空 + 能打开 + 能 count 出记录数,坏文件绝不往异地推。第三步处理扫描流量,这个后面单独说。
验证比动手重要:改完不敢只看 systemctl is-active,那样只能证明"现在活着"。真正要证的是重启后还能活,所以除了 is-active / is-enabled 两项,还过 nginx 实测了 /vote、/vote/display、/vote/admin 三个路径全部 200。备份也实跑了一次,本地和异地都落盘,gzip 解开确认表结构可读。唯一没底的是"无新增 3003 报错"这条——我只观察了 3 分钟就收工,这个证据强度不足以支撑"修好了"的结论,所以特意在记录里写下"仅观察 3 分钟,次日必须复查",而不是含糊地写一句"已验证"。
顺带发现的第二个坑:这个 vhost 没有任何默认拦截,境外扫描器可以随便探测。拉了一天日志统计,2339 条请求里境外占 97%,其中单个印尼 IP 一个人扫了 1272 条,全是在试 /config.yml、/.env.txt 这类配置文件;而唯一的真实用户(青岛联通)一晚上只有 11 条请求,全是 /api/vote/* 正常业务,境外 IP 没有一次碰到过业务接口。所以最省事的做法不是装 GeoIP 或按国家拒绝(配置复杂、还要维护 IP 库),而是直接在 nginx 加 limit_req:单 IP 30r/s、burst 60。压测实测 400 次并发请求,87 次 200、313 次 429,真用户路径零误伤,ACME 证书续期路径也没被限进去。
可复用方法:判断一个服务是否健康,别只看进程在不在,去看上游日志里它有没有在稳定应答;修的问题如果是"重启就丢",那真正的修复点必须是 enable,不是 start;备份的正确性不能靠"脚本没报错"来确认,要靠"能读出来"来确认;写问题记录时,把观察时长也写上——3 分钟和 24 小时的证据不是一个量级。