Skip to content

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 tideways

XHGUI 安装

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.php

XHProf 扩展安装(旧版)

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 性能分析

参考链接