此前,我已经针对 WordPress 文章详情页做过一次 Query String 归一化处理。
当文章详情页带有 amp、nonamp、随机测试参数或其他无意义查询参数时,CDN 不再为每一种参数组合分别建立缓存,而是统一复用 canonical URL 对应的缓存。同时,在缓存未命中并回源时,查询参数也不会继续传给 WordPress。
此前的处理过程记录在这篇文章中:
通过 EdgeOne 与 Cloudflare 统一 WordPress 文章 Query String
不过,后来发生的一次 EdgeOne 524 故障让我发现,仅处理文章详情页还不够。
大量分类页、标签页、系列页、日期归档页和分页地址,同样会被爬虫附加各种查询参数。如果这些参数全部参与 CDN 缓存 Key,就会制造大量缓存碎片,并反复触发 WordPress 动态回源。
这一次,我把相同的 Query String 归一化方案扩展到了 WordPress 的公开列表页面,并分别在中文站 EdgeOne 和英文站 Cloudflare 上完成配置与验证。
一、问题是怎样暴露出来的
2026 年 8 月 2 日晚间,中文站一度出现大量 EdgeOne 524。
故障窗口内,EdgeOne 统计到了 151 次 524:
- 缓存 MISS:147 次
- 动态请求:4 次
- 缓存 HIT:0 次
这些请求并不集中在某一篇文章,而是分散在首页、分类页、标签页、日期归档和分页地址中。
源站 Nginx 日志中同时出现了大量 499。请求中包含首页、分类页以及带有 query-62-page、amp、nonamp 等参数的地址。
PHP-FPM 当时使用以下配置:
- ECS:1 核 CPU
pm = ondemandpm.max_children = 7request_slowlog_timeout = 3srequest_terminate_timeout = 600s
慢日志显示,在故障期间,7 个 PHP-FPM 子进程曾同时卡在 index.php 中。随后日志中又出现:
server reached pm.max_children setting (7)
由于服务器只有 1 核 CPU,继续提高 pm.max_children 并不是一个合理方向。
即使增加 PHP 进程数量,也只是让更多进程争抢同一个 CPU。真正需要减少的是:
同一个页面因为不同 Query String 被反复当作不同资源,导致 CDN 缓存 MISS,并同时回源执行 WordPress。
二、为什么查询参数会放大源站压力
下面这些 URL 在 WordPress 中实际上可能返回完全相同的内容:
https://www.shuijingwanwq.com/tag/cache/
https://www.shuijingwanwq.com/tag/cache/?amp=1
https://www.shuijingwanwq.com/tag/cache/?nonamp=1
https://www.shuijingwanwq.com/tag/cache/?query-62-page=43
https://www.shuijingwanwq.com/tag/cache/?random=value
但如果 CDN 默认将完整 Query String 纳入缓存 Key,它们就会被视为多个不同的缓存对象。
每一种新参数组合首次出现时,都可能触发一次源站回源。
当爬虫同时访问大量分类、标签、分页和归档页面时,这些缓存碎片就可能在短时间内占满 PHP-FPM 的全部进程。
因此,这次优化采用两层处理:
- Query String 不参与 CDN 缓存 Key;
- 缓存 MISS 回源时,直接删除 Query String。
第一层避免缓存碎片,第二层避免无意义参数继续传入 WordPress。
三、需要覆盖哪些公开列表页面
我先整理了中文站当前真实存在的列表页面:
https://www.shuijingwanwq.com/
https://www.shuijingwanwq.com/page/4/
https://www.shuijingwanwq.com/category/program-development/
https://www.shuijingwanwq.com/category/program-development/page/2/
https://www.shuijingwanwq.com/tag/cache/
https://www.shuijingwanwq.com/tag/cache/page/2/
https://www.shuijingwanwq.com/series-category/blog-management-practices/
https://www.shuijingwanwq.com/series/wordpress-cdn-global-acceleration-practice/
https://www.shuijingwanwq.com/series/wordpress-cdn-global-acceleration-practice/page/3/
https://www.shuijingwanwq.com/2026/
https://www.shuijingwanwq.com/2026/page/32/
https://www.shuijingwanwq.com/2026/07/
https://www.shuijingwanwq.com/2026/07/page/3/
https://www.shuijingwanwq.com/2026/07/30/
规则还需要兼容未来可能出现的分页:
/series-category/blog-management-practices/page/2/
/2026/07/30/page/2/
作者归档没有纳入范围。
本站是单作者博客,并且已经在 Yoast SEO 中关闭作者归档,因此 /author/.../ 不属于当前有效的公开列表页面。
只处理带结尾斜杠的 canonical URL
规则只匹配结尾带 / 的地址。
没有结尾斜杠的地址,例如:
/tag/cache
/category/program-development
会先由 WordPress 或现有规范化逻辑 301 跳转到:
/tag/cache/
/category/program-development/
随后再由 CDN 规则处理。
这与此前文章详情页的处理方式保持一致。
四、搜索页面必须保留 Query String
不能简单地对首页和所有 /page/数字/ 地址删除查询参数。
WordPress 搜索页依赖 s 参数:
https://www.shuijingwanwq.com/?s=autopoly
https://www.shuijingwanwq.com/page/2/?s=autopoly
搜索分页的路径本身也是 /page/2/。
如果只根据 URL Path 判断,搜索分页就可能被误认为普通首页分页,随后删除 s=autopoly,最终返回普通文章列表。
因此,列表页分支必须额外要求:
查询参数 s 不存在
我还同时排除了列表页面中的 WordPress 预览参数:
preview
preview_id
preview_nonce
五、EdgeOne 中文站配置
我没有为文章详情页和公开列表页分别创建两条规则,而是把它们合并到同一条规则中。
规则名称为:
www 文章与公开列表页 Query String 归一化
最外层条件只有:
HOST 等于 www.shuijingwanwq.com
配置过程中遇到的一个限制
最初,我还在最外层加入了:
请求方法等于 GET、HEAD
但加入请求方法条件后,EdgeOne 界面不再允许添加“自定义 Cache Key”操作。
因此,最终删除了请求方法条件,只保留 HOST 条件,再在内部使用文章详情页和公开列表页两个分支。
六、EdgeOne 文章详情页分支
文章详情页继续使用此前已经验证过的正则:
^/[0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]+/$
例如:
/2026/07/27/20445/
该分支执行两项操作:
- 自定义 Cache Key → 查询字符串 → 全部忽略
- 回源请求参数设置 → 查询字符串 → 全部忽略

这里需要同时配置两项操作。
只忽略 Cache Key,可以避免不同参数生成不同缓存对象;但缓存 MISS 时,查询参数仍可能继续传到源站。
只删除回源参数,则无法保证不同参数变体一定复用同一个缓存对象。
两者组合后才能同时实现:
参数变体共用缓存
+
源站只接收干净 URL
七、EdgeOne 公开列表页分支
公开列表页使用与文章详情页平级的 ELSE IF 分支。
最终使用的正则为:
^(?:/|/page/[1-9][0-9]*/|/category/(?:[^/]+/)+(?:page/[1-9][0-9]*/)?|/(?:tag|series|series-category)/[^/]+/(?:page/[1-9][0-9]*/)?|/[0-9]{4}(?:/[0-9]{2}){0,2}/(?:page/[1-9][0-9]*/)?)$
这条正则覆盖:
- 首页和首页分页
- 单级或多级分类及其分页
- 标签及其分页
- 系列及其分页
- 系列分类及其分页
- 年、月、日归档及其分页
同时不会匹配文章详情页:
/2026/07/30/21863/
列表页分支还设置了以下条件:
查询参数 s:不存在
查询参数 preview:不存在
查询参数 preview_id:不存在
查询参数 preview_nonce:不存在
另外保留了一条特殊端点排除条件:
/(?:feed(?:/(?:rss2|rss|atom|rdf))?|embed|trackback)/?$
运算符为“正则不匹配”。
这可以避免多级分类路径中的 feed、embed 或 trackback 被当成普通分类层级。
例如下面这些地址不会进入公开列表页去参分支:
/category/program-development/feed/
/category/program-development/embed/
/category/program-development/trackback/
列表页分支同样执行:
- 自定义 Cache Key → 查询字符串 → 全部忽略
- 回源请求参数设置 → 查询字符串 → 全部忽略

保存发布后,整条规则由文章详情页分支和公开列表页分支组成。

八、EdgeOne 测试结果
发布规则后,我对 14 个中文站列表页面分别发起两次请求:
- 第一次请求干净 URL;
- 第二次请求相同 URL,但附加唯一测试参数。
例如:
https://www.shuijingwanwq.com/page/4/
https://www.shuijingwanwq.com/page/4/?swq_qs_test=随机值
首页和首页分页的结果均表现为:
干净 URL:MISS
带参数 URL:HIT
正文哈希一致
其中首页第一次冷回源的 TTFB 达到了约 14.21 秒,/page/4/ 达到了约 14.68 秒。
而带参数请求复用缓存后,TTFB 降到了约 1.2 秒。
这说明同一个列表页面一旦建立缓存,后续即使带有随机查询参数,也不需要重新执行一次接近 15 秒的 WordPress 冷回源。
分类页、分类分页、标签页和标签分页也都成功表现为:
MISS → HIT
并且正文一致。
以下页面同样被规则覆盖:
series-category- 系列首页
- 系列分页
- 年度归档
- 月度归档
- 日期归档
- 各类归档分页
月度归档出现两次 MISS
测试中,下面两个页面出现了 MISS → MISS:
/2026/07/
/2026/07/page/3/
但源站日志显示,两次回源收到的都是没有查询参数的干净 URL:
GET /2026/07/ HTTP/1.1
GET /2026/07/ HTTP/1.1
GET /2026/07/page/3/ HTTP/1.1
GET /2026/07/page/3/ HTTP/1.1
源站没有收到:
?swq_qs_test=...
两次请求分别来自不同的 EdgeOne IP,因此更可能是被调度到了不同的边缘节点,而不是 Query String 规则失效。
九、EdgeOne 搜索页面保护验证
搜索首页仍然返回:
您正搜索 autopoly - 永夜
搜索分页仍然返回:
您正搜索 autopoly - 第2页 共2页 - 永夜
搜索页面与普通首页、普通分页的正文也不同。
源站日志中,测试参数只出现在两条搜索请求里:
/?s=autopoly&swq_qs_test=...
/page/2/?s=autopoly&swq_qs_test=...
其他 14 组文章或列表页请求均没有将测试参数传到源站。
这证明:
- 普通列表页会忽略缓存 Key 中的 Query String;
- 缓存 MISS 时,普通列表页不会把 Query String 传给源站;
- 搜索请求因为存在
s参数,被正确排除; - 搜索首页和搜索分页没有受到影响。
十、Cloudflare 英文站配置
英文站 en.shuijingwanwq.com 使用 Cloudflare。
这里同样没有新建多条 URL Rewrite Rule,而是扩展原有的文章详情页 Query String 规则。
规则名称修改为:
英文文章与公开列表页 Query String 归一化
最终表达式如下:
(
http.host eq "en.shuijingwanwq.com"
and http.request.uri.query ne ""
and (
(
starts_with(http.request.uri.path, "/20")
and substring(http.request.uri.path, 5, 6) eq "/"
and substring(http.request.uri.path, 8, 9) eq "/"
and substring(http.request.uri.path, 11, 12) eq "/"
and ends_with(http.request.uri.path, "/")
and substring(http.request.uri.path, 12, -1) ne ""
and not (substring(http.request.uri.path, 12, -1) contains "/")
)
or
(
not any(http.request.uri.args.names[*] == "s")
and not any(http.request.uri.args.names[*] == "preview")
and not any(http.request.uri.args.names[*] == "preview_id")
and not any(http.request.uri.args.names[*] == "preview_nonce")
and (
http.request.uri.path eq "/"
or (
starts_with(http.request.uri.path, "/page/")
and ends_with(http.request.uri.path, "/")
and substring(http.request.uri.path, 6, -1) ne ""
and not (substring(http.request.uri.path, 6, -1) contains "/")
)
or (
(
starts_with(http.request.uri.path, "/category/")
or starts_with(http.request.uri.path, "/tag/")
or starts_with(http.request.uri.path, "/series/")
or starts_with(http.request.uri.path, "/series-category/")
)
and ends_with(http.request.uri.path, "/")
and not (
ends_with(http.request.uri.path, "/feed/")
or ends_with(http.request.uri.path, "/feed/rss2/")
or ends_with(http.request.uri.path, "/feed/rss/")
or ends_with(http.request.uri.path, "/feed/atom/")
or ends_with(http.request.uri.path, "/feed/rdf/")
or ends_with(http.request.uri.path, "/embed/")
or ends_with(http.request.uri.path, "/trackback/")
)
)
or (
starts_with(http.request.uri.path, "/20")
and substring(http.request.uri.path, 5, 6) eq "/"
and (
len(http.request.uri.path) eq 6
or (
substring(http.request.uri.path, 5, 11) eq "/page/"
and ends_with(http.request.uri.path, "/")
and substring(http.request.uri.path, 11, -1) ne ""
and not (substring(http.request.uri.path, 11, -1) contains "/")
)
or (
substring(http.request.uri.path, 8, 9) eq "/"
and (
len(http.request.uri.path) eq 9
or (
substring(http.request.uri.path, 8, 14) eq "/page/"
and ends_with(http.request.uri.path, "/")
and substring(http.request.uri.path, 14, -1) ne ""
and not (substring(http.request.uri.path, 14, -1) contains "/")
)
or (
substring(http.request.uri.path, 11, 12) eq "/"
and (
len(http.request.uri.path) eq 12
or (
substring(http.request.uri.path, 11, 17) eq "/page/"
and ends_with(http.request.uri.path, "/")
and substring(http.request.uri.path, 17, -1) ne ""
and not (substring(http.request.uri.path, 17, -1) contains "/")
)
)
)
)
)
)
)
)
)
)
)
这段表达式覆盖:
- 文章详情页
- 首页和首页分页
- 分类页及其分页
- 标签页及其分页
- 系列页及其分页
- 系列分类及其分页
- 年、月、日归档及其分页
其中下面这样的未来分页地址也会匹配:
/series-category/example/page/2/
/2026/07/30/page/2/
Cloudflare 重写动作
动作保持为:
路径:保留
查询:
重写到
Static
值留空
也就是保持原始 URL Path,只把 Query String 重写为空。
规则放置在 URL Rewrite Rules 的第一个位置。
最初我对“第一个”这个放置位置有些疑问,但后续实际测试证明,该顺序没有造成冲突,也没有被后面的规则重新添加查询参数。

十一、Cloudflare 测试结果
英文站测试覆盖了:
- 文章详情页
- 首页
- 首页分页
- 分类页
- 分类分页
- 标签页
- 系列分页
- 年度归档
- 月度归档
- 日期归档
首页、首页分页、分类页和标签页均表现为:
HIT → HIT
而且两次请求的 Age 连续增长。
例如:
首页 Age:5319 → 5329
首页分页 Age:38470 → 38474
分类页 Age:38723 → 38728
标签页 Age:38210 → 38214
这说明带随机参数的 URL 没有创建新的独立缓存对象,而是复用了现有缓存。
分类分页出现了更直接的结果:
干净 URL:MISS
带参数 URL:HIT
带参数请求 Age:4
日期归档同样表现为:
MISS → HIT
不同边缘节点导致两次 MISS
文章详情页、系列分页和年度归档曾出现:
MISS → MISS
但两次请求的 CF-Ray 分别落在 LAS 和 LAX,说明请求进入了不同的 Cloudflare 边缘节点。
源站日志最终确认,这些页面即使发生两次回源,Cloudflare 传给 Nginx 的仍然都是干净 URL:
/2026/07/13/19426/
/category/blog-operations-en/page/2/
/series/wp-blog-multilingual-guide/page/3/
/2026/
/2026/07/
/2026/07/13/
没有任何普通文章或列表请求携带测试参数。
十二、Cloudflare 搜索页面保护验证
英文搜索首页仍然返回:
You are searching for wordpress - Yongye
搜索分页仍然返回:
You are searching for wordpress - Page 2 of 23 - Yongye
搜索页面与普通首页、普通分页的内容也不同。
整个源站日志中,测试参数只出现在两条英文搜索请求里:
/?s=wordpress&swq_cf_qs_test=...
/page/2/?s=wordpress&swq_cf_qs_test=...
这证明英文站同样实现了:
- 普通文章和列表页删除 Query String;
- 搜索请求保留
s参数; - 搜索首页和搜索分页不受影响。
十三、为什么 Cloudflare 测试中的 HTML 哈希有时不同
EdgeOne 中文站测试中,干净 URL 和带参数 URL 的完整正文哈希基本一致。
Cloudflare 英文站则有不少页面出现:
same_body: no
即使两个请求都是同一节点的 HIT,完整 HTML 哈希也可能不同。
这并不能直接说明缓存归一化失败。
WordPress 页面中可能存在每次响应都会变化的字段,也可能存在由边缘节点或其他组件注入的动态内容。因此,完整 HTML 文件的 SHA-256 只能作为辅助参考。
这次判断是否成功,主要依据三项证据:
CF-Cache-Status是否从MISS变为HIT;- 同一节点的
Age是否连续增长; - Nginx 源站日志是否仍能看到测试 Query String。
最终源站日志证明,普通文章和列表页的查询参数确实已经在回源前删除。
十四、最终效果
完成调整后,中文站和英文站都实现了同一套逻辑。
中文站 EdgeOne:
文章详情页和公开列表页
→ 查询参数不参与 Cache Key
→ 缓存 MISS 时不向源站传递查询参数
→ 搜索参数 s 保留
英文站 Cloudflare:
文章详情页和公开列表页
→ Query String 重写为空
→ 不同参数变体复用规范 URL 缓存
→ 缓存 MISS 时源站只收到干净路径
→ 搜索参数 s 保留
以后再出现下面这些请求:
?amp=1
?nonamp=1
?query-5-page=...
?query-62-page=...
?random=value
只要它们落在文章详情页或受支持的公开列表页范围内,就不会再因为参数不同持续制造独立缓存对象和重复回源。
十五、这次优化的局限
Query String 归一化可以显著降低重复回源次数,但不能解决单次冷回源本身过慢的问题。
这次测试中,中文首页和 /page/4/ 的冷回源 TTFB 仍然接近 15 秒,说明 WordPress 列表页面的动态生成速度依然需要继续观察。
不过,这次调整至少解决了一个非常重要的问题:
同一个耗时页面,不会再因为几十种毫无意义的查询参数,被反复当作几十个不同页面执行。
对于只有 1 核 CPU、pm.max_children = 7 的服务器来说,减少不必要的 PHP 请求,比单纯增加 PHP-FPM 进程数量更加实际。
接下来,我会继续观察以下指标:
- EdgeOne 524 数量
- Nginx 499 数量
- PHP-FPM
max_children告警 - 首页和列表页冷回源耗时
- EdgeOne 与 Cloudflare 缓存命中率
再根据后续数据判断,是否需要继续优化 WordPress 列表页查询、W3 Total Cache、Redis 对象缓存或源站页面生成速度。
Linux 服务器运维、部署与线上故障排查
如果你的网站或后端服务部署在 Linux 服务器上,遇到访问异常、Nginx 配置问题、MySQL / Redis 异常、Docker 服务不可用、磁盘占满、CPU / 内存过高等问题,可以联系我做一次远程排查。
适合以下场景:
✅ 网站打不开或访问不稳定
✅ Nginx / PHP-FPM 配置异常
✅ MySQL / Redis 性能或连接问题
✅ Docker 服务部署与维护
✅ 服务器迁移与环境配置
✅ CPU / 内存 / 磁盘异常排查
服务内容:
✅ Linux 环境检查
✅ 网站部署与迁移
✅ Nginx / PHP-FPM / MySQL / Redis 排查
✅ Docker 配置与维护
✅ 服务器性能分析
✅ 长期远程运维支持
如需咨询,请联系我,并注明:Linux 运维咨询。
联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com


发表回复