首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >脚本报"一切正常",其实已经全挂了:三个不报错的坑

脚本报"一切正常",其实已经全挂了:三个不报错的坑

原创
作者头像
用户12800671
发布于 2026-10-14 09:17:54
发布于 2026-10-14 09:17:54
380
举报

(本文含 AI 辅助创作内容)

先看一段真实的现场记录。

我有几组定时任务,跑在没有人盯着的环境里:签到、发文、巡检,各自按点执行。前一天晚上,巡检脚本照例跑完,输出结尾是 all alive。我看着这三个字,关灯睡了。

第二天早上打开工作区,日志里干干净净,状态文件写着"已同步"。然后我去核对真实结果——当天的发布一条都没有,两台浏览器进程早就不在了。

脚本没有说谎。它只是在用自己一套口径判断"活着"。

这篇文章把那次排查拆开写:三个"语法正确、运行不报错、结论却是错的"的坑,以及四个我现在统一在用的兜底做法。只讲运维姿势,不涉及任何平台。

坑一:用 PID 判存活 —— PID 会被复用

最容易写出来的存活判断长这样:

代码语言:python
复制
def alive(pid):
    return subprocess.run(["tasklist", "/FI", "PID eq %s" % pid],
                          capture_output=True).stdout.find(pid.encode()) >= 0

问题在于:进程表中的 PID 号会被系统回收再分配。一个不相干的进程占用了老的 PID,这个函数就会返回"活着"。

那一晚的看门狗进程其实早就退出了,但它的 PID 被另一个程序接走,于是心跳文件里的 PID 一直"找得到"——看起来有人守夜,实际上一整晚没人。

修法:不要用"PID 存在"判活,用心跳新鲜度判活——写入 PID 的同时写一个时间戳,判定条件改成「PID 活着 且 时间戳在 N 秒内」。

代码语言:python
复制
def fresh(path, max_age=180):
    pid, ts = open(path).read().split()[:2]
    return pid_exists(pid) and (time.time() - int(ts)) <= max_age

多花一个字段,换来的是"看起来正常"和"确实正常"的区别。

坑二:依赖会话派生常驻进程 —— 它会跟着会话一起被回收

第二个坑更隐蔽。我在一次会话里起了个看门狗,跑得好好的。会话结束,看门狗没了——连同它守着的两个浏览器一起。

根因是:某些沙箱/托管环境会成组回收在一次会话内派生的进程。你不是"起了个后台进程",你是"在这棵会话进程树上挂了一个节点",树被拔走,节点一起走。

现象上,它和"程序自己崩了"完全一样,所以你第一反应会去查代码里有没有异常——方向就偏了。

修法:常驻进程必须挂在会话之外。两种可行路径:

  1. 交给系统级调度(Windows 计划任务 / cron / systemd),脱离会话树;
  2. 如果那条路走不通(比如命令被策略拦了),把它挂到一个本来就常驻的进程树下面——由常驻服务定期派生,而不是由你的交互会话派生。

第 2 条是当时实际用上的方案:让每分钟/每半小时跑一次的服务去 Popen 保活脚本,子进程自然跨会话存活。

坑三:把"查不到"当成"结果是空"

第三个坑最像玄学:我每天要核对前一天的任务结果,于是写了个脚本去查。回车,屏幕上什么都没有。

我第一反应是命令写错了,改了几遍还是空白。真正的原因有两层:

  • 输出本身可能丢。进程被托管、缓冲区没刷就被回收,输出根本回不来;
  • 更糟的是出问题时也是空白。你手上只有"什么都没有",没有一行线索。

所以这里的问题不是"命令写错了",是你把结论建立在一个不可靠的通道上。

修法:不问、不等、不依赖 stdout —— 让脚本自己把结果写进文件,你只读文件。

代码语言:python
复制
summary = collect()
with open(report_path, "w", encoding="utf-8") as f:
    json.dump(summary, f, ensure_ascii=False)      # 给机器:做跨天对比
with open(human_path, "w", encoding="utf-8") as f:
    f.write(render_text(summary))                   # 给人:扫一眼就知道该干啥

四个我现在统一在用的兜底做法

上面三个坑单独修都能好,但真正让这一类问题不再复发的,是下面四条习惯:

1. 凡是"跑在没人看着的地方"的程序,结果一定要落到磁盘。

不看进程输出,只看落盘文件。屏幕会被挡住、stdout 会被丢掉、会话会断开,只有文件是稳定的。

2. 成功和失败走同一条落盘路径。

最常见的错误日志是"成功了才写"。于是你手上永远只有顺利时的记录——恰恰在最需要线索的时候什么都没有。只要脚本开了头,最后一行必须写文件,连异常对象一起写进去。

3. 报告分两份:机器读的和人读的。

一份结构化数据留着明天做 diff,一份几百字摘要给人看。混在一份里,机器那份被人话污染,人那份被字段淹没。

4. 差分比快照有用。

单看今天的快照,你无法判断"某条任务不见了"是本来就没跑还是新出的问题。每天存一份结构化结果,第二天做差集:

代码语言:bash
复制
昨天在 / 今天不在 → 掉了
昨天不在 / 今天在 → 新起
字段值变化         → 单独列出来

差异一眼可见,不靠记忆。

附带的一个设计原则:只读

上面那个汇总脚本,我只让它查,不让它改——不发布、不写入任何业务状态、不动任何凭据。

这一点看着不起眼,但它决定的其实是使用频率:一个"什么时候都敢随手跑一遍"的工具,和一个"跑之前要先想三秒会不会出事"的工具,实际被用的次数差一个数量级。

小结清单

把这篇压成一段可以直接抄走的检查表:

  • 存活判断用「PID + 心跳时间戳」,不要只看 PID
  • 常驻进程挂在会话之外(系统调度,或常驻服务的子进程)
  • 结论不依赖 stdout,只依赖落盘文件
  • 失败路径也必须写文件,异常对象一起写
  • 报告分机器版 / 人读版
  • 每天存快照,跨天做差分
  • 只读工具优先,让它"敢随便跑"

排查问题最怕的从来不是难,而是你手上什么证据都没有。上面这些做法都不聪明,它们只做一件事:保证"事情发生过"这件事本身被留下来。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 坑一:用 PID 判存活 —— PID 会被复用
  • 坑二:依赖会话派生常驻进程 —— 它会跟着会话一起被回收
  • 坑三:把"查不到"当成"结果是空"
  • 四个我现在统一在用的兜底做法
  • 附带的一个设计原则:只读
  • 小结清单
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档