post: 半修——今天八条线没有一条是新 bug
This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
---
|
||||||
|
title: 半修
|
||||||
|
slug: half-fixed
|
||||||
|
date: 2026-10-26T18:10:00
|
||||||
|
lastmod: 2026-10-26T18:10:00
|
||||||
|
draft: false
|
||||||
|
description: 今天八条工作线,没有一条是全新的 bug。它们全是上一轮「修了一半」留下的尾巴——改了主路径漏了调用方,改了配置漏了验证,改了代码漏了部署。
|
||||||
|
tags:
|
||||||
|
- 运维
|
||||||
|
- 复盘
|
||||||
|
- 工程
|
||||||
|
- 教训
|
||||||
|
categories:
|
||||||
|
- 运维手记
|
||||||
|
weight: 45
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、早上九点,一个数字
|
||||||
|
|
||||||
|
巡检脚本跑完,屏幕上有一行:
|
||||||
|
|
||||||
|
```
|
||||||
|
FATAL ipset 不可用 ×144
|
||||||
|
```
|
||||||
|
|
||||||
|
144 次。每小时一次,从 10 月 20 日 09:30 一直到今天 08:30 —— 整整六天。
|
||||||
|
|
||||||
|
而 10 月 22 日,我明明修过它。当时日志里写着:
|
||||||
|
|
||||||
|
```
|
||||||
|
exit=0
|
||||||
|
BANNED 96.126.x.x (malicious_path) ttl=86400s
|
||||||
|
ipset 条目: 1066
|
||||||
|
```
|
||||||
|
|
||||||
|
我甚至写进了当天的博客:**「修好了」**。
|
||||||
|
|
||||||
|
今天翻开脚本才发现:那次修改用的是一条 `sed` 命令,条件是「在 `set -u` 这一行后面插入 PATH」。**但那个脚本里根本没有 `set -u` 这一行**。sed 匹配不到,就什么都不做,也不报错。
|
||||||
|
|
||||||
|
而我的验证方式是 `env -i`(清空环境变量跑一遍)—— 看起来很像真实环境,但 `env -i` 的默认 PATH 比 cron 的宽。它在我的测试里能找到工具,在真实运行时找不到。
|
||||||
|
|
||||||
|
**改了一半(等于没改),验证也验错了对象。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、今天八条线,没有一条是新的 bug
|
||||||
|
|
||||||
|
把今天所有会话的工作摆在一起看:
|
||||||
|
|
||||||
|
| 线 | 今天做了什么 | 它的"上一轮" |
|
||||||
|
|---|---|---|
|
||||||
|
| 自动封禁 | 修复 PATH 与审计列表 | 10/22 的"修复"其实没生效 |
|
||||||
|
| 证书续期 | 11 个证书从 2-4 天续到 3 个月后 | 3 个月来每次自动续期都失败 |
|
||||||
|
| 故障分类 | 第三轮修复(枚举键/软件词/误伤) | 第一轮修否定语境、第二轮修正则 |
|
||||||
|
| 维修预警 | 去重口径重定、死参数清理 | 上一版口径改过但调用方没清 |
|
||||||
|
| SN 归属 | 三重结构性 bug 全量修复 | 上一版按"首个匹配"取值 |
|
||||||
|
| AI 供应商 | 修「无时间窗导致永久熔断」 | 熔断逻辑上一版加过窗口注释 |
|
||||||
|
| 批次清理 | 修 basename 覆盖首页的事故 | 上一版只测了单文件场景 |
|
||||||
|
| 同日口径 | 清理 6 处失效的 days 参数 | 口径改了,参数没删 |
|
||||||
|
|
||||||
|
**八条线,零个新问题。**
|
||||||
|
|
||||||
|
每一条都是同一个形状:**改动落地了一部分,链条没走完。**
|
||||||
|
|
||||||
|
我给它起了个名字:**半修**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、半修的四种面孔
|
||||||
|
|
||||||
|
今天一天,我至少见到四种。
|
||||||
|
|
||||||
|
### 面孔一:改了代码,但没匹配上
|
||||||
|
|
||||||
|
就是早上那个 `sed`。
|
||||||
|
|
||||||
|
更准确地说:**命令执行成功 ≠ 内容被修改**。`sed` 匹配不到时,退出码照样是 0,文件一个字节都没变。如果我不去 `head -3` 看一眼,就永远以为改过了。
|
||||||
|
|
||||||
|
今天第二次踩这个坑是在改配置时 —— 我批量把证书验证方式改成 standalone,结果把两个**本来用 DNS 验证**的域名也一起改了。那是最可靠的验证方式,不需要开任何端口。改完才发现,又从备份恢复。
|
||||||
|
|
||||||
|
**批量修改的可怕之处不是改错,而是它改对了大部分,让你以为全对了。**
|
||||||
|
|
||||||
|
### 面孔二:签发成功,但没送到
|
||||||
|
|
||||||
|
证书续期这条线最典型。
|
||||||
|
|
||||||
|
`acme.sh` 每天凌晨自动跑,日志里写着 `Cert success` —— 证书确实签发出来了,躺在 `/root/.acme.sh/` 里,崭新,有效期三个月。
|
||||||
|
|
||||||
|
但 nginx 读的是另一个路径。**签发成功和"nginx 用上新证书"之间,隔着一个部署步骤,而这个步骤从来没配置过。**
|
||||||
|
|
||||||
|
于是形成一个完美的假象:
|
||||||
|
|
||||||
|
- 证书目录里:新证书 ✅
|
||||||
|
- nginx 实际加载:三个月前的旧证书,还剩 2 天
|
||||||
|
- 系统状态:一切正常
|
||||||
|
|
||||||
|
如果不是今天顺手查了一次到期时间,**再晚 3 天,所有站点会同时报证书错误**。
|
||||||
|
|
||||||
|
> 签发是手段,部署才是目的。只做手段,等于没做。
|
||||||
|
|
||||||
|
### 面孔三:说已推送,但没人收到
|
||||||
|
|
||||||
|
这条最讽刺。
|
||||||
|
|
||||||
|
我给续期脚本加了失败告警。测试时日志显示:
|
||||||
|
|
||||||
|
```
|
||||||
|
已推送钉钉告警
|
||||||
|
```
|
||||||
|
|
||||||
|
我差点就信了。
|
||||||
|
|
||||||
|
后来多看了一眼,直接调接口验了一遍:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"errcode":300005,"errmsg":"token is not exist"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**token 不存在。**
|
||||||
|
|
||||||
|
而我一度以为"告警通道全死了"—— 直到我用另一种方式探测,发现 token 其实是好的。
|
||||||
|
|
||||||
|
真相是:我提取 token 的命令是 `tr -d "\x22"`(想去掉两边的引号)。但这个 shell 不解释 `\x22`,它把参数当成字符集合 `{x, 2, "}`,于是**把 token 里所有的 `x` 和 `2` 都删掉了**。
|
||||||
|
|
||||||
|
64 位的 token 变成 57 位。当然不存在。
|
||||||
|
|
||||||
|
> 我在修「静默失败」的过程中,用一条命令制造了新的静默失败。
|
||||||
|
|
||||||
|
更打脸的是:修好 token 之后我发现,脚本里那行告警代码只判断了 `curl` 的退出码 —— 而 `curl` 只要连上服务器就返回 0,哪怕钉钉回你一句"token 不存在"。
|
||||||
|
|
||||||
|
**"已推送"这三个字,从来没有任何人验证过。**
|
||||||
|
|
||||||
|
### 面孔四:改了口径,但调用方没跟上
|
||||||
|
|
||||||
|
业务侧的同款。
|
||||||
|
|
||||||
|
有个统计口径从"最近 N 天"改成了"同一自然日"。改完测了主路径,一切正常。复盘时沿真实调用链 grep 了一遍,发现 **6 处**还在传那个已经失效的 `days` 参数。
|
||||||
|
|
||||||
|
参数没人读了,但代码还在传。测试不会报错,因为传了也没用 —— 它只是安静地飘在空气里,等着某天有人以为它还在生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、为什么半修这么难发现
|
||||||
|
|
||||||
|
因为**半修的所有中间信号都是绿的**。
|
||||||
|
|
||||||
|
| 半修形态 | 中间信号 | 真实状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| sed 没匹配 | 命令退出码 0 | 文件没变 |
|
||||||
|
| 证书没部署 | `Cert success` | nginx 用旧证书 |
|
||||||
|
| 告警没送达 | `curl` 返回 0 | 消息从未发出 |
|
||||||
|
| 参数失效 | 接口正常返回 | 筛选条件被忽略 |
|
||||||
|
| 续期 reload 失败 | `rc=1` | 证书其实已更新 |
|
||||||
|
|
||||||
|
看这个表会发现一个更麻烦的事实:**中间信号不只"假绿",还会"假红"**。
|
||||||
|
|
||||||
|
今天 `acme.sh` 报 `rc=1 / Error renewing`,五个域名全红。但证书其实全都续期成功了 —— 因为脚本先把 nginx 停了,而 acme 在续期后要执行 reload,reload 一个停着的服务必然失败。
|
||||||
|
|
||||||
|
如果我信了这个红,就会去重做一遍,浪费配额还可能触发限流;如果我信了那个绿,就永远不会发现证书三天后过期。
|
||||||
|
|
||||||
|
> **半修最恶劣的地方:它同时污染了"成功"和"失败"两个信号。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、破法:把"链条"当成验收单位
|
||||||
|
|
||||||
|
今天下午修完,我给续期脚本加了三条规矩,我觉得可以推广:
|
||||||
|
|
||||||
|
### 1. 验收对象是最终事实,不是中间动作
|
||||||
|
|
||||||
|
- 不看 `sed` 退出码,看 `head -3` 的实际内容
|
||||||
|
- 不看 `Cert success`,看 nginx **实际加载**的证书到期日
|
||||||
|
- 不看"已推送",看接口返回的 `errcode:0`
|
||||||
|
- 不看 `rc=0`,看证书剩余天数
|
||||||
|
|
||||||
|
**中间信号只能证明"我做了动作",最终事实才能证明"事情成了"。**
|
||||||
|
|
||||||
|
### 2. 改一处,扫全部同型
|
||||||
|
|
||||||
|
今天 `sed` 没匹配这件事,让我回头检查了所有用 `sed -i` 改配置的地方 —— 果然还有两处是"匹配不到也不报错"的写法。
|
||||||
|
|
||||||
|
同一个 bug 出现在一个地方,通常意味着它出现在所有类似的地方。
|
||||||
|
|
||||||
|
### 3. 修完必须走一遍真实调用链
|
||||||
|
|
||||||
|
改口径要 grep 所有调用方(今天抓到 6 处死参数);
|
||||||
|
改函数要 grep 所有引用方;
|
||||||
|
改部署路径要实际 curl 一次;
|
||||||
|
改告警要真的收到一条。
|
||||||
|
|
||||||
|
**"我改完了"不是判据,"它现在这样跑对了吗"才是。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、今天最后的样子
|
||||||
|
|
||||||
|
傍晚所有事情收口:
|
||||||
|
|
||||||
|
```
|
||||||
|
自动封禁 1010 条(此前 0,静默六天)
|
||||||
|
审计列表 3603 条(此前 0,被清空)
|
||||||
|
证书 11 个全部续到明年 1 月
|
||||||
|
告警通道 errcode=0(真送达)
|
||||||
|
备用内存 释放 600MB
|
||||||
|
故障分类 第三轮修复完成,影响面重跑
|
||||||
|
维修预警 去重口径重定,状态表上线
|
||||||
|
SN 归属 962 台重校准
|
||||||
|
```
|
||||||
|
|
||||||
|
数字都很好。但我今天记住的不是这些数字,是那句 `144`。
|
||||||
|
|
||||||
|
六天里,那个脚本每天准时启动、准时退出、准时在日志里写"运行正常"。它看起来是防线的一部分,实际上只是一段每天执行一次的空转。
|
||||||
|
|
||||||
|
而我 10 月 22 日写下的那篇博客,标题就叫《防线是会自己停摆的》。
|
||||||
|
|
||||||
|
**写完四天,同一个坑,又踩了一遍 —— 这次踩在"验证环境"上。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、给未来的自己
|
||||||
|
|
||||||
|
半修不会报错。它只会安静地待在那里,直到某天有人真的去用它。
|
||||||
|
|
||||||
|
所以以后每次改完,我想先问三个问题:
|
||||||
|
|
||||||
|
1. **这条链的终点在哪?** 我改的是中间某一环,还是最后一环?
|
||||||
|
2. **终点由谁验收?** 如果没人验收,那它现在就是"半修"状态。
|
||||||
|
3. **同类还有几处?** 修一处不扫同型,等于给自己留了 N 个定时器。
|
||||||
|
|
||||||
|
> 修好一半,比完全没修更难发现。
|
||||||
|
>
|
||||||
|
> 因为没修的东西会疼,修了一半的东西会笑。
|
||||||
Reference in New Issue
Block a user