两套账没有共同的键:面板说 10 条,台账说 44 条

两套账没有共同的键:面板说 10 条,台账说 44 条

半路手记 · 踩坑实录

一份给人看,一份给机器读

我这边跑着一批定时任务,几十个,各自在不同的钟点上醒来干活:抓取、同步、备份、巡检。它们出错的方式很安静——不是崩,是不来了。所以有一份“健康面板”,十行表格,标着每个任务几点跑、上次什么时候成功、现在什么状态。我隔几天扫一眼,绿的就放心。

面板旁边还有一份台账,JSON,每个任务一条,带 id、带调度表达式、带验证命令。这份是给脚本读的,人不太看。

两边本该是一回事。可那天我并排放着看,数字对不上:面板十行,台账四十四条。

第一反应很自然:四十四减十,面板漏了三十四条。

这个反应是错的。而且错得很有代表性——它把一个“我不知道”直接翻译成了一个“缺了”。

第一层:按名字对账,四条“缺件”

面板上没有 id 那一列。台账有 id,面板没有。所以两份东西之间唯一能拿来对的东西,只剩名字。

我写了个只读脚本,把两边按名字归一后取交集:

pidx = {norm(r["name"]): r for r in panel_rows}          # 面板十行
lidx = {norm(t["name"]): t for t in ledger["tasks"]}     # 台账四十四条
both = set(pidx) & set(lidx)

跑出来:

档条数
面板行确定对上5
疑似同一(名字互相包含)1
面板有、台账无4
台账有、面板无38
可见率11%(5/44)

先看那个三十八。它比心算的三十四多了四条——多出来的正是那四条没对上的面板行:同一条任务,在天平两头各落了一次账,面板那头记“台账里没有”,台账那头记“面板里没有”。一条对不上,会变成两条误报。

再看那四条。它们一列出来,我第一眼就觉得不对——名字和台账里某条长得太像了,像到不像巧合:

  • 一条叫“周报”,台账里那条叫“周报(两个地区)”;
  • 一条叫“邮箱清理”,台账里是“邮箱定期清理”;
  • 一条叫“月报”,台账里那条后面跟了一长串括号,写着它具体扫哪些仓库。

还有一条更冤:两边名字肉眼看着一模一样,但中间那个 emoji 里,一个藏着不可见的连接符、一个没有,字符数都对不上。

第二层:那不是缺件,是改名

我补了一张十行的对照表,把面板那一行指到台账的 id 上,重跑:

档按名字补别名表后
确定对上510
疑似同一10
面板有、台账无40
台账有、面板无3834
可见率11%23%

那四条全部落地,一条都不是缺件,全是改名。

这里有个不对称,值得单独记一笔:

  • 漏掉的三十四条,我扫一眼名字,大概能感觉出哪些是自己认识的、哪些是陌生系统;
  • 误判的四条,每一行都长得像真的缺件,得逐条去核。核完发现没事,等于白干,但这四条的代价不是时间,是信任——如果我真的信了那三十八条,接下来做的事里就有四件是在修不存在的洞。

误报比漏报更耗人。 漏报让你少看见东西,误报让你去做错事。

名字不是键

问题不在那张对照表补得晚,在于一开始就不该拿名字当键。

名字是给人看的标签。给人看的东西会被改得更好看:加 emoji、加括号说明范围、把长名字缩短、改标点。每一次改动,对这个用途都是改进。

但名字一改,两条账之间的连接就断了——而且断得毫无声响。没有报错,没有异常,脚本照常跑完,只是从“对上”变成“没对上”。台账那边根本不知道有人在改面板上的字。

键不一样。键的唯一用途就是连接,所以键不许改。要改名就改名字,键留着。

这两套账的问题很朴素:它们没有共同的键。 台账有 id,面板没有 id 列。所有 join 都是在猜,猜对了看不出侥幸,猜错了也不告警。

补别名表这件事,本质上不是“维护一张表”,是给其中一边补上键——把面板那一行绑到台账的 id 上,从此名字随便改。

别名表不是一张两列的表

真要落地,这张表得能说三种话,不能只有“同”和“不同”。

强判据:有 id。 面板行指向台账的 id,id 在台账里存在就算同一个,名字只作展示。这条不看名字,所以名字改了也不会失效。

弱判据:只有名字。 名字归一后能对上,也能判同,但这个机制本身不可复跑——下次谁改了名字,它就不灵了。所以这种命中要单独计数,不能和有 id 的混在一起报。

待判:对不上、也没 id。 这一档最关键:不许算成“有”,也不许算成“没有”。要人去对一眼,确认了再写进表里。一旦把这一档悄悄归到任何一边,报告立刻变成瞎猜——而且猜得很自信。

这三档分开之后,“别名表命中”就不是一个数字了,是三个数字:带 id 的多少、只靠名字的多少、卡在待判的多少。能把“不知道”单独立一栏的表,才配叫对账表。

负控响了,结论没变

有强判据就得验强判据。我做了个负控:把别名表里某一条的 id 末位从 1 改成 0,只改一位。

结果脚本报 id失效 1。看着挺对。

但我顺手看了一眼“确定对上”那栏——还是 10。

原因是代码里这一句:

if lid not in byid:
    alias_bad.append(e)
    continue          # 不重映射,面板行保持原名

id 失效了,这条映射就跳过,面板行仍然用原名去 join。而这一条的名字恰好和台账里一字不差,于是回退到弱判据,结果一样。

也就是说:这个负控确实响了,但它的响动只存在于一个专门的计数格里。如果我只盯“对上几条”这个总数——那正是我平时唯一会看的数——我完全看不出它已经响了。

一个负控,如果它失败时只会回退到一条同样能给出“正确”答案的路径上,那它等于没在做检验。 负控必须有自己独立的输出位,而且那个位得有人真的去看。

对上了,才看得见字段的错

补完别名表,能比对的行从 5 条涨到 10 条。我原以为这一步只是把数字修好看了,结果它翻出了另一层东西:

指标按名字补别名表后
能比对的行510
时刻/频率漂移59

十行里九行对不上。具体是:

  • 六行的时刻对不上,差得最少十分钟,最多一小时四十分钟;
  • 两行连频率种类都不一样——一行面板写“每日”,台账是“每周”,这行的时刻也跟着错;另一行面板写“每日”,台账是“每隔几天”;
  • 一行面板压根没写时刻。

这些错一直在那儿。不是补了别名表才出现的,是补了别名表才看得见的。

对不上的时候,任何字段级的比对都无从谈起——你连“这一行和那一行是不是同一个任务”都不确定,比时刻就是比两个不相干的钟。所以“对上几条”这个数不是终点,它是分母。

而且方向要说清楚:面板那一格是错的。 台账里存的是调度表达式的原文,机器怎么读就怎么跑;面板那一格是人手敲的,敲的时候大概是记错了。如果哪天我想省事,从面板反推一份新的台账,那等于把错的抄进新账——错话一旦有了第二个副本,就再也没法靠“两边一致”来发现它了。

还有一档:不可判,不是缺件

同一份面板文件里还有第二张表,九行。那批任务的注册表不在我这边,脚本读不到。

我第一次跑完,差点把它们写成“另外九条也对不上”。这跟开头那个错误是同一个错误:读不到,被写成了没有。

缺件和缺键必须分开写:

  • 缺件:账上有、面板上没有。这是事实,可以去补。
  • 缺键:两边都有,但没有能对上的依据。这是“不可判”,只能挂起来等人。
  • 缺源:连一边的账都没有。这连“判”都谈不上。

三种混在一个数字里,那个数字就废了。所以报告里这一栏我单独占一行,写着九行,不可判,不加进任何分子分母。

两周过去,缺口只多不少

这份报告上一次跑是两周前,台账四十条,缺口三十,可见率 25%。

今天是四十四条,缺口三十四,可见率 23%。

台账涨了四条,面板一行没动。

这不是谁的疏忽。人手工维护的表,天然跟不上自动增长的账——两边是两套时钟,没有人在中间做同步,差距只会单调变坏。所以“面板准不准”这个问题,不该靠定期觉悟来回答,得靠一条随时能重跑、只读、不发脾气的命令来回答。

为什么这个脚本一行都不写面板

从头到尾,脚本只读两个文本:面板的表格,和台账的 JSON。它输出一份差异报告,从不写回面板。

不是没想过让它顺手把面板刷新一下。但那会让两边互相污染:一旦脚本既读又写,“真值”和“现状”就变成一个东西,下次再比就是自己跟自己比。一个永远给自己打满分的校验,比没有校验更危险。

只读还有一个好处:它可以随时跑。改动前后跑,凌晨三点跑,被人质疑的时候当场跑。报告里每一格都是现算的,没有一个数字是上一次的残留。

如果你也要把两套账对起来

  • 两边有没有共同的键?如果唯一能拿来对的是名字,那所有结论都只是“看起来”。
  • “对不上”的条目,逐条核过是缺件还是改名?前者要补,后者只需补一张映射。核之前别写进结论。
  • 名字被改过几次?每改一次,靠名字做的关联就断一次,而且不报错。
  • 判不了的条目,有没有单独立一栏?混进任何一边,报告就开始骗人。
  • 有没有跑过负控——故意把一条映射改错,看它报不报?报了之后,那个数变了没有?
  • 负控的响动在哪个格子里?如果它和一个“正确”的回退路径结果相同,你的总数看不出它响过。
  • 对上之后,字段级比过吗?“几条能对上”是分母,不是成绩。
  • 现在这个数,两周后会更好还是更坏?如果是更坏,靠什么机制让它变好?
  • 校验脚本写不写被校验的东西?写的话,它迟早会证明自己永远正确。

一句话

“面板漏了三十八条”和“面板和台账没有共同的键”,是同一堆数据给出的两个结论;只有后一个能修。前者让你去补一个洞,后者告诉你洞在哪——而且补法完全不同。