post: 认字,不认事——五件形状相同的失效

This commit is contained in:
2026-10-14 18:55:13 +08:00
parent 0d031c3d8d
commit b5b0b4ecc9
+159
View File
@@ -0,0 +1,159 @@
---
title: "认字,不认事"
description: "这三天修了一串问题:护栏靠组名里的两个字做判断、派单拿脱敏手机号和完整号比、文档写着从未生效、配置写了就以为生效了。它们形状完全相同——都在认字,不认事"
slug: literal-not-facts
date: 2026-10-14T18:53:13+08:00
lastmod: 2026-10-14T19:20:00
tags:
- 字面匹配
- 静默失效
- 防腐
- 教训
categories:
- 运维手记
---
先讲一件尴尬的事。
前天我改了一个配置:某个城市的寄修业务,归属从"寄修组"调整为"上海长租组"——这是正确的改动,验证过,派单行为零变化。
**第二天,一张工单推错了群。**
地址在南京的跨站寄修单,被推到南京群、@了南京的同事——而这单按新规则应该只通知上海的备件工程师。
查下来,肇事者不是我的改动本身,而是**一个一直躺在那儿的护栏**:
```python
if "寄修" in ownerGroupName:
... # 寄修单不走城市推送
```
护栏的判断依据,是组名里的**两个字**。
我把组名从"寄修组"改成"上海长租组"——名字里没有"寄修"了,护栏当场失明。
更讽刺的是:**系统其实知道这单不对**。推出去的消息里,自带一行告警"⚠️ 维修站与地址不匹配"。它看见了,但没有拦。
## 为什么会犯这个错
我改配置时,验证过"派单行为零变化"——但只验证了**直接消费这个配置的脚本**。
而那个护栏在**另一个脚本**里,它读的是**同一个字段**(处理组名称),只是用途不同。
> **改配置必须扫消费方——判据是"谁读这个字段",不只是"谁调用这个函数"。**
修复也干脆:护栏不再认组名里的字,改成比较**结构性事实**——处理站所属的城市,和客户地址所在的城市,是不是一地。跨站就按处理站路由给对应分公司的备件工程师。
## 顺着这条线,越挖越多
护栏修完,我把最近三天的修复摆在一起看,**形状完全相同**:
### ① 脱敏手机号,和完整号比
某天开始,派单平台的接口返回的手机号变成**脱敏形态**:`138****5678`。
而本地配置里存的是完整号。
代码做的是双重校验:"你包含我吗?我包含你吗?"——脱敏号和完整号,**既不包含,也不被包含**,永远判"不符"。
于是工程师被跳过,派单动作**从不执行**,工单停在待接单。而这一切只留下一行 warning,不阻断——**静默跑了很多天**。
修复:识别到脱敏形态时,只比对**可见的前缀和后缀**。既有校验的意图(防同名不同人)保住了,字面不相等的问题也解了。
### ② 文档写着"从未生效"
查派单问题时,我先入为主写下一个结论:"这功能从未生效"。
复盘时被自己打脸:日志里躺着 **51 次成功记录**(持续一个多月),最后成功在三天前,失败从两天前开始。
真相是:**平台在三天前改了行为**(开始返回脱敏手机号),功能从"一直好用"变成"突然失效"——这是**回归**,不是"从未生效"。
更扎心的是:那句"从未生效"就写在我**两小时前刚更新的文档**里。
> **推翻自己结论的时候,最先要改的就是刚写的文档**——它最容易带着旧结论流向后人。
(顺带还纠正了第二个错误结论:之前说"那几张单卡在待接单",实际去平台一查,**平台自动派单兜了底,人选都对,业务没受损**。我拿日志里的文案当了事实。)
### ③ 配置里写了,就以为生效了
还有一个延保拦截需求:某型号笔记本键盘掉键,厂家有延保政策,要在派单时拦截。
查完发现,配置里**早就写了**这个型号——但拦截**从未生效过**。
为什么?配置匹配的字段是"物料名称",而这个型号的物料名称是一串硬件配置(CPU_内存_硬盘_屏幕_颜色),**型号本身根本不在里面**。
型号的真实来源,是另一个字段。
> **"配置存在"≠"配置生效"。** 判据必须是实测日志和产物,不是"配置里写了"。
### ④ 响应缺字段,就判失败
还有一种:关单接口返回的响应里**缺一个字段**,系统就判定"关闭失败",提醒操作人去补操作——**实际工单早就关上了**。
照着告警去做,是无效动作。
修复:响应缺证据时,**回查真实产物**(工单现在什么状态),再定性。有意思的是,修复上线当天系统又报了两条"关闭失败",回查后发现**这次是真的关不上**——**修复的价值不在少报警,而在能分清真和假**。
## 一条线串起来
这五件事,领域完全不同:派单路由、手机号校验、文档、延保配置、接口响应。
但形状是同一个:
| 它判断的"字" | 被它错杀的事实 |
|---|---|
| 组名里有没有"寄修"两个字 | 一整类单子的路由正确性 |
| 脱敏号和完整号是否相等 | 派单动作执行没执行 |
| 文档里那句"从未生效" | 51 次成功记录 |
| 配置里写了型号 | 拦截从未生效的事实 |
| 响应里有没有那个字段 | 工单其实已关闭 |
> **它们都在认字,不认事。**
字是便宜的字面量:一个子串、一次相等比较、一句写死的结论。**事**是昂贵的真相:要读结构、要看时间线、要回查产物、要数双向的记录。
认字快,认事慢。所以系统里到处都是认字的判断——**直到有一天,字变了,事没变,或者事变了,字没变。**
## 怎么防
这轮修完,沉淀了几个可复用的判据:
**1. 判断依据优先用"结构性事实",不用"字面子串"**
组是不是寄修组,看它的**归属城市**,不看名字里有没有那两个字。名字是给人看的,随时会改。
**2. 改配置,扫"谁读这个字段"**
不扫消费方的配置改动,是埋雷。而且消费方可能不看配置本身,只看配置**派生出来的数据**。
**3. 说"从未/一直"之前,先数双向计数**
看到 6 次失败就说"从未生效"?去数数成功了多少次。**只数一边的计数,是幻觉。**
**4. 拿不准,回查最终产物**
响应缺字段 → 回查工单状态;日志说失败 → 核平台实况;文档说过期 → 探一次接口。**产物才是事实,其余都是转述。**
**5. 文档结论要带"何时验证过"**
不带时间戳的结论,你不知道它是刚验证的还是两年前的。这次我把时间线(最后成功时间、首次失败时间)直接写进了文档——让下一个读者能看到证据链。
---
## 结尾
这轮还有个小插曲:写文档时我一度想用一个"聪明的启发式"——自动推断某城市是否跨分公司。单测立刻抓出反例:有位同事同时归属两个城市,启发式会把另一个城市的真漂移**静默吞掉**。
最后改成显式配置,老老实实列出名单。
**假阴性比噪音更危险**——噪音至少看得见,静默的漏判什么都不会留下。
认字,还是认事?
字快,事慢。但运维这行,**慢的那个才是对的**。
---
*这两天的工作量:30+ commits 跨 5 个仓库。护栏修复 + 同型(超期预警)双修、脱敏手机号校验重写(8 场景单测)、SKILL 文档更正、延保拦截上线(挖出死配置真源)、4 类假 FAIL 清零(响应回查定案)、跨分公司显式配置、闲鱼巡检 6 类静默缺陷修复。*
*最值钱的一句:**护栏认的是字。字一变,护栏就替你保守着一个已经不存在的世界。***