type
Post
status
Published
date
Oct 6, 2026
slug
summary
一次退出码为零的静默故障:定时器正常触发,三个下游出口却全部没动静。
tags
Write
静默故障
环境变量
排障方法
DSH 排障现场
category
DSH
icon
password
url
有一阵子,我每天早上的简报都按时"跑完"了。定时器在跑,退出码是 0,正文也落了档。直到我随手翻了一下手机,才发现天气没推、微信没发、滴答没同步——三个出口,一个都没响。
最难受的不是坏了,而是它看起来一切正常。我盯着日志看了很久,找不到任何一行报错。
一、四盏绿灯都亮着,只有内容区是白的
这套简报的结构很简单:一个主脚本按点被唤醒,里面依次调用三个子进程,分别去取天气、推微信、同步滴答。主脚本自己只做编排,真正的活儿在子进程里。
故障那天的现象是:
- 定时器确实触发了,时间点对得上;
- 主脚本退出码 0;
- 简报正文正常写进了本地文件。
按常理,这三条同时成立,说明流程走完了。可下游三个出口全都静默。我第一反应是"下游服务挂了",去查微信和滴答那边,一切正常。又怀疑是不是网络问题,手动跑了一遍,三个出口全通。
手动能跑通、定时跑不通——这个反差是整件事的转折点。
二、裸命令找不到,是因为环境里根本没有 PATH
我把定时器实际拿到的环境变量打出来看,愣住了:里面只有一项,是一个 SSH 相关的 socket 路径。没有 PATH,没有 HOME,什么都没有。
这就是根因。定时器由系统级的调度机制拉起,它给的是一套极简环境,不是你在终端里习惯的那套。主脚本之所以能跑,是因为配置文件里用了绝对路径去启动它——绝对路径不依赖 PATH,所以主脚本活了下来。可主脚本内部用裸命令名去起那三个子进程,调度器找不到可执行文件,直接抛 ENOENT。
ENOENT 是"文件不存在",但在这里它的真实含义是"在你的环境里找不到这个命令"。子进程一个都没起来,主脚本却认为调用已经发出,于是继续往下走,退出码 0。
退出码 0 只说明主脚本没崩,不说明它交代的事做成了。 这是我这次学到的第一件事。
三、日志里没有那一行,不等于代码没执行
修的过程中还撞上一个更阴的坑。我一度判断"兜底逻辑根本没执行",依据是日志里没有它该打的那行。后来才发现,是那个负责发通知的子脚本用了强制退出的写法,进程结束时标准输出还没刷盘,那行日志被丢掉了。
代码执行了,日志没留下。
这条反直觉的现象值得单独记下来:日志里没出现某行,不能反推代码没执行。 反过来也一样,日志里出现了某行,也不代表它后面的步骤成功了。日志是证据,但不是全部证据,尤其在进程被强制结束、输出被缓冲、或者输出流本身就被吞掉的时候。
我当时的误判,正是把"没有日志"直接当成了"没有执行",白白多绕了一圈。
四、三层修复,从子进程修到调度配置
定位清楚之后,修复分三层,从下往上:
第一层,子进程的启动方式统一改成用当前运行时的绝对路径去起,不再依赖裸命令名。这样无论环境里有没有 PATH,子进程都能找到自己。
第二层,在调度配置里显式声明 PATH,把系统最基础的几个目录补回去。这是给整个脚本一个正常的起点,而不是让它去猜。
第三层,简报本身加一个兜底:如果检测到运行环境异常,就走一条不依赖外部命令的降级路径,至少把核心内容落下来。
三层叠起来,才算把"环境缺失"这个根因堵住。只修一层都不够——只修子进程,别的脚本还会踩;只修配置,换个调度器又白搭。
五、验证要两条腿:最小环境模拟 + 真实端到端
修完最怕的是"以为修好了"。我用了两种验证,缺一不可。
一种是最小环境模拟:手动构造一套和调度器一样极简的环境,在里面跑主脚本,看它还能不能把三个子进程拉起来。这一步能复现故障、也能证明修复生效,但它只证明"进程起来了"。
另一种是真实端到端投递:让简报真的发一次,去微信那头确认消息到了。这一步才证明"事情做成了"。
只做前者,可能进程起来了但内容还是空的;只做后者,可能这次碰巧网络好,掩盖了环境问题。两条腿一起走,心里才踏实。
六、顺带说清:机器级定时该用哪种机制
这次也让我把本机的几类定时机制理了一遍,它们互不相通,改流程前得先确认自己用的是哪一类:
- 系统级调度:机器睡眠唤醒后会补跑错过的那次,关机错过则不补;
- 宿主进程里的看板任务:错过的触发点直接跳过,不补跑,而且界面上还一直显示"已启用",最难发现;
- 插件内置轮询:宿主不在线就停;
- 手动或会话触发:没有定时语义。
结论很简单:要"到点一定跑"的机器级定时,用系统级调度,别只挂在宿主进程的看板上。 看板适合"宿主在的时候顺手做",不适合"必须按时做"。
顺带还有一个同源的教训:一个从不生效的"绕过开关",比没有开关更危险。它会让人以为"我已经强制过了",把"被拦住"误报成"试过了"。逃生口这种东西,必须实测过再写进文档。
可迁移的判据
- 退出码 0 只代表主流程没崩,不代表它交代的事做成了。 编排层的成功和下游的成功是两件事,要分开验证。
- 定时任务的环境和你终端里的环境不是一回事。 子进程启动、外部命令调用,一律不假设 PATH 存在。
- 日志里没有某行,不能反推代码没执行。 强制退出、输出缓冲、流被吞,都会让日志说谎。
- 修完要两条腿验证:最小环境模拟证明"起来了",真实端到端投递证明"做成了"。
- 要"到点一定跑"的机器级定时,用系统级调度;宿主进程里的看板适合"顺手做",不适合"必须做"。
- 任何声称存在的逃生口,实测过再写进文档。 一个假的绕过开关,会把误判伪装成结论。