XHProf 性能分析
概述
XHProf 是 Facebook 开发的 PHP 性能分析工具(Hierarchical Profiler),能够以极低的性能开销记录函数级别的调用信息,包括调用次数、执行时间、内存使用等。它是 PHP 应用性能分析和瓶颈定位的利器,特别适合在生产环境中进行采样分析。目前已由 Tideways 和 XHGUI 等项目提供现代化的持续性能监控方案。
PHP 版本要求
原始 XHProf 支持 PHP 5.x/7.x。现代替代方案 Tideways 支持 PHP 7.1+ 和 PHP 8.x。本文基于 PHP 8.1+ 编写,主要使用 Tideways/XHGUI 方案。
基础概念
什么是性能分析
性能分析(Profiling)是在程序运行时收集执行数据(函数调用时间、内存分配、调用次数等)的过程。通过分析这些数据,可以精确定位代码中的性能瓶颈。
XHProf 的核心指标
| 指标 | 说明 |
|---|---|
ct (Calls) | 函数被调用的次数 |
wt (Wall Time) | 函数执行的总挂钟时间(含等待) |
cpu (CPU Time) | 函数使用的 CPU 时间 |
mu (Memory Usage) | 函数执行时的内存使用量 |
pmu (Peak Memory Usage) | 函数执行时的内存峰值 |
采样 vs 追踪
XHProf 原始版本采用全量追踪方式,记录所有函数调用。在现代方案(如 Tideways)中,支持采样模式,按一定概率采样部分请求进行分析,将性能开销降至几乎为零。
安装与配置
Tideways 安装(推荐)
Tideways 是 XHProf 的现代替代品,提供了更好的 PHP 8.x 支持和持续性能监控能力。
bash
# 1. 添加 Tideways 仓库
sudo apt-get install -y gnupg2
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 14aa40ec0833edbb
echo "deb https://packages.tideways.com/apt stable main" | sudo tee /etc/apt/sources.list.d/tideways.list
# 2. 安装 PHP 扩展
sudo apt-get update
sudo apt-get install tideways-php
# 3. 安装 CLI 工具
sudo apt-get install tideways-daemon
# 4. 验证安装
php -m | grep tidewaysXHGUI 安装
XHGUI 是一个基于 MongoDB 的性能分析数据存储和可视化界面。
bash
# 使用 Docker 快速部署
docker run -d \
--name xhgui \
-p 8080:80 \
-e MONGODB_HOST=mongodb \
mongo:latest
# 部署 XHGUI
git clone https://github.com/perftools/xhgui.git
cd xhgui
composer install
# 配置 MongoDB 连接
cp config/config.default.php config/config.phpXHProf 扩展安装(旧版)
bash
# PECL 安装
pecl install xhprof
# 配置 php.ini
echo "extension=xhprof.so" >> /etc/php/8.1/mods-available/xhprof.ini
phpenmod xhprof
# 验证
php -m | grep xhprof详细说明
Tideways 使用
基本集成
php
<?php
declare(strict_types=1);
// 在入口文件中启用 Tideways
if (extension_loaded('tideways')) {
tideways_enable(TIDEWAYS_FLAGS_CPU | TIDEWAYS_FLAGS_MEMORY | TIDEWAYS_FLAGS_NO_SPANS);
}
// ... 应用代码 ...
// 在请求结束时收集数据
if (extension_loaded('tideways')) {
$data = tideways_disable();
file_put_contents(
'/tmp/xhprof/' . uniqid() . '.xhprof',
serialize($data)
);
}按概率采样
php
<?php
declare(strict_types=1);
/**
* Tideways 采样管理器
*/
class TidewaysSampler
{
private const SAMPLE_RATE = 0.01; // 1% 的采样率
public static function shouldProfile(): bool
{
// 1% 概率采样
return mt_rand(1, 100) <= (self::SAMPLE_RATE * 100);
}
public static function startIfShould(): void
{
if (!extension_loaded('tideways')) {
return;
}
if (!self::shouldProfile()) {
return;
}
tideways_enable(
TIDEWAYS_FLAGS_CPU | TIDEWAYS_FLAGS_MEMORY | TIDEWAYS_FLAGS_NO_SPANS
);
}
public static function finishAndStore(): ?array
{
if (!extension_loaded('tideways')) {
return null;
}
$data = tideways_disable();
if ($data === false) {
return null;
}
// 存储到 MongoDB(XHGUI 方案)
self::storeToMongo($data);
return $data;
}
private static function storeToMongo(array $data): void
{
$mongo = new MongoDB\Client('mongodb://localhost:27017');
$collection = $mongo->xhprof->results;
$document = [
'profile' => $data,
'meta' => [
'url' => $_SERVER['REQUEST_URI'] ?? 'cli',
'method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
'timestamp' => new MongoDB\BSON\UTCDateTime(),
'cpu_count' => 4,
],
];
$collection->insertOne($document);
}
}XHProf 原始使用
基本分析
php
<?php
declare(strict_types=1);
/**
* XHProf 性能分析封装
*/
class XhprofAnalyzer
{
/**
* 开始分析
*/
public static function start(): void
{
if (!extension_loaded('xhprof')) {
throw new RuntimeException('XHProf 扩展未加载');
}
// 忽略内置函数以减少噪声
xhprof_enable(
XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS
);
}
/**
* 结束分析并返回结果
*/
public static function finish(): array
{
$data = xhprof_disable();
return self::analyze($data);
}
/**
* 分析结果:获取耗时最多的函数
*/
public static function analyze(array $data): array
{
// 移除 main() 顶层调用
$main = $data['main()'] ?? [];
unset($data['main()']);
// 按挂钟时间排序
uasort($data, function (array $a, array $b): int {
return ($b['wt'] ?? 0) <=> ($a['wt'] ?? 0);
});
return [
'main' => $main,
'functions' => array_slice($data, 0, 20),
'total_time_ms' => round(($main['wt'] ?? 0) / 1000, 2),
'total_cpu_ms' => round(($main['cpu'] ?? 0) / 1000, 2),
'peak_memory' => ($main['pmu'] ?? 0) / 1024,
];
}
/**
* 生成文本报告
*/
public static function textReport(array $data): string
{
$lines = [];
$lines[] = sprintf(
"总执行时间: %.2fms | CPU: %.2fms | 内存峰值: %.2fKB",
$data['total_time_ms'],
$data['total_cpu_ms'],
$data['peak_memory']
);
$lines[] = str_repeat('-', 80);
$lines[] = str_pad('函数', 50) . str_pad('调用次数', 10) . str_pad('时间(ms)', 12) . str_pad('自耗时(ms)', 12);
$lines[] = str_repeat('-', 80);
foreach ($data['functions'] as $name => $stats) {
$lines[] = str_pad($name, 50)
. str_pad((string) ($stats['ct'] ?? 0), 10)
. str_pad(round(($stats['wt'] ?? 0) / 1000, 2) . '', 12)
. str_pad(round(($stats['ewt'] ?? 0) / 1000, 2) . '', 12);
}
return implode("\n", $lines);
}
}
// 使用示例
XhprofAnalyzer::start();
// ... 被分析的代码 ...
function heavyCalculation(int $n): int
{
$sum = 0;
for ($i = 0; $i < $n; $i++) {
for ($j = 0; $j < $n; $j++) {
$sum += $i * $j;
}
}
return $sum;
}
heavyCalculation(500);
// 获取分析结果
$result = XhprofAnalyzer::finish();
echo XhprofAnalyzer::textReport($result);调用图分析
php
<?php
declare(strict_types=1);
/**
* XHProf 调用图分析器
* 识别函数调用关系中的性能瓶颈
*/
class XhprofCallGraphAnalyzer
{
/**
* 查找最耗时的调用链
*/
public static function findSlowestPaths(array $data, int $limit = 10): array
{
$paths = [];
foreach ($data as $func => $stats) {
// 仅分析叶子函数(不包含子调用的独占耗时)
$exclusiveTime = ($stats['wt'] ?? 0) - ($stats['cw'] ?? 0);
if ($exclusiveTime > 0) {
$paths[] = [
'function' => $func,
'total_time_ms' => round(($stats['wt'] ?? 0) / 1000, 2),
'exclusive_time_ms' => round($exclusiveTime / 1000, 2),
'calls' => $stats['ct'] ?? 0,
'memory' => round(($stats['pmu'] ?? 0) / 1024, 2),
];
}
}
// 按独占时间排序
usort($paths, fn(array $a, array $b) => $b['exclusive_time_ms'] <=> $a['exclusive_time_ms']);
return array_slice($paths, 0, $limit);
}
/**
* 计算函数的独占时间占比
*/
public static function exclusiveTimeRatio(array $data): array
{
$mainTime = $data['main()']['wt'] ?? 1;
$results = [];
foreach ($data as $func => $stats) {
if ($func === 'main()') {
continue;
}
$exclusiveTime = ($stats['wt'] ?? 0) - ($stats['cw'] ?? 0);
$results[$func] = [
'exclusive_ms' => round($exclusiveTime / 1000, 2),
'ratio' => round(($exclusiveTime / $mainTime) * 100, 2) . '%',
];
}
// 按占比排序
arsort($results);
return $results;
}
}XHGUI 集成
自动分析中间件
php
<?php
declare(strict_types=1);
/**
* XHProf/XHGUI 自动分析中间件
* 可集成到 Laravel/Symfony 等框架中
*/
class XhguiMiddleware
{
public function __construct(
private float $sampleRate = 0.01,
private array $ignorePaths = ['/health', '/metrics'],
) {
}
/**
* 请求开始时调用
*/
public function onRequest(): void
{
if (!$this->shouldProfile()) {
return;
}
$flags = XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY;
xhprof_enable($flags);
}
/**
* 请求结束时调用
*/
public function onResponse(): void
{
$data = xhprof_disable();
if ($data === null) {
return;
}
$this->save($data);
}
private function shouldProfile(): bool
{
// 检查扩展
if (!extension_loaded('xhprof')) {
return false;
}
// 检查路径
$uri = $_SERVER['REQUEST_URI'] ?? '';
foreach ($this->ignorePaths as $path) {
if (str_starts_with($uri, $path)) {
return false;
}
}
// 按概率采样
return (mt_rand() / mt_getrandmax()) < $this->sampleRate;
}
private function save(array $data): void
{
try {
$mongo = new MongoDB\Client('mongodb://localhost:27017');
$collection = $mongo->xhprof->results;
$collection->insertOne([
'profile' => $data,
'meta' => [
'url' => $_SERVER['REQUEST_URI'] ?? '',
'simple_url' => parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH),
'request_ts' => new MongoDB\BSON\UTCDateTime((int)(microtime(true) * 1000)),
'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
],
]);
} catch (\Throwable $e) {
error_log("XHProf 保存失败: " . $e->getMessage());
}
}
}实战示例
Laravel 集成
php
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
/**
* Laravel 服务提供者 - XHProf 集成
*/
class XhprofServiceProvider extends ServiceProvider
{
public function boot(): void
{
if ($this->app->environment('local') && extension_loaded('xhprof')) {
$sampleRate = (float) config('xhprof.sample_rate', 1.0);
if ((mt_rand() / mt_getrandmax()) <= $sampleRate) {
xhprof_enable(
XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS
);
}
}
}
public function register(): void
{
$this->app->terminating(function () {
if (!extension_loaded('xhprof')) {
return;
}
$data = xhprof_disable();
if ($data === null) {
return;
}
// 存储分析数据
$this->storeProfile($data);
});
}
private function storeProfile(array $data): void
{
$path = storage_path('xhprof/' . date('Y-m-d'));
if (!is_dir($path)) {
mkdir($path, 0755, true);
}
$filename = date('H_i_s') . '_' . uniqid() . '.json';
file_put_contents(
$path . '/' . $filename,
json_encode($data, JSON_PRETTY_PRINT)
);
}
}CLI 命令行分析
php
<?php
declare(strict_types=1);
/**
* CLI 场景下的 XHProf 分析命令
*/
class ProfileCommand
{
public function run(string $targetScript): void
{
if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
echo "错误:需要 xhprof 或 tideways 扩展" . PHP_EOL;
exit(1);
}
echo "分析目标: {$targetScript}" . PHP_EOL;
echo "开始分析..." . PHP_EOL;
$startMemory = memory_get_usage(true);
// 启用分析
if (extension_loaded('tideways')) {
tideways_enable(TIDEWAYS_FLAGS_CPU | TIDEWAYS_FLAGS_MEMORY);
} else {
xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY);
}
// 执行目标脚本
require $targetScript;
// 停止分析
$data = extension_loaded('tideways')
? tideways_disable()
: xhprof_disable();
$endMemory = memory_get_usage(true);
// 输出报告
echo PHP_EOL . "=== 分析结果 ===" . PHP_EOL;
echo "内存使用: " . round(($endMemory - $startMemory) / 1024 / 1024, 2) . "MB" . PHP_EOL;
echo PHP_EOL;
if (is_array($data)) {
$this->printTopFunctions($data, 15);
}
}
private function printTopFunctions(array $data, int $limit): void
{
$flat = [];
foreach ($data as $key => $stats) {
// 提取函数名
$parts = explode('==>', $key);
$func = trim($parts[0]);
if (!isset($flat[$func])) {
$flat[$func] = [
'wt' => 0,
'cpu' => 0,
'mu' => 0,
'ct' => 0,
];
}
$flat[$func]['wt'] += $stats['wt'] ?? 0;
$flat[$func]['cpu'] += $stats['cpu'] ?? 0;
$flat[$func]['mu'] += $stats['mu'] ?? 0;
$flat[$func]['ct'] += $stats['ct'] ?? 0;
}
// 排序
uasort($flat, fn($a, $b) => $b['wt'] <=> $a['wt']);
echo str_pad('函数', 45) . str_pad('调用', 8) . str_pad('耗时(ms)', 12) . str_pad('CPU(ms)', 12) . str_pad('内存(KB)', 12) . PHP_EOL;
echo str_repeat('-', 89) . PHP_EOL;
$i = 0;
foreach ($flat as $func => $stats) {
if ($i >= $limit) break;
echo str_pad($func, 45)
. str_pad((string) $stats['ct'], 8)
. str_pad(round($stats['wt'] / 1000, 2) . '', 12)
. str_pad(round($stats['cpu'] / 1000, 2) . '', 12)
. str_pad(round($stats['mu'] / 1024, 2) . '', 12)
. PHP_EOL;
$i++;
}
}
}性能基线对比
php
<?php
declare(strict_types=1);
/**
* 性能基线对比工具
* 对比两次运行的结果找出性能回归
*/
class PerformanceBaseline
{
/**
* 运行并收集分析数据
*/
public static function profile(callable $fn, int $warmup = 3, int $runs = 5): array
{
xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY);
// 预热
for ($i = 0; $i < $warmup; $i++) {
$fn();
}
// 正式运行
$times = [];
for ($i = 0; $i < $runs; $i++) {
$start = hrtime(true);
$fn();
$times[] = (hrtime(true) - $start) / 1_000_000;
}
$profile = xhprof_disable();
sort($times);
return [
'profile' => $profile,
'times' => $times,
'median_ms' => $times[(int)(count($times) / 2)],
'avg_ms' => array_sum($times) / count($times),
'min_ms' => $times[0],
'max_ms' => $times[count($times) - 1],
];
}
/**
* 对比两次分析结果
*/
public static function compare(array $baseline, array $current): array
{
$regressions = [];
foreach ($current['profile'] as $func => $stats) {
if (isset($baseline['profile'][$func])) {
$baseWt = $baseline['profile'][$func]['wt'] ?? 0;
$currWt = $stats['wt'] ?? 0;
if ($baseWt > 0) {
$change = (($currWt - $baseWt) / $baseWt) * 100;
if ($change > 20) {
$regressions[] = [
'function' => $func,
'baseline_ms' => round($baseWt / 1000, 2),
'current_ms' => round($currWt / 1000, 2),
'change' => round($change, 1) . '%',
];
}
}
}
}
return [
'baseline_time' => $baseline['median_ms'],
'current_time' => $current['median_ms'],
'time_change' => round(
(($current['median_ms'] - $baseline['median_ms']) / $baseline['median_ms']) * 100, 2
) . '%',
'regressions' => $regressions,
];
}
}注意事项
性能开销
| 工具 | 开销 | 适用场景 |
|---|---|---|
| XHProf(全量) | 5-15% | 开发环境、压力测试 |
| Tideways(采样) | < 0.1% | 生产环境持续监控 |
| Tideways(全量) | 2-5% | 临时调试 |
生产环境使用
在生产环境中,务必使用采样模式。全量分析会导致显著的性能下降和大量数据存储需求。
数据管理
- 设置合理的分析数据保留期限(建议 7-30 天)
- 定期清理旧数据
- 监控存储空间使用
- 对分析数据建立索引以加速查询
最佳实践
1. 建立性能基线
在每次发布前记录性能基线,与上线后的数据进行对比,快速发现性能回归。
2. 设置告警阈值
- 单个函数耗时超过总耗时的 10% 应告警
- 接口响应时间超过基线 20% 应告警
- 内存使用超过基线 50% 应告警
3. 结合 APM 工具
XHProf/XHGUI 适合详细分析,但日常监控建议结合 APM 工具(如 New Relic、Datadog、Scout APM)使用。
下一节
继续学习:Xdebug 性能分析