努力
加油

如何排查WordPress中MySQL占用CPU过高的问题?

WordPress 跑在 MySQL 上,CPU 飙高是经典的老问题。别一上来就想着加缓存插件或者换服务器,先搞清楚“谁”在吃 CPU。

排查思路分三步:定位慢查询 -> 分析执行计划 -> 优化索引或代码。下面按实际生产环境的操作习惯,一步步拆解。

第一步:开启慢查询日志(Slow Query Log)

这是最核心的一步。MySQL 不会告诉你所有耗时的 SQL,只记录超过阈值的。你需要让 MySQL 把“罪犯”抓出来。

  1. 临时开启(不重启生效)
    登录 MySQL,执行:

    SET GLOBAL slow_query_log = 'ON';
    SET GLOBAL long_query_time = 2; -- 单位秒,建议设低一点,比如 1 或 0.5,方便捕获更多潜在问题
    SET GLOBAL log_queries_not_using_indexes = ON; -- 关键!记录没走索引的查询
  2. 永久配置(修改 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)通常有专门的“慢日志”页面可以直接查看,不用去服务器找文件。

  3. 等待积累
    让网站跑个 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);,防止历史版本无限累积。

第四步:基础设施层面的快速止血

如果 SQL 优化来不及,先保证服务不挂:

  1. 连接池与超时:检查 PHP-FPM 的 max_children 和 MySQL 的 max_connections。如果连接数打满,新请求会排队,表现为 CPU 高但响应极慢。适当增加连接数上限,但不要无脑加,否则上下文切换也会拖垮 CPU。
  2. 启用 Query Cache(谨慎):MySQL 5.7 之前有 Query Cache,5.7 之后移除,8.0 彻底移除。如果你还在用老版本,可以开启。但在高并发写入场景下,Query Cache 会导致锁竞争,反而降低性能。建议直接上应用层缓存(Redis/Memcached)。
  3. 读写分离:如果读多写少,考虑将读请求分流到只读副本。国内云厂商(阿里云 RDS、腾讯云 TDSQL-C 等)都提供一键读写分离,成本不高,效果立竿见影。

总结 Checklist

  1. 开慢日志:long_query_time = 1,抓出 Top 10 慢 SQL。
  2. 看 EXPLAIN:确认是否全表扫描(type=ALL)、是否用了 Filesort。
  3. 加索引:针对 WHERE 和 ORDER BY 字段建联合索引。
  4. 改代码:减少 wp_postmeta 的复杂查询,避免 N+1 查询。
  5. 上缓存:Redis 缓存热点数据和 SQL 查询结果。
  6. 调 Cron:改用系统 Crontab,避免 PHP 进程阻塞。

最后提醒:不要盲目追求“完美”的 SQL 优化。WordPress 本身架构决定了它在某些场景下就是重 IO 和重 DB。对于大多数中小站点,Redis 缓存 + 合理的索引 + 关闭不必要的插件,比死磕某一条 SQL 更有效。