返回博客

监控告警用了永远为真的条件:createtime 从不被 UPDATE 触碰

给某个定时刷新任务写了一套健康检查:token 快过期 记录超过 23 小时没刷新 → 触发告警。逻辑看起来无懈可击。上线一周后,同事反映告警每次都在 token 还剩十分钟时准时响,但手动确认 cron 任务其实一直在正常跑。

折腾了半小时才意识到:那条 AND createtime < now - 23*3600 从服务运行满 24 小时起就永远为 true

现象

告警条件:

SELECT * FROM platform_token
WHERE expiretime < UNIX_TIMESTAMP() + 600
  AND createtime < UNIX_TIMESTAMP() - 82800;  -- 23h

预期:两个条件同时成立才说明 cron 卡住了。 实际:停掉 cron,告警频率毫无变化。移除第二个 AND,告警频率同样毫无变化。

根因

去翻实际的写入代码(ThinkPHP 5 的 Model):

if ($existing) {
    // UPDATE 路径——只更新业务字段
    $this->save([
        'token'      => $result['token'],
        'expiretime' => time() + $result['expires_in'],
        'user_id'    => 0,
    ], ['platform' => 'xxx']);
} else {
    // INSERT 路径——才会写 createtime
    $data['createtime'] = time();
    $this->save($data);
}

createtime 只在 INSERT 分支里出现。ThinkPHP 的 save($data, $where) 只更新 $data 里有的字段,不会自动 touch 其他列。

结果:行插入后,createtime 被永久冻结在创建时刻。服务运行超过 24 小时后,这一列对任何行都满足 < now - 23h,AND 条件退化为恒真——相当于直接写 WHERE 1=1

坑:列名叫 createtime 不代表它会被 UPDATE 触碰

createtime / created_at / inserted_at 这类命名暗示「写入即冻结」。ORM 的 partial-update 路径(ThinkPHP save($data, $where)、Laravel update([...])、Hibernate dirty-check)只写你显式传入的字段。依赖这些列来判断「最近有没有被刷新」,等于用一把永远不走的时钟来测速。

正解

选一个更新路径实际会写入的列作为新鲜度基准,优先级如下:

  1. 专用心跳列last_run_at / last_refreshed_at。在 UPDATE 路径里显式赋值,语义最清晰。
  2. 业务值倒推时间:token 场景里 expiretime 每次刷新都会前移。用 expiretime - TTL 反推上次刷新时刻:
    -- 如果 expiretime 距 TTL 前推超过 23h 仍没动,说明 cron 卡了
    WHERE expiretime < UNIX_TIMESTAMP() + 600
      AND (expiretime - 7200) < UNIX_TIMESTAMP() - 82800
    7200 为 token TTL,按实际值替换。)
  3. updatetime / updated_at:前提是 ORM 的自动时间戳已启用(ThinkPHP 需 protected $autoWriteTimestamp = true;Laravel 默认开启)。不能假设,要确认。
正解:上线前做一次列新鲜度核查

在 DB 上跑一条聚合查询,看候选列的值分布:

SELECT
  MIN(createtime), MAX(createtime),
  MIN(updatetime), MAX(updatetime),
  MIN(expiretime), MAX(expiretime)
FROM platform_token;

如果某列的 MIN 和 MAX 几乎一样、且远早于今天,这列就是「冻结列」,不能用于新鲜度判断。选一个值域随时间正常散布的列,或者直接加一列专用心跳。

排查信号

以下任何一条成立,说明你可能落入了同一个坑:

  • AND 条件看起来「是额外保护」,但删掉它告警频率不变
  • 把 cron 停掉,告警行为不变
  • DB 里该列的所有行都集中在同一个历史日期附近,尽管应用号称每天都在刷新它

一句话外卖

监控的新鲜度断言必须基于更新路径实际写入的列,列名是什么无所谓——先去读代码,确认哪条路径写了它。