PHP-FPM 慢请求排查实战:用 slow log 定位 Typecho 站点偶发卡顿与外部请求阻塞

拆开 Typecho 的 PHP-FPM 慢请求问题

用 Typecho 建站的站长都知道它轻量,一个页面几十毫秒就能出来。但随着文章数累积到几千篇、开了几个插件、评论表涨到几万行之后,很多人的站会开始出现一种症状:大部分时候很快,但每隔一会儿就卡一下,卡的时候要两三秒才出页面,而且往往同时有多个请求一起卡。

这种"偶发整体卡顿"比"一直很慢"更难排查,因为它不是稳定的性能瓶颈,而是资源竞争或外部依赖造成的尖峰。这篇文章不讲泛泛的 PHP 优化,而是给出一套针对 PHP-FPM + Typecho 场景的定位方法,从 FPM 的 slow log 入手,一步步找到具体是哪行代码或哪个查询在拖后腿。

第一步:把 PHP-FPM 的 slow log 打开

大部分 PHP-FPM 的安装默认关闭了慢日志,这是排查卡顿最大的障碍。没有慢日志,你只能看到"总体变慢",看不到"具体哪个脚本慢"。

在 pool 配置里(Debian/Ubuntu 通常在 /etc/php/8.x/fpm/pool.d/www.conf,CentOS 在 /etc/php-fpm.d/www.conf)打开这几项:

request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log
request_terminate_timeout = 60s
request_terminate_timeout_track_finished = yes

request_slowlog_timeout = 3s 意味着执行超过 3 秒的请求会被记录并 dump 出 PHP 的调用栈。request_terminate_timeout 是硬性终止时间,防止某个请求永远挂住占着进程不放。

改完 systemctl reload php8.x-fpm(reload 即可,不用 restart,不会中断现有请求),然后观察 slow.log。有卡顿的时候,它会给出一份非常直接的证据:

[pool www] pid 12345
script_filename = /var/www/blog/index.php
[0x00007f...] file_get_contents() /var/www/blog/usr/plugins/Xxx/Plugin.php:88
[0x00007f...] curl_exec() /var/www/blog/var/Typecho/Common.php:312
[0x00007f...] Typecho_Http_Client->send() /var/www/blog/usr/plugins/Xxx/Plugin.php:88

这份栈是从下往上读的:最下面是最外层调用,最上面是当前卡住的位置。上面这个例子一眼就能看出:某个插件在 file_get_contentscurl_exec 上阻塞了——这是"慢请求"里占比最高的一类,占我见过的案例里至少一半。

第二类高频元凶:插件里的同步外部请求

PHP 是同步阻塞的语言。一个请求如果在代码里发起 HTTP 请求去调用外部接口(比如评论审核 API、文章统计上报、图床接口、AI 摘要生成),那么整个 FPM 进程就会被占住,直到那个外部请求返回或超时

最要命的是超时时间。如果代码里 file_get_contents 没有设置超时,或者 curl 没设 CURLOPT_TIMEOUT,那么当外部接口抽风时,进程会一直等下去,默认可能是几十秒甚至无限。

症状就非常典型了:

  • 平时很快,偶尔整体卡住几秒到几十秒
  • 卡顿是"成片"的——因为 FPM 进程池里的进程被同时占满,后续请求全部排队
  • 外部接口恢复后,卡顿自动消失

解决办法有三条,按优先级:

  1. 给所有外部请求加超时。curl 加 CURLOPT_TIMEOUT => 3, CURLOPT_CONNECTTIMEOUT => 2file_get_contents 用 stream context 设 timeout。这是必须做的底线。
  2. 把非关键的外部调用改成异步或延迟执行。比如文章页里的统计上报、AI 摘要,完全可以不阻塞页面渲染,改成写队列后台跑,或者用 JS 在浏览器侧发起。
  3. 实在要同步调用的,加本地缓存。同一个数据在 5 分钟内重复请求,直接从缓存返回,能砍掉绝大部分外部调用量。

第三类:数据库慢查询与锁等待

Typecho 默认的查询都不慢,但插件和自定义主题经常写出没有走索引的查询。slow.log 里会看到类似 PDO::querymysqli_query 卡住的栈。

这时候要配合 MySQL 的慢查询日志一起看:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';

Typecho 场景下最常见的慢查询模式是:

  • 按 meta 字段(标签、分类)查询文章时,typecho_relationshipstypecho_metas 的 JOIN 没有合适索引
  • 评论表的 WHERE cid = ? AND status = ? 在大数据量下没有复合索引
  • 用了 LIKE '%关键词%' 这种前置通配的搜索,索引完全失效

另外要特别注意锁等待。如果队列应用(比如后台跑定时任务更新统计)和前台请求同时写同一张表,可能出现行锁等待。这种慢在 slow.log 里表现为 PDOStatement::execute 卡住,而单看那条 SQL 并不慢。用 SHOW ENGINE INNODB STATUS 或者 SELECT * FROM sys.innodb_lock_waits 能看到具体的等待关系。

第四类:FPM 进程池配置不合理

如果 slow.log 里什么都没抓到,但站点确实会卡,那问题可能不在代码,而在 FPM 的进程池参数。

关键参数是 pm.max_children。它决定了同时能处理多少个请求。如果设得太小(比如默认的 5),高并发时请求就排队;设得太大,内存又会被吃光,触发 OOM 或者开始用 swap,那才是真正的灾难。

合理的算法是:

pm.max_children = 可用内存 / 单个 PHP 进程平均占用

单个 PHP 进程的内存占用可以从 ps -o rss,cmd -C php-fpm 观察,Typecho 站通常在 30–60MB 之间,装了重型插件的可能到 100MB。一台 1GB 内存的 VPS,留 200MB 给系统和 MySQL,大概只能给 12–15 个 children。

同时把 pm.max_requests 设上(比如 500),让进程处理一定请求数后自动重启,避免内存泄漏长期累积。PHP 插件写得不好时内存泄漏是真实存在的,这个设置是最后一道保险。

还有一个容易被忽略的参数:pm.status_path。打开它,然后用 Nginx 暴露一个只有内网或本地能访问的状态页,curl http://127.0.0.1/fpm-status?full 能看到当前每个进程在跑什么脚本。卡顿发生的时候看一眼,能立刻知道是哪个 URL 在耗时。

第五类:磁盘 IO 与文件系统

这条在 VPS 上尤其常见。便宜的小鸡用的是网络存储或者被别人抢 IO 的邻居盘,磁盘读写在高峰期会突然变慢几十倍。

PHP 请求虽然不直接做大量 IO,但以下操作都会踩到:

  • session 文件读写(默认存在 /var/lib/php/sessions,大量请求下这个目录会成为热点)
  • OPcache 校验文件修改时间(opcache.validate_timestamps=1 时每个请求都要 stat 文件)
  • 日志写入(Nginx 和 PHP 都在写日志)
  • 模板编译缓存

iostat -x 1 观察 %utilawait。如果 %util 长期接近 100% 而 await 很大,说明磁盘是瓶颈。优化的方向是把 session 和缓存移到内存(tmpfs 或者 Redis),并且把 opcache.validate_timestamps 关掉(代价是改代码后要手动 reload FPM)。

把排查流程固化下来

总结成一份可复用的清单:

  1. 开 PHP-FPM slow log,设 request_slowlog_timeout = 3s。这是第一步,没有它后面全是猜。
  2. 卡顿发生时抓 slow.log 的调用栈,看最顶层卡在哪个函数。外部请求、数据库、文件 IO 三类占九成。
  3. 查 MySQL 慢查询 + 锁等待,给高频 WHERE 条件补复合索引。
  4. 算 max_children,按可用内存除以单进程占用,宁小勿大。
  5. iostat -x 1 看磁盘,确认不是邻居盘的锅。
  6. 开启 FPM status 页,实时看哪个脚本在耗时间。

这套方法的精髓是"用证据代替猜测"。PHP 性能问题最怕的就是凭感觉优化——把缓存全开、把参数调大、把插件删光,结果可能一点用都没有,还引入了新问题。slow log 加上一个 status 页,总共不到十分钟的配置,能让你在卡顿再次发生时直接拿到答案,这比任何经验主义都管用。

最后提一个心态上的建议:不要把"偶尔卡一下"当成必须消灭的敌人。如果一天只卡几次、每次几秒,而你的站没有商业 SLA,把时间花在写内容上通常比花在抠最后那点延迟上回报更高。运维是手段,不是目的。

Last modification:September 20th, 2026 at 08:26 pm

Leave a Comment