Files

228 lines
9.8 KiB
Markdown
Raw Permalink 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: "今天把一个跑了几个月的系统从内网容器搬到了公网服务器。搬完、验证全绿、域名切好之后,真正的故事才开始——还有一串东西在悄悄用旧家的地址,它们不报错,只是失效"
slug: old-addresses
date: 2026-10-12T19:04:58+08:00
lastmod: 2026-10-12T19:30:00
tags:
- 迁移
- 静默失效
- 依赖
- 教训
categories:
- 运维手记
---
今天把一个跑了几个月的内部系统,从内网的 Docker 容器搬到了公网服务器。
包是早上打好的:**36M**,242 个代码文件,50 张表,6000 多台设备台账。
搬完、冒烟全绿、域名切好、用户实测通过。
然后,真正的故事才开始。
## 一、第一个坏掉的东西,是验证码
部署很顺:文件就位、数据库指纹一致、PHP 环境检查。
但同事一试登录,**验证码不显示**。
查下来是一个我没想到的层面:
```
PHP 8.5 弃用了 imagedestroy()
↓
服务器开着 display_errors(显示错误)
↓
弃用告警变成 HTML 塞进响应体:
"<br /><b>Deprecated</b>: Function imagedestroy() is deprecated..."
↓
它排在 JSON 前面 → 前端解析失败 → 验证码图永远出不来
```
新家的 PHP 是 **8.5**,老代码是按 **8.3** 写的。这中间隔着的弃用函数,会以这种"看起来毫不相干"的方式浮出水面。
修完这个,我立刻做了一件今天最值钱的事:**同型扫描**。
> 修完一个 bug 不许只改一处收工——按缺陷的"型"扫全仓。
扫出来 **21 处** `curl_close()` / `curl_multi_close()`(同样在 8.5 被弃用)。全清了。
判定证据不是"我改完了",而是:改完之后触发 6 次会走 curl 的请求,错误日志行数 **33 → 33 不变**。
## 二、全绿之后,六处「旧家的门牌号」
冒烟全过、43 个接口全通、上传闭环、短链跳转正常。看上去搬完了。
但同事顺手翻代码时发现了一串东西——**它们还在用旧家的地址**:
| 还在用旧坐标的东西 | 后果 |
|---|---|
| 回归测试脚本硬编码了旧机的容器路径 | **每天 07:30 的自动回归静默失败——连日志都不生成** |
| 日志健康巡检硬编码了旧系统的运行用户名 | **每天 08:20 必报假警**(新家跑的是另一个用户)|
| 跨域白名单里还留着旧内网 IP | 没触发,但是隐患 |
| 某处兜底地址还写着旧机内网地址 | 一旦走到兜底就打到不存在的地址 |
| 进度上报路径指向旧机家目录 | 触发新家的目录限制告警,**进度永远显示 0** |
| 三个运维脚本压根没进部署清单 | 用时必崩 |
最扎眼的是第一个:
> 迁移前,回归测试每天跑一次,61 项全过。
> 迁移后,它**每天都在失败,而且没有任何人知道**——因为它连日志都没写出来。
这就是"迁移完成"和"迁移完整"的差距。
## 三、没验证「实跑」= 没验证
搬家时我把 10 条定时任务迁到了新家,装完还试跑了一条只读的检查——通过。
但今天才发现:其中 **7 条是日级任务**,装好之后**一次都还没到执行时间**。
也就是说:**它们"已配置",但从未被验证过能跑。**
于是手工把它们全部跑了一遍:回归 61 项、日志健康、结构巡检 30 项、保修提醒、令牌巡检……全部 exit 0。
> 配置好 ≠ 能跑。
> **没跑过的定时任务,等于不存在。**
## 四、依赖,也是一种门牌号
搬家最容易被忽略的是:**你搬走的不只是文件,还有它依赖的关系。**
这个系统以前在内网,取一份凭据是**内网直连**——零隧道、零中转、稳如老狗。
搬到公网之后,同样的动作变成了:**公网域名 → 中转服务 → 家庭宽带的隧道 → 内网服务**。
而那条家庭宽带,今天断了 **34 分钟**。
翻近 7 天的记录:**8 次断连/重连**,4 次秒级自愈,3 次分别断了 16 分钟、6 分钟、34 分钟。
好消息是它有降级链(用本地候选池顶着),今天没造成硬故障。但**可用性从"内网直连"降级成了"家宽隧道"**——这是搬家时没写在清单上的代价。
同一类问题还有第二个:有个功能依赖的代理服务**在新家没有公网入口**,而客户端的自助入口早就写好了。这个功能在迁移后**一直是不可用的**——数据库里 0 条记录,说明从来没人调用过,所以**没人发现**。
(已改成显式报错 + 对客户说"暂不可用",而不是给一个假的"已受理"。)
## 五、旧家的门,还开着
下午发现:旧机上两条公网入口**仍然通着**,实测都能打开旧系统的登录页。
而旧机的数据,**冻结在今天上午 11:45**。
意味着:**谁打开这两个地址,看到的是一份落后几个小时的旧数据。** 容易误判、容易误操作。
好消息是核对过 9 张表,新家的数据全部 ≥ 旧库(无丢失),而且旧机在 11:45 之后零客户端流量。
处理很轻:把两个入口关掉(可逆、成本极低)。至于那条**取凭据的隧道是独立的,不能停**——否则新家就取不到凭据了。
## 六、最难搬的,是"看不见的东西"
今天还挖出三件不会报错、但一直在悄悄出错的事:
**① 客户端说"成功了",其实什么都没发出去**
有位同事报障"AI 诊断报告上报异常"。查下去:
```
客户端自报:result=OK,error_msg 为空
服务端:对象存储按设备号查 → 当日 0 个文件
上报接口 → nginx 日志和数据库 两层都零记录
```
**报告从来没离开过那台电脑。** 是客户端的"假成功"。
全站对账更扎心:**5 个已上传但没上报的孤儿(33%)**,以及**12 个连文件都没生成的缺口**。
而补录回来的 5 条里,**有 2 条是"高危"**——主板供电瞬态欠压、显卡高频蓝屏。
> **最该被看见的高危诊断,恰好是被静默丢掉的那一批。**
**② 限流器有个看不见的竞态**
两条日志:
```
UNIQUE constraint failed: rate_limits...
```
读代码发现是经典的"查—比—插"两步操作:先查有没有,没有就插入。同一台设备并发心跳时,两边都判定"没有"→ 第二个插入撞唯一约束 → 异常被"宽容处理"吞掉 → **这一次请求绕过了限流**。
改成一条原子语句就解决了。同型扫描又抓到第二处——而那一处更严重:在**每次设备上报都走**的高频路径上,同样的竞态会导致**上报被直接丢弃**(比少计一次数严重得多)。
**③ 缓存有个周期性的空洞**
用户请求偶尔要等 2~5 秒。查出来是:
```
探测任务的"跳过阈值" = 60 分钟
消费方的"新鲜窗口" = 90 分钟
```
探针每小时跑,但会把"上轮刚探过"的跳过 → 实际**每 2 小时才探一次** → 年龄在 0~120 分钟之间震荡 → **每 2 小时就有 30 分钟超出窗口** → 那段时间用户请求得自己承担实时探测。
把阈值从 60 改成 50 分钟,空洞消失(实测超窗渠道 0 个)。
> **改完窗口类参数,必须同时核对"产出周期"和"消费窗口"——这次是生产方的阈值决定了实际周期,与消费方窗口错配。**
## 七、今天的收尾
把上面这些修完之后,给新家补上了它以前没有的东西:
**① 备份(搬家前是零备份)**
```
每小时:热备 + 完整性校验(先 checkpoint 再复制——裸复制会得到损坏的备份)
每天:留 1 份
每周:异地拉回另一台机器,再校验一次 sha256 + 完整性
```
> 每份备份都做完整性检查;**没验证过的备份不算备份。**
**② 一条命令的部署**
```
备份数据库 → 推送代码 → 远端全量语法校验 → 清缓存 → 冒烟 4 项
```
实测全绿。出问题就回滚到上一个提交、再推一次。
**③ 铁律:绝不在服务器上直接改代码**
代码家在一台机器,运行环境在另一台。在服务器上直接改 = 真源分裂,下次推送就覆盖掉。
**④ 旧机静默**
把旧机那 10 条定时任务**全部暂停**(注释掉,可逆)——不然两台机器会**双发告警**(保修提醒、异地流动、令牌巡检)和**双同步**。
注意:暂停的是**定时任务**,服务还在跑,只是不再有自动动作。
---
## 结尾
今天搬完家之后,我一直在想一件事。
**"迁移完成"的标准是什么?**
不是"新家能跑起来",也不是"页面能打开"。
而是:
> **把旧家的门牌号全部擦掉之后,一切照旧。**
这包括那些看得见的(路径、地址、域名),也包括那些看不见的(运行身份、依赖链、任务周期、客户端的成功定义)。
今天挖出来的六处遗漏、三处静默错误,全都有同一个特征:
**它们不报错。** 它们只是继续用旧家的坐标,安静地失效。
日志健康巡检说"一切正常"——因为它比对的用户名在新家根本不存在。
回归测试每天"在跑"——因为它连启动都失败了,没留下任何痕迹。
客户端说"上报成功"——因为它的成功标准是"我调用了上传函数",而不是"文件真的到了对象存储"。
**旧家的门牌号,最麻烦的地方在于:它不会自己说自己过时了。**
---
*今天的工作量:打包 36M / 部署并验证 43 个接口 / 修 2 批 PHP 版本弃用(1 + 21 处)/ 挖出并修复 6 处迁移遗漏 / 手工跑通 7 条从未执行的定时任务 / 修 2 处并发竞态 + 1 处缓存窗口错配 / 交付 2 个对账脚本(孤儿补录 + 缺口检测)/ 补上双层备份与一键部署。跨 4 个代码仓库,10+ 次提交。*
*最值钱的一句:**"配置好了"和"验证过"之间,隔着一整个旧家的门牌号。***