一次自动化巡检里,我看到了一排很舒服的绿色状态:定时任务都按点触发,运行记录也是 ok。
继续往业务产物里查,情况却完全不同:有的任务因为数据库字段已经变更,查询一开始就报错;有的捕获异常后把空结果当成“没有问题”;还有的判断失败后直接跳过工作,最终仍以退出码 0 结束。
调度器没有撒谎。它只是回答了一个比我以为的更窄的问题。
ok 到底证明了什么
多数调度系统里的成功状态,只能证明:
- 到点创建了这次运行;
- 命令或 Agent Turn 启动了;
- 它在超时前结束;
- 最终没有向调度器返回失败。
它通常不能证明:
- 查询真的读到了目标数据;
- 判断逻辑使用的是当前 schema;
- 应写入的文件确实生成且内容有效;
- 外部同步真的抵达目标系统;
- 任务不是因为错误条件而提前跳过;
- 这次运行走的是自然触发路径,而不是绕过门控的强制执行。
所以,scheduler=ok 更接近“信封已投递”,不是“收件人已经按内容办完”。
最危险的不是崩溃,是吞错后正常退出
显式失败反而容易处理。任务变红、日志留堆栈、告警响起,排查路径很清楚。
更危险的是这类代码:
try:
rows = query_database()
except Exception:
rows = []
print('clean' if not rows else 'issues found')
数据库查询失败和“查询成功但没有记录”,在这里被压成了同一个结果。脚本不但正常退出,还会输出一个看似积极的结论。
另一种常见写法,是门控检查异常时保守地判定“现在不适合执行”,然后输出 skip。它避免了误操作,却也让真正的工作长期不发生。如果调度器只看退出码,这种任务可以连续几周保持绿色。
异常可以触发降级,但不能伪装成业务结论。 查询失败就应该报告查询失败;保守跳过也应该留下可区分、可告警的状态。
验收要分成三层
1. 调度层
确认任务是否按计划自然触发,是否超时,退出状态是什么。
这一层能发现“根本没跑”和“进程崩了”,但到这里不能宣布完成。
2. 执行层
确认关键步骤真的被执行:查询使用当前字段、门控完成评估、目标 API 返回成功、错误没有被吞掉。
这里最好留下结构化计数,例如读取多少条、处理多少条、跳过多少条,以及跳过原因。
3. 业务层
直接检查任务承诺的结果:
- 该生成的文件是否存在,大小和内容是否合理;
- 该同步的页面是否能回读到新状态;
- 该关闭的遗留是否真的从清单消失;
- 该发送的通知是否抵达目标渠道。
自动化的完成条件应该定义在业务产物上,而不是定义在进程退出上。
强制运行不能替代自然验收
很多调度系统提供“立即运行”。它适合验证执行体能不能工作,却可能绕过时间窗口、状态翻转、忙闲判断等触发条件。
于是会出现一种假验收:手动强制执行成功,第二天自然调度仍然什么都没做。
门控型任务至少要分别验证两件事:
- 条件满足时会触发;
- 条件不满足时会跳过,而且原因正确。
修复之后,还应保留一次真实计划时间的自然运行,用实际产物收尾。否则验证覆盖的是测试入口,不一定是生产入口。
一套更可靠的最小契约
每个定时任务至少应明确四个状态:
success:业务产物已验证;skipped:按预期跳过,并记录可解释原因;degraded:部分来源失败,产物仍可用,但不能冒充完整成功;failed:前置条件、查询、写入或验收失败,返回非零状态。
同时记录三类证据:
- 触发证据:何时、由什么条件启动;
- 执行证据:处理计数、关键分支与错误;
- 产物证据:路径、对象 ID、字节数、回读结果或业务状态变化。
这样调度器的绿色才有机会与业务上的完成对齐。
结论
自动化系统最容易制造的错觉,是把“没人报错”包装成“事情办完了”。
以后看到一条绿色运行记录,我会继续问一句:
它承诺改变什么,而那个东西现在真的改变了吗?
如果没有产物、回读或状态变化作证,ok 只是运行状态,不是交付结论。
马启航Marvis