没有不值得去解决的问题,也没有不值得去学习的技术!

WordPress CPU 再次满载:动态参数如何穿透 CDN 与 W3TC 页面缓存

作者:

前一轮 WordPress 502 故障中,我最终发现,一批携带 Sogou web spider/4.0 User-Agent 的异常高频请求持续冲击源站,导致只有 1 核 CPU 的阿里云 ECS 长时间满载,PHP-FPM 队列也接近耗尽。

在 EdgeOne 中增加 *Sogou* User-Agent 拦截规则以后,502 很快消失,CPU、Load 和 PHP-FPM 队列也恢复正常。

原本以为这次故障已经基本解决。

但安装并恢复阿里云主机监控以后,我很快发现:

CPU 仍然会不定时冲到 95%~100%。

这意味着,Sogou UA 只是上一轮严重故障的一个触发因素,服务器背后还存在另一个更基础的问题。

这次继续排查以后,我最终把问题收敛到了:

带任意 Query String 的前台请求,可以同时降低 CDN 缓存命中,并让 W3 Total Cache 的 Disk: Enhanced 页面缓存失效,最终直接进入 PHP-FPM。

这比单纯屏蔽某一个爬虫更值得处理。

而且,它也改变了我接下来优化 WordPress 缓存架构的方向。


一、Sogou 已经拦截,为什么 CPU 还是会冲到 100%?

恢复阿里云主机监控以后,我为 ECS 配置了 CPU、内存、磁盘和公网带宽报警。

随后很快收到多次 CPU Critical 告警。

例如:

Plaintext
2026-07-26 22:43  CPU = 100%
2026-07-27 01:34  CPU = 100%
2026-07-27 07:40  CPU = 99.26%

而这些告警并不是 CPU 全天持续偏高。

从一天监控曲线来看:

Plaintext
平时 CPU:约 20%~45%
偶尔突然升高
短时间达到 95%~100%
随后恢复

内存则一直比较稳定。

图1:阿里云云监控中一天的 CPU、内存和系统 Load 曲线
图1:阿里云云监控中一天的 CPU、内存和系统 Load 曲线

这首先说明:

当前这台 1 核 2GB ECS 并不是全天资源不足,而是某些时间段发生了突发负载。

因此我没有立刻升级到 2 核 4GB,而是继续找 CPU 峰值来源。


二、一开始我怀疑 WordPress Cron

服务器当前禁用了访客请求触发的 WP-Cron:

Plaintext
DISABLE_WP_CRON = 1

然后通过系统 Cron 每 5 分钟主动执行一次:

Plaintext
*/5 * * * * cd /data/wwwroot/www.shuijingwanwq.com && /usr/local/php/bin/php wp-cron.php >/dev/null 2>&1

这个频率很容易让人怀疑:

Plaintext
每 5 分钟执行 wp-cron.php

某些 WordPress 定时任务很重

CPU 被打满

WordPress Cron 列表里确实还有一个每 5 分钟运行一次的任务:

Plaintext
clarity_collect_batch

因此,Cron 一度成为第一嫌疑。

图2:服务器 root crontab 与 WordPress Cron 任务列表
图2:服务器 root crontab 与 WordPress Cron 任务列表

三、实时观察一轮 WP-Cron,却发现它非常轻

为了确认,我没有修改 Cron,而是直接观察下一轮 wp-cron.php

10:00 左右:

Plaintext
10:00:02  wp-cron 未运行

10:00:08
php wp-cron.php
CPU ≈ 31.2%

10:00:13
wp-cron 已经结束

也就是说:

一次普通 WP-Cron 运行只有几秒。

它显然无法解释:

Plaintext
CPU ≥95%
连续数分钟

因此,WP-Cron 并不能作为当前 CPU Critical 的主要解释。

当然,某些特殊 Cron Hook 仍然可能偶尔比较重,但排查优先级已经应该下降。


四、把监控缩到 15 秒以后,确认 07:40 的确持续满载

接着,我把阿里云进程监控时间范围缩小到:

Plaintext
07:35~07:45

统计周期也因此缩短到约 15 秒。

这一次 CPU 曲线非常清晰:

Plaintext
约 07:37
CPU 开始接近 100%

持续数分钟

约 07:40 后
逐渐恢复

所以:

Plaintext
07:40 CPU 99.26%

不是监控误报。

服务器当时的确处于持续满载状态。

图3:07:35~07:45 的 15 秒 CPU 监控曲线
图3:07:35~07:45 的 15 秒 CPU 监控曲线

不过阿里云的进程 TopN 并没有完整抓到这几分钟中的高占用进程,因此我继续回到服务器日志。


五、真正的异常出现在 PHP-FPM

PHP-FPM 主日志里,很快出现了非常关键的一行:

Plaintext
[27-Jul-2026 07:36:16]
server reached max_children setting (7)

也就是说:

Plaintext
pm.max_children = 7

已经全部用完。

与此同时,从 07:36 开始出现大量:

Plaintext
index.php
executing too slow
3~4 秒

而且这些请求不是后台,也不是 Cron。

基本都是前台请求:

Plaintext
/index.php
/index.php?amp=1
/index.php?noamp=mobile
/index.php?nonamp=1
/index.php?query-62-page=...

从 07:36 到 07:42,一直都有 PHP-FPM slow request。

图4:PHP-FPM 达到 max_children=7,并持续出现前台慢请求
图4:PHP-FPM 达到 max_children=7,并持续出现前台慢请求

这时,故障链已经发生变化:

Plaintext
大量前台请求

进入 PHP-FPM

7 个 Worker 全忙

单核 CPU 接近 100%

新请求继续排队

所以 Cron 并不是这次 CPU 峰值的主要矛盾。


六、Slow Log 进一步证明:WordPress 正在完整渲染这些请求

PHP-FPM Slow Log 中可以看到很多真实的 WordPress 调用链。

包括:

Plaintext
Gutenberg
Polylang
W3 Total Cache
Redis Object Cache
Post Views Counter
WP_Query
mysqli_query

例如其中一部分请求会经过:

Plaintext
W3 Total Cache

Redis Object Cache

Polylang

Gutenberg 区块渲染

说明 Redis Object Cache 确实仍然在工作。

另外还有多次调用:

Plaintext
Post Views Counter

pvc_get_most_viewed_posts()

WP_Query

mysqli_query()

这里需要特别区分:

Object Cache 生效,并不代表 Page Cache 生效。

Redis 可以减少一部分数据库查询和对象读取成本,但只要 WordPress PHP 已经启动:

Plaintext
插件
主题
Gutenberg
Polylang
动态区块
数据库查询

仍然需要继续执行。

而真正能让 PHP-FPM 完全轻松下来的,是:

整页 Page Cache 命中。


七、8 分钟只有 687 个请求,却足以打满服务器

我随后分析了:

Plaintext
07:35~07:42

这 8 分钟的 Nginx 日志。

总请求数:

Plaintext
687

状态码:

Plaintext
499   367
200   173
301    97
404    44
307     5
403     1

其中 499 超过一半。

这通常意味着:

客户端还没等服务器完成响应,就已经主动断开连接。

结合此时 PHP-FPM 已经 7/7、请求执行时间超过 3 秒,499 更像是服务器过载后的结果。


八、流量不是单一 IP,而是明显分散

来源 IP Top 中,并没有某一个 IP 发几百次请求。

最高的也只有:

Plaintext
17
16
14
13
...

但来源分布在大量 IP 和网段中。

而且其中有一些 IP,与前一天携带 Sogou UA 的异常流量来源发生了重合,例如:

Plaintext
1.71.146.14
122.246.2.213
1.71.147.97
222.79.116.117
1.71.147.232

因此存在一种可能:

前一天按 *Sogou* UA 拦截以后,部分自动化请求仍然通过其他 User-Agent 继续访问。

不过由于 User-Agent 可以伪造,我仍然不打算简单把这些请求全部归类成某个确定机器人。

真正更值得关注的是它们的访问行为。


九、User-Agent 已经无法作为可靠的唯一判断条件

这一批请求中混合了:

Plaintext
Chrome
Firefox
Safari
Android Chrome

以及明确机器人:

Plaintext
MJ12bot
Googlebot
meta-externalagent
Amazonbot

甚至很多普通浏览器 UA 看上去完全像真人。

所以:

Plaintext
UA = Sogou
→ Block

只能解决很小的一类情况。

如果以后自动化流量直接伪装成:

Plaintext
Chrome
Safari
Firefox

继续按 UA 打补丁就没有意义了。


十、真正值得注意的是 Query String

这 687 个请求里:

Plaintext
amp=1            106
noamp=mobile      33
nonamp=1          49
query-62-page     59

同时还有:

Plaintext
/page/119?query-62-page=111
/page/134/?query-62-page=135

...?amp=1
...?nonamp=1
...?noamp=mobile

这让我开始怀疑:

真正的问题可能不是某一种爬虫,而是带参数 URL 本身能够绕过缓存。

图5:07:35~07:42 的来源 IP、User-Agent、URI 和状态码统计
图5:07:35~07:42 的来源 IP、User-Agent、URI 和状态码统计

十一、于是直接测试 W3TC Page Cache

为了确认,我直接绕过 CDN,通过 127.0.0.1 请求源站。

先测试正常首页:

Plaintext
/

第一次:

Plaintext
HTTP 200
TTFB = 9.03 秒

第二次:

Plaintext
HTTP 200
TTFB = 0.11 秒

W3TC 输出:

Plaintext
使用页面缓存 Disk: Enhanced

而且第二次:

Plaintext
Served from

时间与第一次完全一致。

这证明正常 URL 的 Page Cache 工作完全正常:

Plaintext
第一次
WordPress 完整生成

↓ 写入 Page Cache

第二次
直接复用 HTML
图6:正常 URL 第一次约 9 秒、第二次仅约 0.1 秒
图6:正常 URL 第一次约 9 秒、第二次仅约 0.1 秒

十二、加一个参数以后,W3TC 页面缓存直接失效

随后测试:

Plaintext
/?amp=1

第一次:

Plaintext
TTFB = 11.11 秒

第二次:

Plaintext
TTFB = 9.42 秒

W3TC 明确输出:

Plaintext
Disk: Enhanced (Requested URI contains query)

而且两次 Served from 时间不同。

也就是说:

第二次请求没有复用第一次生成的页面。

?noamp=mobile

Plaintext
第一次:7.94 秒
第二次:9.01 秒

?nonamp=1

Plaintext
第一次:7.70 秒
第二次:7.83 秒

情况完全一样。

图7:带 Query String 后,W3TC 显示 Requested URI contains query
图7:带 Query String 后,W3TC 显示 Requested URI contains query

至此,这次 CPU 峰值的核心机制已经非常清楚。


十三、真正危险的不是 amp,而是“任意参数”

一开始很容易想到:

Plaintext
amp
noamp
nonamp

那就直接屏蔽或者重定向这三个参数。

但很快我意识到:

这是治标不治本。

因为攻击者根本不需要使用这些参数。

完全可以生成:

Plaintext
?abc=1
?abc=2
?foo=123
?random=999
?test=xxxxx

参数名和参数值理论上都可以无限变化。

只要当前缓存机制判断:

Plaintext
存在 Query String

W3TC Page Cache 不复用

那么攻击者就相当于拥有一个非常简单的:

强制 WordPress 动态渲染开关。

对于一台 1 核 ECS 来说,这一点尤其危险。


十四、这也解释了为什么 687 个请求就能造成明显影响

正常缓存请求可能只需要:

Plaintext
0.1 秒

而带 Query String 的动态请求:

Plaintext
7~11 秒

差距接近两个数量级。

如果 7 个 PHP-FPM Worker 同时处理这样的请求:

Plaintext
7 个 Worker
×
每个持续数秒

服务器很快就会进入:

Plaintext
pm.max_children = 7

请求排队

CPU 100%

响应变慢

499 增加

这已经不再是“访问量大不大”的问题。

而是:

同样的访问量,以缓存请求和动态请求进入服务器,成本完全不同。


十五、因此我暂时不打算升级 2 核 4GB

这次分析以后,我反而更不倾向于马上升级 ECS。

因为平时:

Plaintext
CPU 约 20%~45%
内存约 40%~50%

真正的 100% 峰值主要发生在缓存被大量穿透的时候。

升级到 2 核 4GB 可以提高承受能力,但不能解决:

Plaintext
任意 Query String

持续制造动态 WordPress 请求

今天是 687 个请求。

以后完全可能变成:

Plaintext
1000
2000
5000

所以更合理的顺序应该是:

Plaintext
先修缓存穿透

继续观察 CPU

正常业务仍然频繁满载

再考虑升级服务器

十六、我也不准备深度修改 W3TC

理论上,可以研究 W3TC,让:

Plaintext
/article/
/article/?foo=1
/article/?foo=999

共用同一份 Page Cache。

对于一些已知参数,W3TC 本身也有相关机制。

但真正的问题是:

参数可以有无限多个。

我真正想要的是:

Plaintext
默认忽略无意义 Query String

只有真正会改变内容的少数参数
作为例外

如果为了实现这一点去修改:

Plaintext
advanced-cache.php
W3TC 内部缓存逻辑
Cache Key

不仅排查时间会继续增加,而且以后 W3TC 升级、WordPress 升级还可能增加维护成本。

这已经偏离了我这次的目标:

尽快解决 CPU 被异常请求打满的问题。


十七、更适合我的方案:让 CDN 做 URL 规范化

因此,我目前准备把这件事放到 CDN 层。

中文站:

Plaintext
www.shuijingwanwq.com
→ Tencent EdgeOne

英文站:

Plaintext
en.shuijingwanwq.com
→ Cloudflare

目标逻辑基本一致。

对于确定“不依赖 Query String 内容”的公开页面:

Plaintext
/article/
/article/?foo=1
/article/?foo=999

CDN 层尽量认为它们是:

Plaintext
同一个缓存对象

如果 CDN 已经 HIT:

Plaintext
直接返回

如果 CDN MISS:

Plaintext
回源时清理无意义参数

源站收到干净 URL

W3TC Disk: Enhanced

继续复用正常 Page Cache

于是最终形成:

Plaintext
客户端
/article/?random=123

↓ CDN

忽略无意义 Query String

↓ HIT

直接返回

↓ MISS

回源:
/article/

↓ W3TC

命中 Disk: Enhanced Page Cache

↓ PHP-FPM

不需要完整执行 WordPress

这比去改 W3TC 内部逻辑更符合我现在的需求。


十八、为什么这次不直接全站忽略 Query String

这里还有一个非常重要的边界。

WordPress 并不是所有参数都没有意义。

例如:

Plaintext
?p=123
?s=wordpress
?page_id=123
?preview=true

以及这次日志中出现的:

Plaintext
query-62-page

其中一些参数确实会改变页面内容。

因此不能简单:

Plaintext
全站所有 Query String
→ 全部忽略

否则可能出现:

Plaintext
/?s=PHP
/?s=WordPress

拿到同一个缓存页面。

甚至:

Plaintext
?p=100
?p=200

也出现错误内容。

所以后续配置的核心不是:

“所有参数都忽略。”

而是:

只针对适合强缓存、参数通常不应该改变内容的 URL 类型做规则。

我目前首先考虑的就是:

Plaintext
WordPress 文章详情页

这是数量最多、访问价值最高,同时语义又最明确的一类公开页面。


十九、EdgeOne 与 Cloudflare 都可以采用类似思路

中文站的 EdgeOne 可以从两个方面处理:

Plaintext
Cache Key
+
回源请求参数

英文站 Cloudflare 也有对应能力:

Plaintext
Cache Rule
+
Transform Rule

因此即使两个站点使用的是不同 CDN,最终仍然可以保持同一种架构逻辑:

Plaintext
www / EdgeOne
        \
         → Query String 规范化
        /
en / Cloudflare


源站收到干净公开页面 URL


W3TC Disk: Enhanced

这样后续维护也不会变成两套完全不同的思路。


二十、这次排查最大的收获,其实不是找到某一个爬虫

一开始的问题看起来是:

Plaintext
Sogou Spider

CPU 100%

502

于是很自然地认为:

把 Sogou 挡掉就好了。

但恢复监控以后,新的 CPU Critical 让我继续往下查,最终发现真正值得长期处理的是:

Plaintext
自动化请求

大量带 Query String 的 URL

CDN 未能充分吸收

W3TC Disk: Enhanced 因 Query String 不复用 Page Cache

请求进入 PHP-FPM

完整执行 WordPress

单个请求耗时数秒

少量并发即可打满单核服务器

相比“封一个 User-Agent”,这显然是更底层的问题。

而它也让我重新理解了一点:

WordPress 性能优化不能只看有没有缓存,还要看请求是否能够稳定落到同一个缓存键上。

缓存存在,但如果攻击者或者爬虫可以通过随意添加参数不断绕过,那么缓存保护能力就会大幅下降。


二十一、当前阶段:先停止继续深挖服务器

到这里,我认为这一轮问题已经定位得足够清楚。

暂时不准备:

Plaintext
升级 ECS
提高 pm.max_children
修改 W3TC 内部代码
逐个封禁参数
逐个封禁 IP
继续追某一个 User-Agent

下一阶段真正需要做的是:

Plaintext
EdgeOne

验证文章详情页 Query String 缓存规范化

Cloudflare

配置等价规则


测试随机参数 URL


确认 CDN HIT


强制 MISS 时确认 W3TC 仍能命中干净 URL Page Cache


继续观察 CPU Critical 是否明显减少

这些具体操作,我准备放到下一篇文章中单独记录。

因为相比这篇的“问题为什么发生”,下一篇更适合完整记录:

如何在 EdgeOne 与 Cloudflare 中为 WordPress 文章详情页建立 Query String 缓存规范化规则。


写在最后

这次故障排查其实经历了几个不同阶段:

Plaintext
502

CPU 100%

PHP-FPM 队列耗尽

发现 Sogou UA 异常流量

EdgeOne 拦截

502 恢复

恢复阿里云主机监控

继续出现 CPU Critical

怀疑 WordPress Cron

Cron 实测正常

定位 PHP-FPM 前台慢请求

发现大量带 Query String 的 URL

直接测试 W3TC

确认 Query String 导致 Page Cache 不复用

最终决定从 CDN 层处理缓存规范化

所以这一次,真正解决问题的关键不是某一条命令,也不是某一个插件。

而是逐步把:

“什么东西在占 CPU”

继续追问到:

“为什么这些请求会落到 PHP 上”。

现在答案已经比较明确。

下一步,就是把这个分析真正转化为 EdgeOne 和 Cloudflare 的生产配置。

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理