归档说明:本文于 2026 年 11 月 12 日上线,页面日期用于系列归档;案例发生时间以正文为准。
最近抽空把放了很久的个人旧博客彻底收拾了一遍,顺手让 Agent 全程参与了环境盘点、版本升级、配置迁移和上线验收。
这台老服务器上的历史包袱不算少:博客程序还停留在古早的 Typecho 1.1/17.10.30,底层跑着 PHP 7.2.33,站点配置和进程管理全绑在宝塔面板上,最前面还套了一层阿里云 CDN。单拎出其中任何一件事都不算难,但要把“卸载宝塔面板 + PHP 跨大版本升级 + Typecho 升级 + HTTPS 回源改造”放在同一台线上老机器上一次做完,稍不留神就会把站点搞挂。
最终结果还算圆满:博客顺利升到了 Typecho 1.3.0 和 PHP 8.3.36,宝塔面板彻底卸干净,HTTPS 访问和回源也全部跑通了。不过在切换过程中,自动化脚本先后触发了两次自动回滚:一次是因为 Nginx 平滑重载的进程切换时序,另一次是因为验收脚本在旧 Python 环境下的中文编码报错。这里把整个改造和排障过程记下来(文中域名统一脱敏为 blog.example.com)。
一、卸面板之前,先把站点配置独立搬出来
要卸载宝塔面板,第一步绝不是直接跑卸载命令,而是先把网站目录、数据库和现有的 Nginx 配置完整备份下来,再把站点迁移到完全独立于面板路径之外的标准 Nginx 配置目录里。
这个先后顺序非常关键:面板接管的站点往往在配置文件里引用了大量面板自带目录里的片段(比如 SSL 证书路径、PHP-FPM sock 路径、公共 rewrite 规则)。如果不先把这些引用剥离出来,一旦删掉面板目录,正在跑的 Nginx 就会因为找不到被 include 的文件而直接罢工。
我采用的迁移顺序是:
- 先把站点配置和证书挪到独立目录,确保不依赖面板路径也能正常拉起旧版站点;
- 确认旧站独立运行无误后,再清理面板服务、开机启动项和面板专用端口;
- 采用旁路方式独立编译/安装
PHP 8.3.36,单独拉起新的 PHP-FPM 服务,给后面的版本切换留出随时可回退的双轨空间。
在动手升级前,我还专门记下了一份精确的数据基线:当时博客里一共有 38 篇文章、4 个独立页面、70 条评论和 30 条友情链接,外加主题里的自定义样式和 42 张随机封面图。切完新版本后,脚本逐一核对了 9 张生产数据表的记录行数和 65 项预期配置项,防止出现“首页看着能打开,里面数据其实丢了一半”的情况。
当然,数据库行数对得上,也只能说明数据没丢;页面渲染、静态附件路径和主题设置还得靠实际访问来验收。后面遇到的主题封面图空白就是一个典型例子:数据库里的记录一条没少,但旧配置里引用的失效图片地址照样会让页面开天窗。
二、Typecho 跨版本升级:代码与数据库必须同进同退
从 Typecho 1.1 直接跨到 1.3.0,有两道绕不开的坎:
第一道坎是 PHP 版本要求。Typecho 1.3.0 官方发布说明 将最低 PHP 版本要求提高到了 PHP 7.4,这也是为什么这次必须先把 PHP 从 7.2.33 升到 8.3.36。
第二道坎是数据库结构变化。执行 Typecho 1.3.0 的官方升级流程时,程序会把 options 表里原本用 PHP serialize() 存储的序列化配置转换成 JSON 格式。这意味着代码和数据库必须绑定在一起升级或回滚:如果升级失败后只把代码切回 1.1,却留着已经转成 JSON 的数据库,旧版代码读配置时就会直接乱套。
因此,在动正式站之前,我先在同机拉起了一个隔离的测试副本,把后台登录、文章编辑、评论管理、主题和插件挨个测了一遍:
- 老旧的
Titleshow插件在新版 PHP 下报了兼容性错误,先在副本里修好验证通过; Cuckoo主题从2.0.0升到2.3.0后,文章封面图突然变成了一片空白。排查发现,主题文件和本地图片其实都在,只是新版主题读取了一条早年留下的、早已失效的外部图片 CDN 配置。把这条脏配置修正后,42 张随机封面才重新正常加载出来。
三、第一次自动回滚:Nginx reload 之后动作太快了
副本验证全过之后,开始执行正式切换脚本。没想到第一次切换刚跑完,健康检查就抓到了 HTTP 502 Bad Gateway,保护机制立刻触发自动回滚,把旧版代码、旧数据库和 PHP 7.2 恢复了回来。
为什么在副本里好好的,一到正式切换就报 502?
顺着时间戳翻看日志才恍然大悟:切换脚本在执行完 nginx -s reload(将 FastCGI 指向 PHP 8.3)之后,紧接着就停掉了旧的 PHP 7.2 服务,并立刻发起健康检查请求。
但正如 Nginx 官方进程控制文档 所说明的,reload 是一个平滑异步过程:主进程加载新配置并拉起新 worker 后,旧 worker 并不会瞬间消失,而是会处理完手头残余的连接再退出。当脚本紧跟着停掉 PHP 7.2 时,恰好还有一部分请求落在了尚未退出的旧 worker 上,旧 worker 去连已经关闭的 PHP 7.2 socket,当场就报了 502。
找到原因后,修起来就很清楚了:调整切换脚本的时序,先切维护页并等待新 worker 完全接管流量,确认新 PHP 8.3 稳定响应后,最后再去停掉旧的 PHP 7.2 进程。
四、第二次自动回滚:页面明明返回了 200,验收脚本自己崩了
调整完 Nginx 切换时序后发起第二次切换。这一回更让人哭笑不得:Web 服务其实已经稳稳当当地返回了 HTTP 200,但验收脚本自己却报错退出了,结果又触发了一次自动回滚!
查了一下脚本的异常堆栈,原来这台老机器上的系统默认 Python 还是 Python 3.6,而且非交互式执行环境里带着 LC_ALL=C。当验收脚本去读取返回的 HTML 正文(里面包含中文博客标题和文章内容)时,Python 3.6 默认按 ASCII 编码去解码字节流,当场抛出了 UnicodeDecodeError。验收脚本一崩溃,外层调度器以为站点挂了,二话不说又走了一遍回滚分支。
修复办法很简单:在验收脚本里显式加上 encoding="utf-8"(并规范环境变量)。
这两次看似折腾的自动回滚,反倒给我吃了两颗定心丸:
- 它实战检验了备份与回滚脚本是真的管用:两次回退都干净利落地恢复了现场,没有把线上站点卡在半死不活的中间态;
- 它提醒我写自动化验收脚本时,必须把“网络请求失败”“页面内容不符合预期”和“验收脚本自身运行异常”在日志里严格区分开。如果只看最后一个非零退出码,很容易误判故障发生的位置。
五、HTTPS 改造:卡在 CDN 动态回源的 301 死循环
源站升级完成后,接着处理 HTTPS。证书部署好之后,浏览器到阿里云 CDN 边缘节点这一段已经走上了 HTTPS,但打开网页却遇到了重定向死循环(ERR_TOO_MANY_REDIRECTS)。
排查 CDN 配置才发现:虽然边缘节点开了协议跟随(forward_scheme),但 CDN 的“动态回源”配置里,回源端口依然写死了 80。于是整个请求链变成了这样:
- 用户用
https://blog.example.com访问 CDN; - CDN 命中动态回源规则,走
80端口(HTTP)去请求源站; - 源站 Nginx 配置了 HTTP 强制跳 HTTPS,于是给 CDN 返回了一个
301 Moved Permanently跳往https://blog.example.com; - 浏览器收到 301 后再次请求 HTTPS,无限循环。
定位到这个参数后,把阿里云 CDN 的动态回源协议和端口一并改为 HTTPS / 443(相关参数可参考 阿里云 CDN 域名功能配置文档),死循环立刻消失。从外网实测请求 http://blog.example.com,经过一次干净的 301 跳转后,稳稳返回 HTTPS 200,浏览器到 CDN、CDN 到源站两段都走上了 HTTPS。

图 1:浏览器使用 HTTPS 时,旧 HTTP 80 回源仍会触发源站 301 循环。改为 HTTPS 443 回源,并维护两端证书。(机制示意,点击图片可放大。)
六、证书自动续期:既要更新源站,也要同步给 CDN
既然源站和 CDN 都上了 HTTPS,证书续期就必须两边同时管起来:如果只用 Certbot 续了源站本地的证书,忘了同步给阿里云 CDN,到期那天外部访问照样会报证书过期。
为了不在老服务器上明文存放长期有效的阿里云 AccessKey,我给这台 ECS 绑定了最小权限的 RAM 实例角色(仅授予当前域名所需的 CDN 证书更新权限),并在机器上配置了每天北京时间 04:17 运行的定时巡检脚本。脚本在运行时通过阿里云元数据服务(IMDSv2)实时换取临时安全凭据,一旦检测到本地证书更新,就自动调用接口把新证书推送到 CDN(机制说明见 阿里云 ECS 实例角色与访问控制文档)。
目前,证书的首次签发、CDN 自动上传链路以及定时任务的首轮触发都已经实测跑通(至于真正临近 90 天到期时的自动续签全流程,还得等时间到了再做最终确认)。
七、收尾验收与还没还完的技术债
全部切完之后,我按清单做了一轮最终验收:
- 首页、文章详情页、后台登录页、代码高亮静态资源全部正常加载,并通过了无头浏览器的画面渲染检查;
- 博客里原有的加密文章在未登录状态下访问,正文依然正常隐藏,没有因为升级而意外暴露;
- 之前用来做兼容测试的副本服务全部停掉,测试期间创建的临时账号和临时凭据文件全部清理干净,确认正式站没有残留维护页或测试端口。
不过有一笔技术债还是得如实记在账上:这次改造只通过旁路方式把 PHP 升到了 8.3.36、把 Typecho 升到了 1.3.0,但这台老服务器底层的操作系统依然是 CentOS 7.3,数据库还是 MySQL 5.5,系统自带的 OpenSSL 和 cURL 版本也很老。这些底层老组件没法靠原地升级彻底解决,后续还是得另找时间做一次干净的新系统整机迁移,把这笔历史旧账彻底结清。