主站在跑业务,不能拿来试错。于是我做了一件很朴素的事:把整站复制一份到子目录,所有改造先在这里验证,通过了再搬回生产环境。这件事听起来是个工程问题,做完之后我发现,它更多是个判断问题——难的不是把东西修好,是分清哪些该修、哪些本来就不该动,以及你的自检到底覆盖了什么。


一、为什么要复制一个老站

这个站跑了很久,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。

三层里每一层单独看都说得过去:空的就回退、有值就输出、缺协议就补全。凑在一起,就长出了一个永远打不开的链接。

这类 bug 的共同特征

单看任何一层,逻辑都是合理的;只有把三层串起来才出错。所以你在任何单个文件里都找不到「写错的地方」,只能靠实际输出反推。这也是老系统难改的根本原因——它的错误不存放在某个文件里,而是存在于调用链的接缝处。

PX-4 产品页的动力总成区块,三列图文与轮播均正常显示
修复之后的产品页。这个位置的图片原本包着一层永远打不开的链接,现在链接留空,图片照常展示。

修的时候有个岔路口:改数据,还是改代码?

改数据更快,把那些 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 要先看状态码、模板要防住空值、回退链要覆盖到最新一轮——它们都只是这三条的具体表现。

老系统不会因为你改了几行代码就变新。但你可以因为搞清了它的边界,而不再怕它。