主站在跑业务,不能拿来试错。于是我做了一件很朴素的事:把整站复制一份到子目录,所有改造先在这里验证,通过了再搬回生产环境。这件事听起来是个工程问题,做完之后我发现,它更多是个判断问题——难的不是把东西修好,是分清哪些该修、哪些本来就不该动,以及你的自检到底覆盖了什么。
一、为什么要复制一个老站
这个站跑了很久,WordPress 加一堆历史插件,页面、文章、产品、多语言、表单,全都长在一起。上面还压着几轮性能优化和样式改造,谁改的、改了哪一层、有没有副作用,已经没有完整记录了。
在这种站上直接动手,风险不在技术。风险在于你按下保存的那一刻,不知道会牵动什么。改一个模板,可能影响所有产品页;调一次缓存,可能让某个栏目变成空白;上一次改完没出事,不代表下一次也不会。
所以我复制了一份到子目录,当成试验田。逻辑很简单:生产环境只接受验证过的东西,没验证过的一律先在复制站试。改坏了就重来,不影响任何人。
这个思路本身没问题。有问题的是我一开始对「复制成功」的定义。
二、1748 个页面全通,然后呢
迁移完成那天,自检报告非常漂亮:1748 个网址全部返回 200,1746 条路由全部指向正确对象,缺失资源 0 个,内容不匹配 0 个。
当时我的判断是:复制完成了。
后来回头查,发现这份报告没能覆盖的东西比它覆盖的多:
| 项目 | 自检结论 | 实际情况 |
|---|---|---|
| 页面可用性 | 1748 个全部 200 | 准确,确实全通 |
| 多语言站 | 未覆盖 | 103 套语言站,抽检 15 个全部 404 |
| 企业简介页 | 未覆盖 | 不存在,访问直接 404 |
| 爬虫声明文件 | 未覆盖 | 不存在 |
为什么全通?因为我那份巡检清单,是从本站自己的页面列表里生成的。
这句话是整个项目最重要的一次认知修正:自检的边界,等于你输入清单的边界。它只能证明「我导进来的东西都能打开」,在结构上不可能发现「我少导了什么」。
换个说法更好懂:考完试对答案,你把每一道答过的题都检查了一遍,字迹工整、计算正确——但你从来没数过卷子一共几道题。
三、差点被误判成事故的四组页面
复核时发现一组很扎眼的数据:四个页面的内容,跟另外四个页面字节级完全相同。
比如「服务页」和另一个听着毫不相干的页面,HTML 一字不差,连 md5 都一样。第一反应是数据错乱——整页被顶替了,导入时 ID 映射串了。
我差点把它写进问题清单,按「严重内容事故」处理。
幸好写之前多问了一句:访问这些地址的时候,服务器返回的是什么状态码?
结果全是 301 永久跳转。
这四个页面在部署脚本里被明确列为「别名」——原站这几个页面本来就是空的,所以被指向了对应的保留页面。注释写得很清楚:Empty legacy pages now point to their maintained equivalents.
所以「内容完全相同」不是事故,是跳转的必然结果。任何工具跟着跳转走一遍,拿到的当然都是落地页的内容。
比对内容之前,先看状态码。这一步花十秒钟,能省掉一次误判。
这件事让我改了一个习惯:以后判断「两个东西是不是同一个东西」,先确认它们是怎么到达的,再看它们长什么样。到达方式不同、内容相同,往往是设计;到达方式相同、内容不同,才可能是故障。
四、老系统的病,长在想不到的地方
真正需要修的硬伤有四类,其中一类特别能代表老系统的性格。
产品页上有六个链接,点开必然失败,因为它们的地址是 http://3.11、http://3.12 这样的东西——把图片的编号当成了网址。
顺着查下去,发现这不是一处笔误,而是三层叠加:
第一层,数据。这些图片的描述字段(alt)是空的。取值函数遇到空值,退了一步,用了图片标题顶替。而图片标题是「3.12」。第二层,模板。模板把 alt 字段直接塞进了链接地址的位置——它假设这里存的是一条网址,而实际存的是一个编号。第三层,解析。浏览器和解析器看到「3.12」,认为这是个域名,自动补上协议头,变成了 http://3.12。
三层里每一层单独看都说得过去:空的就回退、有值就输出、缺协议就补全。凑在一起,就长出了一个永远打不开的链接。
单看任何一层,逻辑都是合理的;只有把三层串起来才出错。所以你在任何单个文件里都找不到「写错的地方」,只能靠实际输出反推。这也是老系统难改的根本原因——它的错误不存放在某个文件里,而是存在于调用链的接缝处。

修的时候有个岔路口:改数据,还是改代码?
改数据更快,把那些 image 的 alt 清空或者填成正常描述,页面立刻正常。但我选了改代码——在模板里加一道判断:只有 alt 本身是一条完整网址时才输出链接,否则留空。
理由是:数据会变,逻辑不会。今天这批图的 alt 是空的,明天运营换一张图、填了个「正面图」进去,改数据的方案会立刻复发;而代码里那道判断,对任何数据都成立。
同一个站里还有一类更朴素的污染。联系页上两个图标怎么都不显示,查下来是图片地址里多写了一层目录——wp-content/pxid-lab/wp-content/uploads/,等于把路径自己套了自己一遍。文件一直是好的,只是没人拼对过地址。
这种错误和前面那个畸形链接是同一类问题的两面:都不是「东西坏了」,而是「指向它的方式错了」。区别在于,路径重复只要改数据就够,而把 alt 当链接那个必须改逻辑——因为下一次换图,数据还会再错一遍。

五、什么该修,什么不该修
复核报告里列了六条「问题」。最后我判定其中两条不改。这个判断比修好四条更花时间,也更重要。
第一条:首页比原站大 61KB。
拆开看,多出来的体积里,有 75KB 是九个内联样式块,占整页 37%。第一反应是「样式内联得太多了,应该拆成外链」。但再想一层:这些样式是首屏关键样式,内联进 HTML 是为了让浏览器拿到文档就能渲染,不用等额外的 CSS 请求。
拆成外链,页面体积会小,但首屏会更慢。所以这不是缺陷,是用体积换首屏时间的取舍。改它等于把之前一次优化撤销掉。
第二条:访问速度偏慢。
原始数据是 0.67 到 0.85 秒,看着确实慢。但我把缓存命中与否分开测之后,画风变了:
| 场景 | 首字节时间 | 说明 |
|---|---|---|
| 清空缓存后首次访问 | 0.72 秒 | 需要现场生成页面 |
| 缓存命中 | 0.10 秒 | 直接读静态文件 |
| 原站对照 | 0.20 秒 | 同样的缓存机制 |
缓存命中时,复制站比原站还快一倍。原来那个「偏慢」的结论,是拿冷启动的单次采样当成了常态。
老系统改造最容易踩的坑,不是修不好 bug,是把别人的设计取舍当成 bug 去修。
判断标准其实很简单:这个改动做完,系统是更快了还是更慢了?如果是后者,你修的不是问题,是别人的答案。
六、50 轮部署,46 份记录
这是整个项目里最让我后背发凉的一个发现。
线上实际运行的版本是第 50 轮部署产生的。但所有交付记录、报告、补丁包,只记录到第 46 轮。
三个补丁包全部落后于线上。它们的说明里都写着「这不是完整备份,不能单独部署」,但这行字在交接时太容易被跳过了。如果真出事,按交付物回退,退到的不是「之前」,是「更早的之前」。
更麻烦的是回退文档:只覆盖了前 22 轮,明确写着「从第 22 轮倒序退回第 1 轮」。第 36 到 50 轮那批缓存层改动,一行说明都没有。
也就是说,真正容易出问题的那一半改动,恰好是没有退路的那一半。
这件事的教训跟技术无关:老系统最危险的地方,不是代码烂,是没人知道它现在到底是什么状态。代码烂可以重写,状态不明你连从哪开始都不知道。
七、如果只带走一处改动
复制站的使命是试验田。田地迟早要拆,拆之前唯一要想清楚的问题是:地里的东西,哪些值得搬回主站?
我修了四项,逐项对照原站之后,结论是只有一项值得带走:
| 修复项 | 是否带走 | 原因 |
|---|---|---|
| 产品页畸形链接 | 值得 | 原站数据干净没犯错,但模板代码完全一样,下次换数据源照样会中招 |
| 图片路径重复 | 不必 | 属于导入过程中的数据污染,原站本来就没有 |
| 企业简介页 | 不必 | 原站本来就有这页,是复制站缺,方向相反 |
| 爬虫声明文件 | 不必 | 原站已有且工作正常 |

四项里三项是复制站自己的历史遗留,带回去没有意义。真正值得的是那一处模板判断——它加在原站上,不会带来任何视觉变化,因为原站这一处输出的本来就是空链接。它只是把「将来可能出错」变成了「不会出错」。
这一步的判断方式,我觉得比修复本身更有价值:不要问「我修了什么」,要问「这个修复在原站上会不会产生副作用、会不会防止未来的问题」。两个问题都答不上来的修复,就是自我感动。
八、拆完之后留下的判断
整件事做完,我把收获归成三条,都不是技术层面的:
第一,先确认你的自检覆盖了什么,再相信它的结论。「全部通过」这句话本身没有信息量,得看它检查了什么、漏了什么。清单来源决定了结论的天花板。
第二,分清 bug 和取舍。老旧系统上每一个让你觉得别扭的地方,背后可能都有一次权衡。修之前先问:它是坏的,还是它只是不符合我的直觉?
第三,删掉试验田之前,先想清楚要带走什么。复制站本身没有价值,它的价值是被验证过的判断。而判断不以文件形式存在,只能在你拆之前提取出来。
至于那些技术细节——301 要先看状态码、模板要防住空值、回退链要覆盖到最新一轮——它们都只是这三条的具体表现。
老系统不会因为你改了几行代码就变新。但你可以因为搞清了它的边界,而不再怕它。