Posts 每日巡检的假红:三周里修掉的几处误判
Post
Cancel

每日巡检的假红:三周里修掉的几处误判

  采集跑起来后,我给它配了每日巡检脚本 audit-daily.mjs,每天凌晨由 pm2 的 cron 拉起来,结果推到 Telegram。它分两段:归档前的 pre 审昨天数据,归档后的 post 审刚归档的那天。检查项按字母排。A 看采集进程实时健康,B 看结算喂价有没有大段断档,D 看盘口有没有交叉,F 最重要:拿我存下来的 Chainlink 喂价,按官方规则重算每个已结算市场,再和官方结果逐个比较。另外还有两个外部锚点,① 用 Polymarket 的 CLOB REST 盘口对照我重建的盘口。

  从 6 月中旬到 7 月初,巡检红了好几次,数据却都没问题,是检查本身判错了。这里记一下这三周修过的几种误判。

F:边界秒缺 tick,邻居说明不了什么

  F 的规则是:市场结束那一秒的价格 ≥ strike 判 Up,否则判 Down。喂价大约每秒一条,正常情况下结束那一秒(feed_ts_ms == end_sec)会有一个 tick,直接拿来比就行。

  麻烦是这一秒偶尔会缺。6 月中旬第一次碰到时,我加了 indeterminate(不可判定)状态:边界秒缺 tick,而前后两个邻居分别落在 strike 两侧,就不裁决,直接在报告里记成不可判定。

  6 月 19 号又红了一次。06-18 的一个 btc 5 分钟市场,relay 上 chainlink 那个 topic 单独停发 10 秒,正好跨过结算边界。同一时间盘口流一直在进,说明我的连接没断,是上游单个 topic 断供。边界秒缺了,前面的邻居也缺了,只剩后面一个孤零零的 tick。旧逻辑只处理了「两边都有、分跨两侧」,没处理「只有一边」,于是拿这个孤立 tick 裁决,结果判错。当时改成:边界秒缺失时,只有两侧邻居都在并落在同一侧才裁决;单侧缺失或跨两侧,一律 indeterminate。

  结果三天后又红了。06-21 的 doge-updown-5m-1782001500,边界秒前后有 8 秒的洞,前一个 tick(-5s)是 0.0835079,后一个(+3s)是 0.0835072,strike 是 0.0835064。两个邻居都在 strike 上方,只高出大约 1e-6,价格贴着线在抖。按我 19 号的规则,两侧同侧,可以裁决,判 Up。官方判的是 Down。

  两个同侧的 tick 也不能确定边界秒的价格。中间那 8 秒,价格可能跌破 strike 后再回来,结束那一秒正好落在下面。06-22 我把这条兜底也删了:边界秒没有精确 tick,一律 indeterminate,不再看邻居。

  不可判定的数量在有断档的日子会涨,06-21 涨到 35 个,06-20 涨到 28 个,大多是当天上游断流那段时间里结束的市场。这些只披露,不算红。判据前后改了三次。

① 和 A:巡检撞上了瞬时状态

  另外几次假红是时间点的问题:巡检读到了一个几秒后就会自己恢复的状态。

  ① 外锚要把活跃市场重建出来的盘口和实时 REST 做对比。重建和 REST 之间有 flush 延迟,行情快的时候,会瞬间重建出一个交叉盘口(bid >= ask),或者和 REST 偏差超过 0.05,大约 1 秒后就好了。改成最多重试 3 次、间隔约 2 秒,只要拿到一次干净的样本就过。但如果穿越一直持续到最后,那说明重放确实不一致,要报红,不能跳过。

  A 检查采集进程的盘口连接数。06-21 读到 28/42,06-20 读到 28/38。查了一下才发现,pre 巡检的 cron 是 02:30 UTC,post 是 02:45,这两个时间点恰好都是 5 分钟和 15 分钟市场同时轮换的边界。新一批市场的 WebSocket 还没握手完,巡检就去读了 /health,几秒后就恢复成 28/28。同样加了重试,最多 5 次、每次 2 秒,连接数一直不够才判红。

  06-22 部署的时候又撞了一次,报的是「A health 不可达: fetch failed」。部署用 pm2 delete + start 重建了巡检 app,而 cron 型的 app 一启动就会立即跑一次;同一时间采集进程也被 delete + start 了,health 端口还没绑上。采集进程还配了 max_memory_restart 1500M,以后偶发的内存重启也可能撞上巡检。fetchHealth 统一加了重试,最多 6 次、每次 3 秒,持续连不上照样判红,因为那才是真的断线。

  这几处重试都有上限,到上限后仍然 fail-closed。

D:锁定盘口不是交叉盘口

  7 月 4 号,D 报了交叉簿:xrp-updown-5m-1783171500 两条腿,结算前大约 33 秒,快照读出来是 0.99/0.99,镜像那条腿是 0.01/0.01。

  这其实是上游的合法状态。一个快结算的涨跌市场,结果基本定了,买盘堆到 0.99,还有残余的 0.99 卖单挂着,bid 等于 ask,是锁定盘口(locked),不是交叉盘口(crossed)。旧判据写的是 bid >= ask,把锁定也算进去了。

  D 读的是每个 token 当天最后一条 book 快照。快照逐字入库,中间没有合并和重放,所以在单个快照上抓不到我自己的 bug,只会命中上游的真实状态。我把判据改成严格的 bid > ask,锁定盘口放行。拿 07-04 重跑后,D 从「穿越 2 个」变成「穿越 0 个」,之前留下的失败记录文件也自动清掉了。

warn 分两档

  修了几轮后,报告里的黄色(⚠️)越来越多,而且不是一回事。有些需要我去看,比如回填放弃、断档超过 600 秒、未结算市场超过 5 个;有些只是披露,比如 F 有几个边界不可判定、外锚这次跳过了,我看到也做不了什么。

  06-20 把 warn 分成两档:needs-look 仍然走 warn(),纯披露的走新加的 disclose()。只有没有 fail、没有 needs-look,只剩披露的时候,报告标题才加上「(无需处理)」。一旦出现一条需要看的,这个后缀就去掉。❌ 仍然表示失败。Telegram 消息也改成每项检查单独一行,原来是用 `` 拼成一长行。

查断流时发现日志没了

  06-21 有一次上游断流,我想翻采集进程的日志看看当时发生了什么,结果一个历史归档都没有。

  日志轮转用的是 pm2-logrotate,配置是每整点轮转、retain=7。我一开始怀疑是把日志放在项目自定义目录 logs/ 的缘故,后来才确认根因是文件名。pm2-logrotate 清理旧归档时,是按 <basename>__ 在同一个目录里做 substring 匹配的。采集进程的日志叫裸的 out.log / error.log,前缀是 out__,于是同目录下所有 *-out__ 的归档(导出、巡检、交付 bot 的)都被算进同一组,一起按 7 个的上限清理。采集主日志的归档刚生成就被当成超量删掉了。默认的 ~/.pm2/logs 不会中招,因为那里的文件名是 app-name-out,前缀又长又唯一。

  我改了三处:

  1. 所有日志名加唯一前缀,采集进程改成 chainlink-data-{out,error}.log,以后新加 app 也不允许用裸名。
  2. pre 和 post 两个巡检原来共用一个 audit-*.log,两个进程写同一个文件,pm2-logrotate 从两边轮转、截断、删除,互相踩。拆成各自的文件。
  3. 新加一个 cron app chainlink-data-logarchive,每 15 分钟把已经轮转完的采集日志硬链接到 logs/archive/,保留 14 天。只链接轮转完的完整归档,不碰正在写的活动文件,所以没有竞态;硬链接在原文件被删之前就建好,原文件被删了它还在。

  第 3 条是因为 retain=7 加上每小时轮转,热日志只能留大约 7 小时,而巡检是次日凌晨才跑,出了事往往隔十几个小时才去看。这个归档脚本也是 fail-closed 的:只忽略 ENOENT / EEXIST 这种良性竞态,其他错误都计数、写 stderr、退出码非 0,让 pm2 那边能看到它红了。它和数据库共用一块盘,清理要是悄悄失效,占用就会无限增长。

  改了 out_file 之后,pm2 restart 和 reload 都不会重新读配置,必须 pm2 delete 再 pm2 start ecosystem.config.cjs --only <app>。


outcometick 工程笔记(3/21)

上一篇:给数据做每日巡检:8 项内检、3 个外锚和一次上游断供

下一篇:归档放在 GitHub Release 上,先后撞上的两个上限

This post is licensed under CC BY 4.0 by the author.

给数据做每日巡检:8 项内检、3 个外锚和一次上游断供

归档放在 GitHub Release 上,先后撞上的两个上限