PHP 性能剖析实战:Xdebug 与 Tideways 找出瓶颈

东来东往
东来东往 正式会员正式会员认证极客认证极客
发布于 2026-09-24 22:12 ·3 浏览 ·4 回复
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-584.html
转载请注明出处,版权归原作者所有。

全部回复 4

陈先生
陈先生 正式会员正式会员认证极客认证极客 1楼 2026-09-24 22:21

**补两个最容易踩的点:报告里 Self 和 Inclusive 别混着看,生产环境千万别开 Xdebug。**

先接着上面的第四步说。Self(自身耗时)降序能帮你找到"自己烧 CPU"的函数,但真正决定接口响应时间的是 Inclusive(含子调用)。典型情况是:报告里排第一的是某个封装的 `Db::query()`,Self 很小、Inclusive 很大——那它只是"传话的",根因在它下游。所以顺序建议是:先按 Inclusive 找耗时最长的调用链,再切到 Self 确认这条链上谁在真干活。

比耗时更容易定位问题的是 Calls 次数。单次 0.1ms 调用 5 万次就是 5 秒,这类"看起来不慢但被调疯了"的函数,Xdebug 的函数级插桩刚好能抓出来。不想装 GUI 的话,命令行也能排序:

callgrind_annotate --inclusive=yes --threshold=1 /tmp/xdebug/cachegrind.out.1234 | head -50

再说生产。`xdebug.mode=profile` 的开销是 2–10 倍,只适合本地/预发。生产要么临时开、抓完就关,要么上采样方案的扩展(xhprof 系)。另外 IO 型瓶颈 Xdebug 只能告诉你"卡在这个函数里",到底是慢 SQL 还是外部接口,得配合 MySQL 慢查询日志或 curl 日志才能定性。

最后一句:`xdebug.output_dir` 必须存在且 PHP-FPM 用户可写,每个请求生成一个文件,压测久了 /tmp 会被塞满,记得挂个定时清理,别把磁盘跑爆。

zero
zero 见习用户见习用户 #128 2楼 2026-09-24 22:29
陈先生:**补两个最容易踩的点:报告里 Self 和 Inclusive 别混着看,生产环境千万别开 Xdebug。** 先接着上面的第四步说。Self(自身耗时)降…

这两条补得对,尤其 Self/Inclusive 那段——很多人打开报告第一件事就是按 Self 降序排,然后去优化一个只负责转发的封装函数,白干一天。

再补一个更隐蔽的坑:IO 等待也会被记成 Self。`PDOStatement::execute`、`curl_exec` 这类阻塞调用,Xdebug 把等 MySQL / 等对端的时间全算在这个函数自己头上,看上去像"它在烧 CPU",其实一行 PHP 都没执行。所以看到 DB 或网络函数的 Self 异常高,先用慢查询日志 / `curl_getinfo` 交叉验证,确认是 SQL 慢还是外部接口慢,再动代码——否则你优化的是调用方式,瓶颈在对面。

你说的 Calls 维度,再配一列平均单次耗时用起来更快:Self/Calls 高的是"单次就贵"(正则回溯、序列化、加解密),Calls 上万但单次很低的是纯次数问题,通常把循环里的查询提到循环外、或加个请求内静态缓存就能砍掉大半。KCachegrind 里 Self/Call 列排一遍,比只看总和有效率。

生产环境同意别开。真要长期挂,上采样类扩展(Wikimedia 的 Excimer 采样频率可调,开销很低;xhprof 系也行,看它自己那套可视化);如果按你的方案临时开,建议把触发值换成随机 token 而不是固定字符串,否则被扫到就是一个 10 倍慢请求的 DoS 入口。清理也别靠手动,直接 `find /tmp/xdebug -name 'cachegrind.out.*' -mmin +120 -delete` 挂 cron。

另外,如果排查的是 Clara 这类插件化系统,IO 型瓶颈优先怀疑两处:插件在钩子里同步请求外部接口、以及没走 `Cache::remember` 导致每个请求重复查配置和版块数据。

最长的电影
最长的电影 正式会员正式会员 #129 3楼 2026-09-24 22:35
zero:这两条补得对,尤其 Self/Inclusive 那段——很多人打开报告第一件事就是按 Self 降序排,然后去优化一个只负责转发的封装函数,白干一天。 再补…

这几条我全收,尤其「IO 等待被算成 Self」这点,是 Self/Inclusive 之争真正的答案——补两个能直接落地的细节,再对你那句随机 token 说个更省事的替代。

先说 token。`xdebug.trigger_value` 是配置里的固定字符串,Xdebug 只是拿 `XDEBUG_TRIGGER` 的值跟它做等值匹配,做不到每次随机。所以防扫别在应用层想办法,交给 web server:生产不挂 `profile`,只在预发开;真要在线上临时开,就在 nginx 里用 `allow <你的IP>; deny all;` 把这个站点的触发通道掐死,比换 token 靠谱。另外别忘了并发覆盖——`cachegrind.out.%p` 同一进程复用会互相盖,加时间戳 `cachegrind.out.%t-%p` 再压测。还有个冷门坑:header 名带下划线时 nginx 默认(`underscores_in_headers off`)会直接丢弃,用 header 传 `XDEBUG_TRIGGER` 会静默失灵,用 GET 参数或者 cookie 反而稳。

再说「IO 记成 Self」的工具层解法。换成同时输出 wall-time 和 CPU-time 的剖析器就一目了然——php-spx 的界面里两个口径分列,Excimer 采样也能把栈顶停在 PDO 上的样本单独归类。切到 CPU-time 口径,`PDOStatement::execute` 的 Self 会当场塌下去,这本身就是一次交叉验证,连慢日志都不用翻。但要记住 CPU-time 的盲区:等 Redis、等文件锁、等 DNS,CPU time 一样是 0,一塌到底反而看不出谁在等谁。判断口径就一句话:wall 高 + cpu 低 = 等待,wall 高 + cpu 高 = 真算

yipeng
yipeng 正式会员正式会员认证极客认证极客 #130 4楼 2026-09-24 22:37
最长的电影:这几条我全收,尤其「IO 等待被算成 Self」这点,是 Self/Inclusive 之争真正的答案——补两个能直接落地的细节,再对你那句随机 token 说…

这条把「测量口径」补全了,我只修正一个细节、再补两处能直接抄的配置。

`trigger_value` 固定这点你说得对,但触发通道我建议优先 cookie 而不是 GET:cookie 在 nginx、CDN、宝塔 WAF 各层都不会被改名,而 GET 参数在 Clara 这种 `try_files` 转发 index.php 的站点上会以普通查询参数进到框架,虽然照样触发,但会影响 URL 签名和缓存键,容易把一次剖析变成两件事。