旧论坛在旧系统上运行多年( 2013 年左右开始建站、17年之后就很少维护了 )。
最近 PHP 爆了几个漏洞,还有梯子频繁被封 IP,就调整了一下架构。
然后一起把漏洞处理一下。
新版 Discuz 对 PHP 的要求已经超过旧环境,原地升级会让操作系统、运行时、数据库和论坛程序一起变化。
结合 aws LightSail CPU 突发型的实例,尽量降低 CPU 占用,避免影响线上服务。
所以就先把 Discuz 程序代码 + 数据库文件拉到本地,用 docker 的方式进行迁移验证。
近期 AI 爬虫是真多,迁移快完事的时候,把我站点都打挂了。
一个没什么人访问的老论坛,在线人数直接创新高。
总计 1058 人在线 - 最高记录是 30437 于 2026-11-3
1. 迁移目标
迁移要同时完成四件事:
- 把 Discuz X3.4 按官方路线升级到 X5.0.4.Open:X3.4 → X3.5 → X5.0.4。
- 把运行环境换到新系统和新版 PHP、MariaDB、Redis。
- 保留 Cloudflare、HAProxy、Xray、Reality 和 Clash 的使用方式。
- 先并行验证,再切正式流量,避免新系统和旧站互相影响。
DNS 只处理论坛域名和 WebSocket 入口。其他子域、邮件记录和无关服务不改。
最终链路仍然是 CDN 到源站:
flowchart LR
U[访客] --> CF[Cloudflare]
CF --> HA[HAProxy]
HA --> NG[Nginx]
HA --> XR[Xray]
NG --> PHP[PHP-FPM]
PHP --> DB[(MariaDB)]
PHP --> RD[(Redis)]
2. 为什么先在线下升级
旧站运行多年,系统和运行时都落后于新版 Discuz 的要求。
直接替换线上目录,升级程序会在真实流量下修改数据库、模板、插件和缓存。
一次失败可能同时影响网站、后台和代理链路。
本地准备了两套环境:
- 一套按旧环境复刻,专门执行 Discuz 官方数据库升级。
- 一套换成新实例准备使用的运行环境,提前验证兼容性。
生产数据先导入本地。升级脚本按官方路线逐级执行,不跳版本。
升级完成后,再逐项检查首页、论坛页、帖子页、登录、后台、插件、头像、附件、私信和模板。
本地验证发现的问题,先在测试库和测试目录里修。只有验证通过的代码、数据库和补丁才进入生产候选包。
3. Discuz 和 PHP 兼容
大版本升级不是把压缩包解压到目录就结束。
旧论坛积累的插件、模板和 UCenter 数据,会和新版核心同时参与请求。
| 问题 | 表现 | 原因 | 处理 |
|---|---|---|---|
| 官方包依赖目录为空 | 新版无法正常启动 | 发布包没有带全依赖 | 单独取回依赖并合并 |
| 封禁缓存方法缺失 | 重建缓存时报缺少方法 | 发布包没有实现该方法 | 按现有表结构补回 |
| UCenter 模式不兼容 | 登录和头像异常 | 旧服务模式不再适用 | 切换到独立模式 |
| 新运行时弃用旧写法 | 页面和插件报错 | 旧代码使用新运行时不再支持的行为 | 应用兼容补丁并全量检查 |
| luckypost 不初始化 | 插件功能失效 | 使用早期风格的构造函数 | 改为新构造方式 |
| 私信没有开启 | 提示私信功能尚未开启 | 新版本新增开关,升级时没有补值 | 补写设置并重建缓存 |
| 早期账号无法登录 | 正确密码仍失败 | 密码格式没有盐值,失败计数又按名称和数字账号分别记录 | 兼容旧哈希,清理两类计数,重置后实测 |
| 登录验证码不显示 | 提示验证码但页面没有输入框 | 跳转令牌漏标记,生成和读取密钥也不一致 | 统一密钥并补齐标记 |
这些问题都要在页面样式之前处理。核心请求、登录和缓存不稳定时,继续修 CSS 只会制造更多噪音。
4. 新实例怎么接数据
新实例原生安装 Nginx、PHP、MariaDB、Redis、HAProxy 和 Xray,不使用 Docker。
旧站依赖容器的地方,在新环境里全部改成系统服务和明确的本地端口。
迁移数据分三部分:
- 代码和补丁进入站点目录。
- 离线升级后的数据库导入新实例。
- 附件和头像单独同步,不塞进代码包。
附件和头像使用可续传的文件同步,完成后检查文件数量和目录权限。
Web 进程必须能读取这些文件,否则页面能打开,图片和下载仍然会坏。
生产配置从旧实例单独迁移。测试环境的数据库主机、Redis 主机、域名和路径不能带过去。
5. 切换前怎么验证
DNS 没有立即改。先把正式域名临时解析到新实例,完整访问一遍。
验证不只看首页状态码,还包括:
- 版块、帖子、个人空间和附件。
- 前台登录、后台登录和登录后跳转。
- 发帖、回帖、私信和头像。
- 编辑器、认证图标、favicon 和静态资源。
- WebSocket 入口和代理节点。
- TLS 证书链。
页面里还不能残留测试端口和测试域名。数据库里的站点 URL、附件 URL 和 UCenter 地址必须改成生产值。
| 问题 | 现象 | 根因 | 处理 |
|---|---|---|---|
| 生产地址被写成空值 | 页面能开,但跳转、头像和附件异常 | 迁移脚本在错误的命令行环境下执行 | 手工修回数据库并重建全部缓存 |
| 静态资源指向临时目录 | 编辑器有按钮但没有图标 | 生成缓存时缺少脚本路径和站点根目录 | 补齐运行环境后重建样式和导航缓存 |
| 旧模板仍在复用 | 修改后浏览器继续显示旧样式 | 模板缓存没有清理,资源地址没有版本变化 | 清理编译模板并重新生成页面 |
迁移脚本因此被标记为不能盲跑。重复执行不能写坏生产值,站点 URL 改完后也不能只刷新应用缓存。
6. 切换时的顺序问题
正式切换改了论坛域名、WWW 域名和 WebSocket 入口的 A、AAAA 记录。
Cloudflare 记录都是代理模式,客户端连接的是 CDN,源站变更不需要等待本地 DNS 缓存。
第一次切换使用新实例的动态公网 IP。随后又把旧实例的静态 IP 迁到新实例,A 记录再做了一次更新。
这个顺序不理想。静态 IP 应该在第一次 DNS 切换前整理好,避免多一次源站地址变化。
对代理模式来说,第二次切换不一定造成长时间中断,但会增加验证项,也容易让配置和文档出现两个版本的 IP。
静态 IP 迁走后,Clash 的 IPv4、IPv6 节点和控制面板配置都要同步。只改 DNS 不够。代理节点如果还记着旧地址,网站恢复了,代理仍然不可用。
旧实例最后被删除,保留一份快照作为回滚点。快照恢复出来的是新实例,不能直接覆盖当前机器,所以回滚还要配合静态 IP 和 DNS。
7. 页面和登录问题
页面问题看起来零散,原因大多相同:旧数据和新缓存没有对齐。
| 问题 | 现象 | 根因 | 处理 |
|---|---|---|---|
| 头像路径不一致 | 头像入口返回后再报文件不存在 | 文件在 UCenter 目录,旧路径指向空目录 | 建立兼容链接,删除失效的旧域名重写 |
| 认证图标路径重复 | 图片地址里出现两段附件前缀 | 数据库保存完整路径,缓存又拼一次 | 统一为相对路径并重建设置缓存 |
| 签到插件显示占位符 | 页面出现未解析的语言标记 | 插件缺少语言文件 | 补充语言文件并刷新语言缓存 |
| favicon 仍是默认图标 | 浏览器标签显示框架默认图 | 根目录图标没有迁移 | 替换为旧站图标并加版本参数 |
| 编辑器图标全部消失 | 工具栏只有文字或空白 | 样式缓存把静态前缀写进临时目录 | 修正缓存环境后重建样式 |
页面和登录问题处理完后,又专门用真实表单验证了登录和需要验证码的失败场景。
只看登录接口返回值不够,跳转、Cookie 和验证码状态必须一起检查。
8. 上线后的流量故障
DNS 切换完成后,源站很快被打满。
访问日志里大量请求集中在论坛帖子、个人中心、搜索和插件接口。
请求来自很多不同地址,单个 IP 的请求并不多。单 IP 限流能挡住一部分异常客户端,但挡不住这种分布式抓取。
8.1 流量特征
三万多条日志里,论坛动态页占了大头,个人中心、搜索和插件接口也持续被访问。最高频的单个地址只有十几次请求,绝大多数地址只出现一两次。
这不是一个脚本在固定频率抓取,而是大量地址分散访问。只按单 IP 限速,命中率很低。必须同时限制异常路径和全体匿名流量。
8.2 Nginx、HAProxy 和 Cloudflare
| 问题 | 表现 | 原因 | 处理 |
|---|---|---|---|
| AJAX 登录 502 | 登录请求短暂失败 | FastCGI 响应头缓冲不足 | 增大缓冲 |
| Nginx fd 太小 | 报打开文件过多 | 进程上限远低于 worker 连接数 | 提到 65535 |
| conntrack 打满 | SSH 重置,Cloudflare 504 | 连接跟踪上限只有 7680 | 提到 262144 并缩短超时 |
| PHP worker 占满 | 页面 502 和 504 | 12 个 worker 无法承受突发流量 | 临时提到 32,并加 10 秒硬超时 |
| 机器人持续抓取 | 大量 502、499 和 444 | 分布式地址访问论坛、私信和搜索接口 | Nginx 路径拦截、限流和 Cloudflare 验证 |
CPU 使用率看起来不高,并不代表机器有余量。大量时间花在等待调度和超时上,Cloudflare 侧持续出现网关超时,SSH 也会间歇性断开。
第一轮处理提高了文件描述符、连接跟踪和 PHP worker 上限,同时增加 PHP 请求硬超时。
源站能短暂恢复,但机器人流量继续进入后,实例的 CPU credits 仍然会被耗尽。
8.3 Nginx 管路径,HAProxy 管连接
第二轮处理把拦截放到 Nginx 的 HTTP 层。
未登录访问回复页、私信页、用户中心动作、热搜入口和 RSS 时,直接断开连接。
静态资源不限流,PHP 请求增加单 IP 和全局限流。
| 层 | 能看到什么 | 适合做什么 | 局限 |
|---|---|---|---|
| HAProxy | IP、端口和 TLS SNI | 连接数、TCP 转发和 SNI 路由 | TCP 模式看不到 URI 和 HTTP 头 |
| Nginx | URI、参数、真实 IP 和 Cookie 字符串 | 路径过滤、单 IP 限流和请求超时 | 不能验证 Discuz 的登录 Cookie |
| Cloudflare | 边缘请求和通行状态 | 人机验证、WAF 和全局拦截 | 需要单独配置规则 |
这里也验证了一个判断:Nginx 能看完整 URI、参数和 Cookie,适合做业务路径过滤。
HAProxy 在这套链路里是 TCP 透传,只能看 IP 和 TLS SNI,看不到 URI。
路径规则放在 Nginx 更准确,也不会破坏 Reality 的透传。
Cloudflare 会把真实访客地址放进请求头。Nginx 据此恢复真实 IP,访问日志和单 IP 限流都能拿到原始地址。
HAProxy 只转发字节流,不解析 HTTP,所以它看不到这个头,也不需要为了路径过滤去拆 TLS。
登录 Cookie 不能只判断是否存在。Nginx 能看到 Cookie 字符串,却不能验证签名、过期时间和会话状态。
机器人带一个伪造值也能绕过简单的字符串判断。紧急阶段因此暂停了 Cookie 绕行,所有动态请求先共用低速率。
8.4 Cloudflare 才是最后的刹车
但仅靠源站限流仍然不够。机器人来自大量不同 IP,源站必须立刻把请求挡在 CDN 边缘。
Cloudflare 里原本已经有一条人机验证规则,只是一直处于关闭状态。
启用 Managed Challenge 后,浏览器先进入安全验证,源站请求量迅速下降,PHP worker 回到空闲,CPU steal 也降了下来。
启用前,源站请求量仍在持续增长。
启用后,正常浏览器先完成一次安全验证,后续访问使用 Cloudflare 的通行状态。
机器人没有通行状态,请求在边缘被拦下。
为了避免 CPU credits 继续被消耗,PHP 请求还临时保持了极低的全局速率。
这个阶段部分访客会看到 429。它不是最终状态,只是给实例恢复 CPU 配额留时间。
9. 这次迁移得到什么
新实例已经承载正式流量,静态 IP、DNS、Clash 和控制面板配置全部对齐。
旧实例已经删除,快照暂时保留。Cloudflare 人机验证仍在运行,源站处于保护模式。
迁移过程里最有效的几条做法是:
- 生产数据先离线升级,不直接在线上做不可逆修改。
- 新旧实例并行,切换前用正式域名验证新站。
- 静态 IP、DNS、代理配置和站点配置一起验收。
- 迁移脚本必须可重复执行,不能把空值或测试域名写回生产。
- 站点地址变化后,代码、数据库和各类缓存要一起刷新。
- 面向公网的论坛要在切流前准备机器人防护,不能等源站被打挂再补。
- 小型实例要观察 CPU credits、连接跟踪和文件描述符,不只看 CPU 百分比。
迁移还没有完全收尾。Cloudflare 的人机验证需要继续观察,Nginx 的临时低速率要在 CPU 配额稳定后恢复。
源站的 80 和 443 长期只应允许 Cloudflare 官方地址访问,避免机器人绕过 CDN 直连。
旧实例快照也不会长期保留。稳定观察一段时间后,再决定是否删除。