WordPress 跑在 MySQL 上,CPU 飙高是经典的老问题。别一上来就想着加缓存插件或者换服务器,先搞清楚“谁”在吃 CPU。
排查思路分三步:定位慢查询 -> 分析执行计划 -> 优化索引或代码。下面按实际生产环境的操作习惯,一步步拆解。
第一步:开启慢查询日志(Slow Query Log)
这是最核心的一步。MySQL 不会告诉你所有耗时的 SQL,只记录超过阈值的。你需要让 MySQL 把“罪犯”抓出来。
-
临时开启(不重启生效)
登录 MySQL,执行:SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 单位秒,建议设低一点,比如 1 或 0.5,方便捕获更多潜在问题 SET GLOBAL log_queries_not_using_indexes = ON; -- 关键!记录没走索引的查询 -
永久配置(修改 my.cnf/my.ini)
如果是阿里云 ECS、腾讯云 CVM 等云主机,通常可以通过控制台直接修改参数组,或者编辑配置文件后重启 MySQL。[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1注意:云厂商的 MySQL 实例(如 RDS)通常有专门的“慢日志”页面可以直接查看,不用去服务器找文件。
-
等待积累
让网站跑个 10-30 分钟,模拟正常流量。如果 CPU 已经很高,可能几秒就能抓到几条慢查询。
第二步:分析慢查询日志
拿到慢查询日志(slow.log)后,不要肉眼硬看。用工具解析。
-
Linux 服务器本地: 使用
mysqldumpslow命令。mysqldumpslow -s t -t 10 /var/log/mysql/slow.log-s t表示按时间排序,-t 10取前 10 条。你会看到类似这样的输出:Count: 50 Time=1.20s (60s) Lock=0.00s (0s) Rows=1000.0, user[wp_user]@host SELECT * FROM wp_posts WHERE post_status = 'publish' AND post_type = 'post' ORDER BY post_date DESC LIMIT 10;重点关注 Count(出现频率)和 Time(单次耗时)。
-
云数据库 RDS: 直接在控制台的“性能洞察”或“慢日志”页面,点击 SQL 语句后面的“分析”,它会直接给出执行计划摘要。
第三步:解读并优化 SQL
假设你抓到了一条典型的慢查询:
SELECT * FROM wp_posts WHERE meta_value = 'some_key' ORDER BY post_date DESC;
1. 检查是否走了索引
使用 EXPLAIN 关键字来分析这条 SQL:
EXPLAIN SELECT * FROM wp_posts WHERE meta_value = 'some_key' ORDER BY post_date DESC;
关注输出中的几个关键字段:
- type: 最好是
ref或const。如果是ALL,说明是全表扫描,必死无疑。 - key: 显示实际使用的索引。如果是
NULL,说明没走索引。 - rows: 预估扫描的行数。如果几万几十万,肯定慢。
- Extra: 如果有
Using filesort或Using temporary,说明 MySQL 在内存或磁盘里做了额外的排序/分组操作,非常耗 CPU。
2. 常见 WordPress 痛点及解决方案
痛点 A:meta_key / meta_value 查询
WordPress 的 wp_postmeta 表是典型的 EAV(Entity-Attribute-Value)模型,数据量极大且结构松散。
- 现象:很多主题或插件为了灵活扩展字段,直接把自定义字段存在
wp_postmeta里,然后WHERE meta_value = 'xxx'。 - 问题:
meta_value很难建立高效索引,尤其是当它存储的是文本或 JSON 时。全表扫描 + Filesort,CPU 瞬间爆炸。 - 解决:
- 代码层:避免在
WP_Query中直接使用复杂的 meta 查询。尽量使用预定义的 Post Type 或 Taxonomy(分类法)来管理内容,而不是无限堆砌 meta 字段。 - 索引层:如果必须查 meta,确保
meta_key上有索引。但要注意,复合索引(meta_key, meta_value)在 value 区分度不高时效果有限。 - 替代方案:对于高频查询的自定义字段,考虑将其拆分为独立的小表,或者使用 Redis 缓存结果。
- 代码层:避免在
痛点 B:ORDER BY 导致的全表排序
- 现象:首页加载慢,日志列表翻页慢。
- 原因:没有合适的联合索引覆盖
WHERE条件和ORDER BY字段。 - 解决:创建联合索引。例如,如果经常查
post_status='publish'并按post_date排序,创建索引:ALTER TABLE wp_posts ADD INDEX idx_status_date (post_status, post_date);这样 MySQL 可以直接从索引树中读取有序数据,无需 Filesort。
*痛点 C:未分页的 `SELECT COUNT()` 或大结果集**
- 现象:后台仪表盘加载慢,统计类功能卡死。
- 原因:某些插件在计算总文章数、评论数时,没有利用缓存,每次都去
COUNT(*)一个大表。 - 解决:
- 确保使用了支持对象缓存的插件(如 WP Super Cache, W3 Total Cache),它们会将这些统计结果缓存到文件或 Redis。
- 检查是否有插件在循环中执行 SQL(N+1 问题),比如在一个
foreach循环里每篇文章都查一次数据库。
痛点 D:垃圾回收(Garbage Collection)和定时任务
- 现象:每天固定时间 CPU 飙升。
- 原因:WordPress 的
wp-cron.php默认是页面触发式,可能导致并发请求堆积;或者自动清理修订版、草稿、附件时产生大量 DELETE 操作。 - 解决:
- 禁用内置 Cron:在
wp-config.php中添加define('DISABLE_WP_CRON', true);。 - 系统级 Cron:在服务器 crontab 中设置每分钟执行一次
wget -q -O - https://yourdomain.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1。这样可以避免 PHP 进程阻塞,让 Web 服务器专心处理用户请求。 - 限制修订版数量:在
wp-config.php中添加define('WP_POST_REVISIONS', 3);,防止历史版本无限累积。
- 禁用内置 Cron:在
第四步:基础设施层面的快速止血
如果 SQL 优化来不及,先保证服务不挂:
- 连接池与超时:检查 PHP-FPM 的
max_children和 MySQL 的max_connections。如果连接数打满,新请求会排队,表现为 CPU 高但响应极慢。适当增加连接数上限,但不要无脑加,否则上下文切换也会拖垮 CPU。 - 启用 Query Cache(谨慎):MySQL 5.7 之前有 Query Cache,5.7 之后移除,8.0 彻底移除。如果你还在用老版本,可以开启。但在高并发写入场景下,Query Cache 会导致锁竞争,反而降低性能。建议直接上应用层缓存(Redis/Memcached)。
- 读写分离:如果读多写少,考虑将读请求分流到只读副本。国内云厂商(阿里云 RDS、腾讯云 TDSQL-C 等)都提供一键读写分离,成本不高,效果立竿见影。
总结 Checklist
- 开慢日志:
long_query_time = 1,抓出 Top 10 慢 SQL。 - 看 EXPLAIN:确认是否全表扫描(
type=ALL)、是否用了 Filesort。 - 加索引:针对
WHERE和ORDER BY字段建联合索引。 - 改代码:减少
wp_postmeta的复杂查询,避免 N+1 查询。 - 上缓存:Redis 缓存热点数据和 SQL 查询结果。
- 调 Cron:改用系统 Crontab,避免 PHP 进程阻塞。
最后提醒:不要盲目追求“完美”的 SQL 优化。WordPress 本身架构决定了它在某些场景下就是重 IO 和重 DB。对于大多数中小站点,Redis 缓存 + 合理的索引 + 关闭不必要的插件,比死磕某一条 SQL 更有效。
云技术博客