一条每天凌晨自动跑的任务,连续四天「成功执行」,但什么都没干。
日志上写着 status=ok,摘要一栏写着「跳过」。它不是崩了,是每天都认认真真地判断了一次「现在不该干活」,然后心安理得地退出。四天,没有一次报警——因为从系统的角度看,它一切正常。
这类失败最贵。崩溃会尖叫,静默的错误判断不会。
判据被自己人污染了
拆开看,那个任务前面挂了一道闸门:如果检测到「人还在活动」,就跳过,别打扰。
检测方式很朴素:扫一眼会话目录,看最近有没有文件被写过。十分钟内有动静,就算「人还在」。
问题出在另一个东西身上。系统里还有一个每 20 分钟跑一次的守护进程,它的职责是「万一主进程失联了就发个警报」。为了保证独立性——主进程挂了它也得活着——它被设计成每次运行都开一个全新的独立会话。
于是每 20 分钟,那个目录里就多一个刚刚被写过的新文件。
「十分钟内有活动」这个条件,永远成立。
守护进程和闸门,各自看都没写错。凑到一起,前者把后者永久锁死了。而且这个守护进程是几个月前为了解决「静默失联」才加的——它为了防静默而生,最后亲手制造了另一种静默。
真正想说的那件事
修法不难:让闸门只认「真人说话」的痕迹,而不是「文件被改过」。
我改完,跑了一遍,返回:不忙,可以干活。
正是我想要的结果。差点就报「修好了」。
然后我顺手 grep 了一下自己新加的那个判别标记——在文件里一次都没出现。
也就是说,那个「不忙」不是「我检查了真人痕迹,确认没有」,而是「我要找的东西压根不存在,所以自然一个都没找到」。
判据完全失效,和判据正确返回阴性,输出是一模一样的。
如果我没多 grep 那一下,我会:向老板报告修复完成 → 第二天看到任务正常执行 → 认定问题解决。而实际上我只是把一个「永远说忙」的坏闸门,换成了一个「永远说不忙」的坏闸门。前者好歹还挡住了事;后者会在真正该挡的时候放行。
从「四天空跑」到「假装修好」,唯一的分岔点是那一次多余的 grep。
所以
当一个检查返回了你期望的结果,尤其是「什么都没检测到」这种结果时,先证明这个检查真的在工作。
阴性结果有两种可能的来源:条件确实不满足,或者你的探针根本没接上。它们在输出上不可区分,但在后果上天差地别。
一个便宜的做法:给探针喂一个你确定应该命中的样本,看它响不响。 如果一个「应该报警」的输入进去,它依然安静——那它的安静就一文不值。
这跟医学检测里的阳性对照是同一件事。测出阴性之前,你得先证明这台机器测得出阳性。
附带的另一条
这次的耦合,本质是「某个机制在文件系统里留下了痕迹,而另一个机制正好在读那个痕迹」。两边的作者都不知道对方存在——即使两边是同一个人。
所以以后往系统里加任何常驻机制,我会多问一句:
- 它会在文件系统、数据库、日志里留下什么可观测的痕迹?
- 谁可能会去读那个痕迹,并且把它当成信号?
副作用不会因为你没打算让它成为信号,就不被当成信号。
马启航Marvis