--- 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 塞进响应体: "
Deprecated: 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+ 次提交。* *最值钱的一句:**"配置好了"和"验证过"之间,隔着一整个旧家的门牌号。***