跳转至

日志还在滚动,数据已经死了:一次静默停更的完整解剖

现象

我的量化系统每天有一组定时任务:盘前更新行情、刷新因子组合信号、跑健康检查。从任何表面证据看,它都活着——定时器每天照常调起,一部分日志文件照常滚动,系统服务无报错。

直到我做例行巡检才发现:行情数据停更了半个多月;一个组合信号"看似在更新"其实从未真正更新过,实际停更起点要追溯到两个多月前;另一个组合信号停更得更早,起点在三个月前。中间没有人发现,包括每天在看它产出的我。

更有意思的是"假活着"的证据结构:系统日志显示任务每天照常被调起;唯一还在按时滚动的日志,恰好属于数据早已死掉的那条链路。 运行证据和产出真相,指向了相反的结论。

直觉判断的盲区

"每日自动更新断了"这个现象摆在面前,直觉给出的怀疑清单几乎固定:定时任务没起?网络挂了?脚本被误删?——这份清单有一个共同前提:故障会留下痕迹。坏了就该有报错,没报错就是没坏。

这次事故专门打破这个前提:定时器每天照常调起(系统日志可查)、一部分日志每天照常滚动(文件可查)、健康检查甚至还在告警(假告警)——所有"运行中"的表面证据都正常,且每一项单独拿出来都足以打消怀疑。正确的理解是把监控对象从"过程证据"换到"产物证据":任务在运行 ≠ 数据在更新,每一步成功 ≠ 链路产出新鲜;那些照常滚动的日志,甚至不是活着的证据,而是重定向失败前留下的最后动作。

当初环境变量块为什么被挪到任务之后、兼容包装为什么只 import 不调用,已无法还原——大概率是几次"顺手改一改"的叠加产物。教训不在某一处手滑,在于整条链路从头到尾没有一个环节核对过"产出到底新不新鲜"。

排查与关键证据

排查方法三件套:影子复跑(只读探针,不改任何产物)、系统日志时间线取证、和定时任务同款的干净环境端到端复现。

第一层:机器级根因——"重定向先于命令体"。 检查线上定时表发现:环境变量块被追加式改写挪到了任务条目之后。而定时任务的环境变量只作用于其后的条目——于是前面几条任务里的日志目录变量展开为空串,重定向目标变成文件系统根目录,打开重定向时就报"权限拒绝",命令体一行都不会执行。关键反转:系统日志里这些任务每天照常出现——"任务被调起"和"任务执行了"是两回事。最小复现只要一行:清掉该变量再执行重定向,当场复现权限拒绝。

日志考古和这个结论严丝合缝:几条受影响链路的日志文件集体停在同一天(月底的一个周五),恢复于修复后的第一个交易日,中间只有一次手工诊断运行——而排在环境变量块之后的那一条任务,日志一路连续,从没断过。

第二层:脚本级根因——三处叠加的小故障。 修好定时表后端到端复跑,链路恢复了,但进一步检查发现更深的问题,三处叠加:

  1. 数据更新脚本的新鲜度判断差一:本地恰好落后一个交易日时,被误判为"已是最新"→ 从此永久停止更新——"永久停在昨天"这种故障模式,比"彻底坏了"难查得多;
  2. 定时环境里裸写的解释器名解析到了系统解释器(没有装科学计算库),流水线第六步崩溃,而 set -e 让整条盘前链路跟着中断;
  3. 一个兼容包装脚本只 import 了目标模块、从不调用它的主函数,加上模块内根目录推导少一层,路径解析失败后静默退出——组合信号自那时起再没真正更新过。

第三层:告警系统自身不可信。 本来健康检查应该兜住这一切,但复盘发现它的读取窗口用日历天、评估窗口用交易日,两个口径不一致,底层交易日历还停更了——于是它报出了一次"漏采"假告警(事后核查当天实际入库量完全正常),而真正的缺口日反而没人管。报警通道自己病了,比没有报警更糟。

根因

  • 机器层:定时任务环境变量的作用域语义 + shell 重定向的失败时机(在命令体之前)。
  • 脚本层:三处独立小故障。每一处单看都"不算大事",叠加起来等于完全停更——叠加故障的可怕之处不是任何一环的难度,是"每环都过得了 code review"。
  • 组织层(真正的根因):整条链路没有"产物新鲜度契约"。每一步成功 ≠ 链路产出新鲜;任务在运行 ≠ 数据在更新。监控的对象从一开始就选错了。

约束与取舍

修复动作背后各有取舍:

  1. 加新鲜度闸门,放弃静默容错。 组合信号刷新后自检:信号日期必须追平行情库的最新交易日,落后则非零退出。代价:定时任务会开始"报错",报警噪音上升,隐含承诺是必须有人响应。这个代价是故意买的。
  2. 失败隔离,放弃 set -e 全链硬失败。 流水线第六步改为:失败不再中断整条盘前链路,后续步骤用已有信号兜底,但不标记完成以便重试。代价:下游可能用到旧信号——用"不标记完成 + 可重试"补偿,而不是假装没发生。
  3. 日志路径写死绝对路径,放弃配置的可移植性。 换取对环境变量顺序的免疫;同时给定时表安装脚本加自检:环境变量必须全部位于第一条任务之前。
  4. 兼容包装脚本的验收标准重写:这类"转发脚本"的完成定义是它真的调用了目标函数并产生了输出,而不是"import 没报错"。

可复用检查清单

  1. 监控产物新鲜度(产物日期必须追平最新交易日),而不是监控"任务是否运行/日志是否滚动"——后两者都可以在数据死了的时候继续演戏;
  2. 定时任务的环境变量必须全部位于第一条任务之前,或者干脆写死绝对路径;
  3. 新加任何"兼容包装/转发脚本",验收标准是真调用 + 真输出;
  4. "恰好落后一期被误判为最新"是新鲜度判断的经典差一陷阱——判断逻辑必须能区分"落后零期"和"落后一期";
  5. 告警阈值用相对量(环比/同期基准),绝对阈值在非平稳数据上必然假告警;每条告警规则上线前先自检它自己的窗口、口径、日历定义是否与被监控对象一致。