学完这篇,你能在一台 PHP 机器上跑起函数级性能剖析,从报告里读出到底是哪几个函数在拖慢请求,并且在生产环境用低开销方案持续监控。
PHP 的性能问题很少是"PHP 本身慢",绝大多数是某个函数被调了几万次、某个查询在循环里跑了 500 遍、某个插件在钩子里同步请求了外部接口。下面按实战顺序走一遍。
第一步:先分清你要抓哪类瓶颈
动手前先把目标定死,否则报告出来只会看到一堆数字:
- CPU 型:接口耗时高、CPU 打满,通常是正则回溯、序列化、模板渲染、加密计算。
- IO 型:CPU 不高但响应慢,通常是数据库 N+1、循环里 file_get_contents、curl 同步等待。
- 内存型:跑到一半 500,通常是大数组、一次性拉全表、递归失控。
Xdebug 走的是函数级插桩,每一次函数调用都记录,适合精确定位"哪个函数被调了多少次";Tideways 走的是低开销采样/常驻监控,适合在生产上长期跑,看整体趋势。两者定位不同,不是二选一。
第二步:装 Xdebug,只开 profile 模式
Xdebug 3 的开关全在 `xdebug.mode`,默认 `develop`,剖析必须显式打开。宝塔面板可在「软件商店→PHP 设置→安装扩展」装,命令行环境用:
pecl install xdebug
php -m | grep xdebug # 确认加载成功
然后在 php.ini 里加:
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
xdebug.profiler_output_name=cachegrind.out.%p
xdebug.start_with_request=trigger
xdebug.trigger_value=PROFILE
改完重启 PHP-FPM(或 Apache)。
注意:`xdebug.mode` 里每多开一项(debug、coverage、trace)开销就成倍上升。做性能剖析时只留 `profile`,其他一律关掉,否则你测出来的是 Xdebug 的开销,不是业务的。
第三步:用可比对的方式跑一次请求
不要在浏览器里随便点点,那样两次样本没法比。正确做法:
curl -s "https://你的域名/forum.php?XDEBUG_TRIGGER=PROFILE" -o /dev/null
或者用 `wrk`、`ab` 固定并发跑同一路径,只为触发一次剖析:
ab -n 1 -c 1 "https://你的域名/目标页面?XDEBUG_TRIGGER=PROFILE"
`start_with_request=trigger` 的好处是:只有带上触发参数的那一次请求才会生成文件,其他请求零干扰。
注意:第一次请求必然慢(要编译模板、预热 OPcache、建连接池)。别拿第一次的数据下结论,至少先跑一遍"热身请求",再跑要分析的那一次。
第四步:打开报告,按"自耗时"排序
生成的 `cachegrind.out.xxxx` 用 KCachegrind(Linux)、QCachegrind(Mac/Win)或 Webgrind(浏览器版)打开。
看报告的顺序固定为三步,不要乱看:
- 先看 Self(自身耗时)降序——这是函数自己花掉的时间,不含子调用。瓶颈一定在这里。
- 再看 Call Count(调用次数)——次数离谱的,通常就是模型层的循环调用。
- 最后看 Inclusive(含子调用耗时)——用来理清调用链,定位"是谁调用了这个慢函数"。
第五步:三类高发信号,直接对号入座
- 某个 `PDO::query` / `PDOStatement::execute` 调用次数上百 → N+1 查询,改成一次 IN 查询或加缓存层。
- `curl_exec`、`file_get_contents` 自耗时很高 → 同步外部请求阻塞了响应,改成异步或加本地缓存。
- `call_user_func_array`、`preg_match` 次数异常 → 前者常见于运行时钩子分发,后者常见于路由匹配和敏感词过滤。
以 Clara BBS 这类无框架 PHP 论坛为例:它有 156 个运行时插件钩子、保存即生效无需清缓存、也没有编译缓存这一层。这意味着每个请求都会完整地走一遍钩子分发和模板解析——如果某个插件在钩子里同步请求外部接口,报告里就会看到 `curl_exec` 占据高自耗时,而且每次访问都慢,因为没有任何编译缓存帮你挡一层。对应地,系统内置的 `Cache::remember` 就是给这类重复计算兜底的,剖析时如果发现某段逻辑没走缓存,优先补上。
注意:看不到缓存命中率、只看耗时,很容易误判。测之前先清一次缓存,再测一次"热缓存"状态,两次对比才知道缓存到底起没起作用。
第六步:生产环境换 Tideways,别硬扛 Xdebug
Xdebug 的开销在生产上是不可接受的。生产上要长期看数据,用 Tideways 系:
- 开源方案 `tideways_xhprof`,在代码里手动开关:
tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU | TIDEWAYS_XHPROF_FLAGS_MEMORY);
// ... 业务代码 ...
$data = tideways_xhprof_disable();
- 商业版是 tideways 扩展 + tideways-daemon,由 daemon 控制采样率并上报,浏览器端直接看火焰图和调用树。采样剖析基于定时中断,开销通常在个位数百分比,可以常态化开启。
安装同样是 `pecl install tideways_xhprof`(宝塔可在 PHP 扩展页勾选),然后重启 PHP-FPM。
注意:不要在同一次请求里同时启用 Xdebug 和 Tideways,双重插桩会互相干扰,数据失真且拖慢几倍。要对比就分开跑两轮。
第七步:改一处,测一次,别攒着一起改
找到嫌疑人之后,用最小改动验证:
- 只改一处,重跑同样的命令,对比 Self 耗时和总耗时变化。
- 优化后用 wrk 跑 30 秒看 P95,而不是看单次请求。单次请求的抖动足以骗过你。
- 把优化前后的 `cachegrind` 文件都留一份,回归时直接翻。
注意:CLI 和 PHP-FPM 的 php.ini 往往是两份(`php --ini` 与 FPM 各自一份)。你在命令行装了扩展,网页请求未必生效——两边都要确认。
小结
- 先分类型(CPU/IO/内存),再决定用哪种工具,别上来就装扩展。
- Xdebug 3 剖析只开 `xdebug.mode=profile`,用 `start_with_request=trigger` 精准触发,生产禁用。
- 读报告固定顺序:Self 耗时 → 调用次数 → Inclusive 调用链。
- `PDO` 高频调用找 N+1,`curl_exec` 高自耗时找同步外部请求,`call_user_func_array` 找钩子分发。
- 生产用 Tideways(`tideways_xhprof` 或 daemon 采样),常驻低开销监控。
- 一次只改一处,用 P95 而非单次请求做验证,注意 CLI 与 FPM 配置分离。