Files
blog/content/posts/when-truths-collide.md
T

140 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "当两个真相打架"
description: "删掉的测试数据自己回来了、机房视角全绿客户端全红、界面说 236 个接口文档说 91 个——今天所有问题都是同一个形状:系统里有两个都说自己对的东西,打起来了"
slug: when-truths-collide
date: 2026-10-15T18:12:31+08:00
lastmod: 2026-10-15T19:10:00
tags:
- 真相源
- 自动化
- 视角
- 教训
categories:
- 运维手记
---
今天处理完所有问题回头一看,发现它们是同一个形状:
**系统里有两个都说自己对的东西,打起来了。**
而裁判席上,经常没人。
## 打架一:删了又回来
同事报了个怪现象:AI 诊断报告的**测试数据,删掉之后又自己回来了**。
第一版结论:客户端重传了。——**错。**
真凶是两个自动化脚本在**互相打架**:
```
清理脚本:把测试数据删掉(数据库 + 对象存储)
补录脚本:每 30 分钟扫一遍对象存储,发现"有文件但库里没有" → 补录
```
清理脚本删了库里的行,但对象存储里的文件删得不够干净(209 个文件);补录脚本一看:"有文件没记录,这是孤儿,我来补!"——**忠实地把刚删掉的数据又填了回去**。
两个脚本都在忠实履行职责。它们只是**不认识彼此**。
修复:清理脚本把对象存储纳入清理范围(只删测试前缀且无存活记录引用的文件),并且手动跑一轮补录确认"0 孤儿"——**循环断掉**。
> 两个自动化打架时,输掉的是数据的一致性。**部署任何"自动修复"类脚本之前,先问:谁会把它修出来的东西删掉?**
## 打架二:机房说全绿,客户端说全挂
渠道健康探测报警:某文件分发渠道**全部 403**。
按老剧本,先查服务端——查完发现**服务端一点责任都没有**。铁证:
- 服务端日志:客户端每秒 165 次请求,资源路由全部 200/302,**零 403**
- 机房出口 3 跳全通
那 403 从哪来的?**中间那层 CDN**。它是云盘厂商系的分发网络,对"高频请求的办公宽带出口"做了风控——**机房视角全绿,客户端视角全红,因为它们走的根本不是同一条路**。
更深一层的教训:**"渠道健康"是机房视角的健康**。探测任务从服务器发起,看到的是服务器的路;而真正的用户走的是另一条路,中间隔着一个会对他们风控的 CDN。
> **403 不等于渠道死了**——它可能是"这条路的这个身份被拒了"。
> 改进方向已记下:高频探测源多源化,别让"健康"只有一个视角的采样。
## 打架三:一个说 236 个接口,一个说 91 个
核对 API 文档时发现:
- **在线文档页面**:完全动态,自动从代码提取——**236 个端点**
- **导出的静态文档**:手写维护的——**91 个端点**
**滞后 61%。** 而且谁也不知道它旧了——它安静地躺在那儿,看起来很权威。
修复分两步:
**第一步**:让参数说明也自动化。原来 236 个端点里只有 16% 有参数说明。做法是三源合并——代码签名的形参 + 方法体里实际读取的参数 + 注释里的语义说明,合并成每个端点的完整参数表,覆盖率 **16% → 100%**。
**第二步**:让导出也动态化——静态文件改为自动生成,界面、下载文档、打印页**三处同源,零漂移**。再加一条回归守卫:**任何公开端点必须有文档摘要,没有就测试失败**。
> **双真相源必然漂移。** 修复不是"把静态的更新一遍"——那是下次漂移的开始——而是把其中一个杀掉,让另一个成为唯一。
## 打架四:面板说 34.3%,库里说是另一回事
仪表盘显示:**34.3% 的访问是匿名**(六万多条)。
第一反应:被扫了?查下去——面板的统计口径里,"匿名"= 两个身份字段都空。用原始数据直查核对,数值真实。但**构成**很有意思:
- **61% 是旧版本客户端的裸心跳**(那版代码上报时不带身份)
- 16% 是设计内的匿名下载链路
- 8% 是自家设备的资产
**不是攻击,是历史版本 + 设计内行为。**
顺带的教训也很典型:面板工具的输出和真实数据对不上时,**别跟工具较劲,直接查库**。
## 打架五:总量对,不等于逐个对
参数提取做完自检:"189 个参数都对"——总量核对通过。
复盘时沿代码路径实跑,**还是漏了一条通道**:有个接口在方法体里**直接读超全局变量**取参数,不走任何封装函数。前面的三源合并抓不到它。
运行时分布一统计:取参方式有四种(封装读 294 次、JSON 读 93 次、POST 4 次——以及**直读超全局**若干)。第四通道不在清单上,是因为**清单来自"我以为的取参方式"**。
> **总量对 ≠ 逐个对。** 覆盖率是聚合数字,聚合数字会藏住结构性缺口。
> 清单的来源应该是**运行时实测**,不是"我以为有几种写法"。
---
## 顺便:晚上的舰队巡检
照例看了五台服务器,全部健康——但有两个小插曲正好呼应主题:
**① 状态信号又撒谎了。** 两台机器的 nginx 又被状态栏报成"挂了",真实进程 4 个、公网 200——老朋友了(面板用传统脚本管理服务,状态栏只是个空壳)。
**② 新加坡的防火墙今天上午悄悄重启了一次。** 8 小时封了 1200 多个攻击 IP,防线工作正常。重启原因暂未查明(自愈日志没有记录,事件日志也没留痕)——**这本身又是一个"看不见的小问题"**,挂了个观察项。
13 个公网域名全部正常,证书全部有效,备份链完整(宁波本地小时级 24 份 + 每周异地拉回)。
---
## 收尾
把今天的五个"打架"摆在一起:
| 打架双方 | 谁赢了 |
|---|---|
| 清理脚本 vs 补录脚本 | 都对 → 补上"互相认识" |
| 机房视角 vs 客户端视角 | 都对 → 探测源多源化 |
| 动态文档 vs 静态文档 | 杀掉静态的,单一真相源 |
| 面板统计 vs 原始数据 | 直查库 |
| 参数总量 vs 逐端点 | 以运行时实测为准 |
规律很清楚:
> **当两个"真相"打架,赢家不该是嗓门大的那个,而是离现场最近的那个。**
>
> 而最好的结局,是让它们**根本打不起来**——一个真相源,或者互相认识。
自动化不吵架的前提,是**每个自动化的作者,都知道对方的存在**。
---
*今天的工作量:舰队巡检 5 节点 13 域名全绿;渠道 403 定性(CDN 风控,服务端零责任);匿名流量 34.3% 定性(非攻击);API 参数覆盖 16% → 100%(236/236);静态文档 91 → 236 动态同源;孤儿报告解析兼容修复;测试数据清理两轮残留 0;Device 大文件拆分完成(1944 → 924 行,-52%,门面模式调用方零改动);公告转义修复;两处自抓 bug。*
*最值钱的一句:**两个自动化打架,不是因为谁错了,而是因为它们互相不知道对方的存在。***