Files
blog/content/posts/literal-not-facts.md
T

7.6 KiB
Raw Blame History

title, description, slug, date, lastmod, tags, categories
title description slug date lastmod tags categories
认字,不认事 这三天修了一串问题:护栏靠组名里的两个字做判断、派单拿脱敏手机号和完整号比、文档写着从未生效、配置写了就以为生效了。它们形状完全相同——都在认字,不认事 literal-not-facts 2026-10-14T18:53:13+08:00 2026-10-14T19:20:00
字面匹配
静默失效
防腐
教训
运维手记

先讲一件尴尬的事。

前天我改了一个配置:某个城市的寄修业务,归属从"寄修组"调整为"上海长租组"——这是正确的改动,验证过,派单行为零变化。

第二天,一张工单推错了群。

地址在南京的跨站寄修单,被推到南京群、@了南京的同事——而这单按新规则应该只通知上海的备件工程师。

查下来,肇事者不是我的改动本身,而是一个一直躺在那儿的护栏:

if "寄修" in ownerGroupName:
    ...  # 寄修单不走城市推送

护栏的判断依据,是组名里的两个字。

我把组名从"寄修组"改成"上海长租组"——名字里没有"寄修"了,护栏当场失明。

更讽刺的是:系统其实知道这单不对。推出去的消息里,自带一行告警"⚠️ 维修站与地址不匹配"。它看见了,但没有拦。

为什么会犯这个错

我改配置时,验证过"派单行为零变化"——但只验证了直接消费这个配置的脚本。

而那个护栏在另一个脚本里,它读的是同一个字段(处理组名称),只是用途不同。

改配置必须扫消费方——判据是"谁读这个字段",不只是"谁调用这个函数"。

修复也干脆:护栏不再认组名里的字,改成比较结构性事实——处理站所属的城市,和客户地址所在的城市,是不是一地。跨站就按处理站路由给对应分公司的备件工程师。

顺着这条线,越挖越多

护栏修完,我把最近三天的修复摆在一起看,形状完全相同:

① 脱敏手机号,和完整号比

某天开始,派单平台的接口返回的手机号变成脱敏形态:138****5678。

而本地配置里存的是完整号。

代码做的是双重校验:"你包含我吗?我包含你吗?"——脱敏号和完整号,既不包含,也不被包含,永远判"不符"。

于是工程师被跳过,派单动作从不执行,工单停在待接单。而这一切只留下一行 warning,不阻断——静默跑了很多天。

修复:识别到脱敏形态时,只比对可见的前缀和后缀。既有校验的意图(防同名不同人)保住了,字面不相等的问题也解了。

② 文档写着"从未生效"

查派单问题时,我先入为主写下一个结论:"这功能从未生效"。

复盘时被自己打脸:日志里躺着 51 次成功记录(持续一个多月),最后成功在三天前,失败从两天前开始。

真相是:平台在三天前改了行为(开始返回脱敏手机号),功能从"一直好用"变成"突然失效"——这是回归,不是"从未生效"。

更扎心的是:那句"从未生效"就写在我两小时前刚更新的文档里。

推翻自己结论的时候,最先要改的就是刚写的文档——它最容易带着旧结论流向后人。

(顺带还纠正了第二个错误结论:之前说"那几张单卡在待接单",实际去平台一查,平台自动派单兜了底,人选都对,业务没受损。我拿日志里的文案当了事实。)

③ 配置里写了,就以为生效了

还有一个延保拦截需求:某型号笔记本键盘掉键,厂家有延保政策,要在派单时拦截。

查完发现,配置里早就写了这个型号——但拦截从未生效过。

为什么?配置匹配的字段是"物料名称",而这个型号的物料名称是一串硬件配置(CPU_内存_硬盘_屏幕_颜色),型号本身根本不在里面。

型号的真实来源,是另一个字段。

"配置存在"≠"配置生效"。 判据必须是实测日志和产物,不是"配置里写了"。

④ 响应缺字段,就判失败

还有一种:关单接口返回的响应里缺一个字段,系统就判定"关闭失败",提醒操作人去补操作——实际工单早就关上了。

照着告警去做,是无效动作。

修复:响应缺证据时,回查真实产物(工单现在什么状态),再定性。有意思的是,修复上线当天系统又报了两条"关闭失败",回查后发现这次是真的关不上——修复的价值不在少报警,而在能分清真和假。

一条线串起来

这五件事,领域完全不同:派单路由、手机号校验、文档、延保配置、接口响应。

但形状是同一个:

它判断的"字" 被它错杀的事实
组名里有没有"寄修"两个字 一整类单子的路由正确性
脱敏号和完整号是否相等 派单动作执行没执行
文档里那句"从未生效" 51 次成功记录
配置里写了型号 拦截从未生效的事实
响应里有没有那个字段 工单其实已关闭

它们都在认字,不认事。

字是便宜的字面量:一个子串、一次相等比较、一句写死的结论。事是昂贵的真相:要读结构、要看时间线、要回查产物、要数双向的记录。

认字快,认事慢。所以系统里到处都是认字的判断——直到有一天,字变了,事没变,或者事变了,字没变。

怎么防

这轮修完,沉淀了几个可复用的判据:

1. 判断依据优先用"结构性事实",不用"字面子串"

组是不是寄修组,看它的归属城市,不看名字里有没有那两个字。名字是给人看的,随时会改。

2. 改配置,扫"谁读这个字段"

不扫消费方的配置改动,是埋雷。而且消费方可能不看配置本身,只看配置派生出来的数据。

3. 说"从未/一直"之前,先数双向计数

看到 6 次失败就说"从未生效"?去数数成功了多少次。只数一边的计数,是幻觉。

4. 拿不准,回查最终产物

响应缺字段 → 回查工单状态;日志说失败 → 核平台实况;文档说过期 → 探一次接口。产物才是事实,其余都是转述。

5. 文档结论要带"何时验证过"

不带时间戳的结论,你不知道它是刚验证的还是两年前的。这次我把时间线(最后成功时间、首次失败时间)直接写进了文档——让下一个读者能看到证据链。


结尾

这轮还有个小插曲:写文档时我一度想用一个"聪明的启发式"——自动推断某城市是否跨分公司。单测立刻抓出反例:有位同事同时归属两个城市,启发式会把另一个城市的真漂移静默吞掉。

最后改成显式配置,老老实实列出名单。

假阴性比噪音更危险——噪音至少看得见,静默的漏判什么都不会留下。

认字,还是认事?

字快,事慢。但运维这行,慢的那个才是对的。


这两天的工作量:30+ commits 跨 5 个仓库。护栏修复 + 同型(超期预警)双修、脱敏手机号校验重写(8 场景单测)、SKILL 文档更正、延保拦截上线(挖出死配置真源)、4 类假 FAIL 清零(响应回查定案)、跨分公司显式配置、闲鱼巡检 6 类静默缺陷修复。

最值钱的一句:护栏认的是字。字一变,护栏就替你保守着一个已经不存在的世界。