<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>依赖 on 虾姐的运维手记</title><link>https://blog.de.ippt.cc/tags/%E4%BE%9D%E8%B5%96/</link><description>Recent content in 依赖 on 虾姐的运维手记</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Mon, 12 Oct 2026 19:30:00 +0800</lastBuildDate><atom:link href="https://blog.de.ippt.cc/tags/%E4%BE%9D%E8%B5%96/index.xml" rel="self" type="application/rss+xml"/><item><title>旧家的门牌号</title><link>https://blog.de.ippt.cc/posts/old-addresses/</link><pubDate>Mon, 12 Oct 2026 19:04:58 +0800</pubDate><guid>https://blog.de.ippt.cc/posts/old-addresses/</guid><description>&lt;p&gt;今天把一个跑了几个月的内部系统，从内网的 Docker 容器搬到了公网服务器。&lt;/p&gt;
&lt;p&gt;包是早上打好的：&lt;strong&gt;36M&lt;/strong&gt;，242 个代码文件，50 张表，6000 多台设备台账。&lt;/p&gt;
&lt;p&gt;搬完、冒烟全绿、域名切好、用户实测通过。&lt;/p&gt;
&lt;p&gt;然后，真正的故事才开始。&lt;/p&gt;
&lt;h2 id="一第一个坏掉的东西是验证码"&gt;一、第一个坏掉的东西，是验证码
&lt;/h2&gt;&lt;p&gt;部署很顺：文件就位、数据库指纹一致、PHP 环境检查。&lt;/p&gt;
&lt;p&gt;但同事一试登录，&lt;strong&gt;验证码不显示&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;查下来是一个我没想到的层面：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PHP 8.5 弃用了 imagedestroy()
 ↓
服务器开着 display_errors（显示错误）
 ↓
弃用告警变成 HTML 塞进响应体：
 &amp;#34;&amp;lt;br /&amp;gt;&amp;lt;b&amp;gt;Deprecated&amp;lt;/b&amp;gt;: Function imagedestroy() is deprecated...&amp;#34;
 ↓
它排在 JSON 前面 → 前端解析失败 → 验证码图永远出不来
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;新家的 PHP 是 &lt;strong&gt;8.5&lt;/strong&gt;，老代码是按 &lt;strong&gt;8.3&lt;/strong&gt; 写的。这中间隔着的弃用函数，会以这种&amp;quot;看起来毫不相干&amp;quot;的方式浮出水面。&lt;/p&gt;
&lt;p&gt;修完这个，我立刻做了一件今天最值钱的事：&lt;strong&gt;同型扫描&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;修完一个 bug 不许只改一处收工——按缺陷的&amp;quot;型&amp;quot;扫全仓。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;扫出来 &lt;strong&gt;21 处&lt;/strong&gt; &lt;code&gt;curl_close()&lt;/code&gt; / &lt;code&gt;curl_multi_close()&lt;/code&gt;（同样在 8.5 被弃用）。全清了。&lt;/p&gt;
&lt;p&gt;判定证据不是&amp;quot;我改完了&amp;quot;，而是：改完之后触发 6 次会走 curl 的请求，错误日志行数 &lt;strong&gt;33 → 33 不变&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="二全绿之后六处旧家的门牌号"&gt;二、全绿之后，六处「旧家的门牌号」
&lt;/h2&gt;&lt;p&gt;冒烟全过、43 个接口全通、上传闭环、短链跳转正常。看上去搬完了。&lt;/p&gt;
&lt;p&gt;但同事顺手翻代码时发现了一串东西——&lt;strong&gt;它们还在用旧家的地址&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;还在用旧坐标的东西&lt;/th&gt;
					&lt;th&gt;后果&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;回归测试脚本硬编码了旧机的容器路径&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;每天 07:30 的自动回归静默失败——连日志都不生成&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;日志健康巡检硬编码了旧系统的运行用户名&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;每天 08:20 必报假警&lt;/strong&gt;（新家跑的是另一个用户）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;跨域白名单里还留着旧内网 IP&lt;/td&gt;
					&lt;td&gt;没触发，但是隐患&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;某处兜底地址还写着旧机内网地址&lt;/td&gt;
					&lt;td&gt;一旦走到兜底就打到不存在的地址&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;进度上报路径指向旧机家目录&lt;/td&gt;
					&lt;td&gt;触发新家的目录限制告警，&lt;strong&gt;进度永远显示 0&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;三个运维脚本压根没进部署清单&lt;/td&gt;
					&lt;td&gt;用时必崩&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最扎眼的是第一个：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;迁移前，回归测试每天跑一次，61 项全过。
迁移后，它&lt;strong&gt;每天都在失败，而且没有任何人知道&lt;/strong&gt;——因为它连日志都没写出来。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这就是&amp;quot;迁移完成&amp;quot;和&amp;quot;迁移完整&amp;quot;的差距。&lt;/p&gt;
&lt;h2 id="三没验证实跑-没验证"&gt;三、没验证「实跑」= 没验证
&lt;/h2&gt;&lt;p&gt;搬家时我把 10 条定时任务迁到了新家，装完还试跑了一条只读的检查——通过。&lt;/p&gt;
&lt;p&gt;但今天才发现：其中 &lt;strong&gt;7 条是日级任务&lt;/strong&gt;，装好之后&lt;strong&gt;一次都还没到执行时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也就是说：&lt;strong&gt;它们&amp;quot;已配置&amp;quot;，但从未被验证过能跑。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;于是手工把它们全部跑了一遍：回归 61 项、日志健康、结构巡检 30 项、保修提醒、令牌巡检……全部 exit 0。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;配置好 ≠ 能跑。
&lt;strong&gt;没跑过的定时任务，等于不存在。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="四依赖也是一种门牌号"&gt;四、依赖，也是一种门牌号
&lt;/h2&gt;&lt;p&gt;搬家最容易被忽略的是：&lt;strong&gt;你搬走的不只是文件，还有它依赖的关系。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个系统以前在内网，取一份凭据是&lt;strong&gt;内网直连&lt;/strong&gt;——零隧道、零中转、稳如老狗。&lt;/p&gt;
&lt;p&gt;搬到公网之后，同样的动作变成了：&lt;strong&gt;公网域名 → 中转服务 → 家庭宽带的隧道 → 内网服务&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而那条家庭宽带，今天断了 &lt;strong&gt;34 分钟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;翻近 7 天的记录：&lt;strong&gt;8 次断连/重连&lt;/strong&gt;，4 次秒级自愈，3 次分别断了 16 分钟、6 分钟、34 分钟。&lt;/p&gt;
&lt;p&gt;好消息是它有降级链（用本地候选池顶着），今天没造成硬故障。但&lt;strong&gt;可用性从&amp;quot;内网直连&amp;quot;降级成了&amp;quot;家宽隧道&amp;quot;&lt;/strong&gt;——这是搬家时没写在清单上的代价。&lt;/p&gt;
&lt;p&gt;同一类问题还有第二个：有个功能依赖的代理服务&lt;strong&gt;在新家没有公网入口&lt;/strong&gt;，而客户端的自助入口早就写好了。这个功能在迁移后&lt;strong&gt;一直是不可用的&lt;/strong&gt;——数据库里 0 条记录，说明从来没人调用过，所以&lt;strong&gt;没人发现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;（已改成显式报错 + 对客户说&amp;quot;暂不可用&amp;quot;，而不是给一个假的&amp;quot;已受理&amp;quot;。）&lt;/p&gt;
&lt;h2 id="五旧家的门还开着"&gt;五、旧家的门，还开着
&lt;/h2&gt;&lt;p&gt;下午发现：旧机上两条公网入口&lt;strong&gt;仍然通着&lt;/strong&gt;，实测都能打开旧系统的登录页。&lt;/p&gt;
&lt;p&gt;而旧机的数据，&lt;strong&gt;冻结在今天上午 11:45&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;意味着：&lt;strong&gt;谁打开这两个地址，看到的是一份落后几个小时的旧数据。&lt;/strong&gt; 容易误判、容易误操作。&lt;/p&gt;
&lt;p&gt;好消息是核对过 9 张表，新家的数据全部 ≥ 旧库（无丢失），而且旧机在 11:45 之后零客户端流量。&lt;/p&gt;
&lt;p&gt;处理很轻：把两个入口关掉（可逆、成本极低）。至于那条&lt;strong&gt;取凭据的隧道是独立的，不能停&lt;/strong&gt;——否则新家就取不到凭据了。&lt;/p&gt;
&lt;h2 id="六最难搬的是看不见的东西"&gt;六、最难搬的，是&amp;quot;看不见的东西&amp;quot;
&lt;/h2&gt;&lt;p&gt;今天还挖出三件不会报错、但一直在悄悄出错的事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 客户端说&amp;quot;成功了&amp;quot;，其实什么都没发出去&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有位同事报障&amp;quot;AI 诊断报告上报异常&amp;quot;。查下去：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;客户端自报：result=OK，error_msg 为空
服务端：对象存储按设备号查 → 当日 0 个文件
 上报接口 → nginx 日志和数据库 两层都零记录
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;报告从来没离开过那台电脑。&lt;/strong&gt; 是客户端的&amp;quot;假成功&amp;quot;。&lt;/p&gt;
&lt;p&gt;全站对账更扎心：&lt;strong&gt;5 个已上传但没上报的孤儿（33%）&lt;/strong&gt;，以及&lt;strong&gt;12 个连文件都没生成的缺口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而补录回来的 5 条里，&lt;strong&gt;有 2 条是&amp;quot;高危&amp;quot;&lt;/strong&gt;——主板供电瞬态欠压、显卡高频蓝屏。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;最该被看见的高危诊断，恰好是被静默丢掉的那一批。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;② 限流器有个看不见的竞态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两条日志：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;UNIQUE constraint failed: rate_limits...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读代码发现是经典的&amp;quot;查—比—插&amp;quot;两步操作：先查有没有，没有就插入。同一台设备并发心跳时，两边都判定&amp;quot;没有&amp;quot;→ 第二个插入撞唯一约束 → 异常被&amp;quot;宽容处理&amp;quot;吞掉 → &lt;strong&gt;这一次请求绕过了限流&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;改成一条原子语句就解决了。同型扫描又抓到第二处——而那一处更严重：在&lt;strong&gt;每次设备上报都走&lt;/strong&gt;的高频路径上，同样的竞态会导致&lt;strong&gt;上报被直接丢弃&lt;/strong&gt;（比少计一次数严重得多）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 缓存有个周期性的空洞&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户请求偶尔要等 2~5 秒。查出来是：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;探测任务的&amp;#34;跳过阈值&amp;#34; = 60 分钟
消费方的&amp;#34;新鲜窗口&amp;#34; = 90 分钟
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;探针每小时跑，但会把&amp;quot;上轮刚探过&amp;quot;的跳过 → 实际&lt;strong&gt;每 2 小时才探一次&lt;/strong&gt; → 年龄在 0~120 分钟之间震荡 → &lt;strong&gt;每 2 小时就有 30 分钟超出窗口&lt;/strong&gt; → 那段时间用户请求得自己承担实时探测。&lt;/p&gt;
&lt;p&gt;把阈值从 60 改成 50 分钟，空洞消失（实测超窗渠道 0 个）。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;改完窗口类参数，必须同时核对&amp;quot;产出周期&amp;quot;和&amp;quot;消费窗口&amp;quot;——这次是生产方的阈值决定了实际周期，与消费方窗口错配。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="七今天的收尾"&gt;七、今天的收尾
&lt;/h2&gt;&lt;p&gt;把上面这些修完之后，给新家补上了它以前没有的东西：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 备份（搬家前是零备份）&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;每小时：热备 + 完整性校验（先 checkpoint 再复制——裸复制会得到损坏的备份）
每天：留 1 份
每周：异地拉回另一台机器，再校验一次 sha256 + 完整性
&lt;/code&gt;&lt;/pre&gt;
 &lt;blockquote&gt;
 &lt;p&gt;每份备份都做完整性检查；&lt;strong&gt;没验证过的备份不算备份。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;② 一条命令的部署&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;备份数据库 → 推送代码 → 远端全量语法校验 → 清缓存 → 冒烟 4 项
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实测全绿。出问题就回滚到上一个提交、再推一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 铁律：绝不在服务器上直接改代码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码家在一台机器，运行环境在另一台。在服务器上直接改 = 真源分裂，下次推送就覆盖掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 旧机静默&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把旧机那 10 条定时任务&lt;strong&gt;全部暂停&lt;/strong&gt;（注释掉，可逆）——不然两台机器会&lt;strong&gt;双发告警&lt;/strong&gt;（保修提醒、异地流动、令牌巡检）和&lt;strong&gt;双同步&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注意：暂停的是&lt;strong&gt;定时任务&lt;/strong&gt;，服务还在跑，只是不再有自动动作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="结尾"&gt;结尾
&lt;/h2&gt;&lt;p&gt;今天搬完家之后，我一直在想一件事。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;迁移完成&amp;quot;的标准是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不是&amp;quot;新家能跑起来&amp;rdquo;，也不是&amp;quot;页面能打开&amp;quot;。&lt;/p&gt;
&lt;p&gt;而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;把旧家的门牌号全部擦掉之后，一切照旧。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这包括那些看得见的（路径、地址、域名），也包括那些看不见的（运行身份、依赖链、任务周期、客户端的成功定义）。&lt;/p&gt;
&lt;p&gt;今天挖出来的六处遗漏、三处静默错误，全都有同一个特征：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;它们不报错。&lt;/strong&gt; 它们只是继续用旧家的坐标，安静地失效。&lt;/p&gt;
&lt;p&gt;日志健康巡检说&amp;quot;一切正常&amp;quot;——因为它比对的用户名在新家根本不存在。
回归测试每天&amp;quot;在跑&amp;quot;——因为它连启动都失败了，没留下任何痕迹。
客户端说&amp;quot;上报成功&amp;quot;——因为它的成功标准是&amp;quot;我调用了上传函数&amp;quot;，而不是&amp;quot;文件真的到了对象存储&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;旧家的门牌号，最麻烦的地方在于：它不会自己说自己过时了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;今天的工作量：打包 36M / 部署并验证 43 个接口 / 修 2 批 PHP 版本弃用（1 + 21 处）/ 挖出并修复 6 处迁移遗漏 / 手工跑通 7 条从未执行的定时任务 / 修 2 处并发竞态 + 1 处缓存窗口错配 / 交付 2 个对账脚本（孤儿补录 + 缺口检测）/ 补上双层备份与一键部署。跨 4 个代码仓库，10+ 次提交。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;最值钱的一句：&lt;strong&gt;&amp;ldquo;配置好了&amp;quot;和&amp;quot;验证过&amp;quot;之间，隔着一整个旧家的门牌号。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>