sj-i/php-profiler

此包已被弃用,不再维护。作者建议使用 reliforp/reli-prof 包。

PHP的采样分析器,用PHP编写,从进程外部读取运行中的PHP虚拟机信息。

安装: 64

依赖: 0

建议者: 0

安全性: 0

星标: 210

关注者: 7

分支: 5

类型:项目


README

Minimum PHP version: 8.0.0 Packagist Github Actions Scrutinizer Code Quality Coverage Status Psalm coverage

Reli是一个用PHP编写的采样分析器(或虚拟机状态检查器),可以从进程外部读取运行中的PHP脚本信息。它是一个独立的CLI工具,因此目标程序无需任何修改。此工具之前的名称是sj-i/php-profiler(我们目前正在更改名称)。

我能用它做什么?

  • 检测和可视化PHP脚本中的瓶颈
    • 它不仅提供函数级别的分析,还提供行级别或指令级别解析
  • 即使在调用大量快速函数时,也能进行无累积开销的分析(这是一个采样分析器,见下面的链接,tideways,xhprof,以及xdebug的分析器,许多分析器都有这个开销)
  • 调查错误或性能失败的原因
    • 即使PHP脚本处于无法解释的无响应状态,您也可以使用它来找出它内部正在做什么。

它如何工作

它通过以下技术实现

  • 解析解释器的ELF二进制文件
  • 从/proc//maps读取内存映射
  • 使用ptrace(2)和process_vm_readv(2)通过FFI读取外部进程的内存
  • 分析PHP虚拟机(又称Zend引擎)中的内部数据结构

如果您有额外的CPU资源,此软件的开销将是可以忽略不计的。

与phpspy的区别,何时使用reli

Reli深受adsr/phpspy的启发。

这两个之间主要的区别是reli几乎完全用PHP编写,而phpspy是用C编写的。在分析时,有时您可能希望自定义获取信息和方式。如果对于PHP开发者的可定制性很重要,您可以使用此软件,虽然这会牺牲一些性能(尽管我们希望代价不是很大)。

此外,reli可以从ZTS解释器中找到虚拟机状态。例如,在守护进程模式下,通过ext-parallel启动的线程的跟踪将自动检索。目前这只能用phpspy完成。Reli还提供只获取目标EG地址的功能,因此如果您想使用phpspy进行实际分析,即使目标为ZTS也可以。

reli的其他功能,phpspy目前还没有包括

  • 输出更准确的行号
  • 使用PHP模板自定义输出格式
  • 获取PHP-VM的运行指令
  • 自动从剥离的PHP二进制文件中检索目标PHP版本
  • 以speedscope格式输出跟踪

没有特别的原因说明为什么这些功能不能在 phpspy 端实现,所以未来可能在 phpspy 上实现这些功能。

另一方面,有一些功能 phpspy 可以做,但 reli 目前还不能做。

  • 重定向子进程的输出
  • 强制设置 EG 的地址
  • 从 sapi_globals 中检索数据
  • callgrind 支持
  • 读取变量
  • 运行更快,开销更低。
  • 等等。

未来,许多可以用 phpspy 实现的功能将使用 reli 实现。

需求

支持的 PHP 版本

执行

  • PHP 8.0+ (NTS / ZTS)
  • 64位 Linux x86_64
  • 必须启用 FFI 扩展。
  • 如果目标进程是 ZTS,则必须启用 PCNTL 扩展。

目标

  • PHP 7.0+ (NTS / ZTS)
  • 64位 Linux x86_64

在针对 ZTS 时,目标进程必须加载 libpthread.so,并且您还必须有解释器和 libpthread.so 的未剥离二进制文件,以便从 TLS 中查找 EG。

安装

从 Composer 安装

composer create-project reliforp/reli-prof
cd reli
./reli

从 Git 安装

git clone git@github.com:reliforp/reli-prof.git
cd reli
composer install
./reli

使用

获取调用跟踪

./reli inspector:trace --help
Description:
  periodically get call trace from an outer process or thread

Usage:
  inspector:trace [options] [--] [<cmd> [<args>...]]

Arguments:
  cmd                                        command to execute as a target: either pid (via -p/--pid) or cmd must be specified
  args                                       command line arguments for cmd

Options:
  -p, --pid=PID                              process id
  -d, --depth[=DEPTH]                        max depth
  -s, --sleep-ns[=SLEEP-NS]                  nanoseconds between traces (default: 1000 * 1000 * 10)
  -r, --max-retries[=MAX-RETRIES]            max retries on contiguous errors of read (default: 10)
  -S, --stop-process[=STOP-PROCESS]          stop the target process while reading its trace (default: off)
      --php-regex[=PHP-REGEX]                regex to find the php binary loaded in the target process
      --libpthread-regex[=LIBPTHREAD-REGEX]  regex to find the libpthread.so loaded in the target process
      --php-version[=PHP-VERSION]            php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
      --php-path[=PHP-PATH]                  path to the php binary (only needed in tracing chrooted ZTS target)
      --libpthread-path[=LIBPTHREAD-PATH]    path to the libpthread.so (only needed in tracing chrooted ZTS target)
  -t, --template[=TEMPLATE]                  template name (phpspy|phpspy_with_opcode|json_lines) (default: phpspy)
  -o, --output=OUTPUT                        path to write output from this tool (default: stdout)
  -h, --help                                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                                Do not output any message
  -V, --version                              Display this application version
      --ansi|--no-ansi                       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction                       Do not ask any interactive question
  -v|vv|vvv, --verbose                       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

守护进程模式

./reli inspector:daemon --help
Description:
  concurrently get call traces from processes whose command-lines match a given regex

Usage:
  inspector:daemon [options]

Options:
  -P, --target-regex=TARGET-REGEX            regex to find target processes which have matching command-line (required)
  -T, --threads[=THREADS]                    number of workers (default: 8)
  -d, --depth[=DEPTH]                        max depth
  -s, --sleep-ns[=SLEEP-NS]                  nanoseconds between traces (default: 1000 * 1000 * 10)
  -r, --max-retries[=MAX-RETRIES]            max retries on contiguous errors of read (default: 10)
  -S, --stop-process[=STOP-PROCESS]          stop the target process while reading its trace (default: off)
      --php-regex[=PHP-REGEX]                regex to find the php binary loaded in the target process
      --libpthread-regex[=LIBPTHREAD-REGEX]  regex to find the libpthread.so loaded in the target process
      --php-version[=PHP-VERSION]            php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
      --php-path[=PHP-PATH]                  path to the php binary (only needed in tracing chrooted ZTS target)
      --libpthread-path[=LIBPTHREAD-PATH]    path to the libpthread.so (only needed in tracing chrooted ZTS target)
  -t, --template[=TEMPLATE]                  template name (phpspy|phpspy_with_opcode|json_lines) (default: phpspy)
  -o, --output=OUTPUT                        path to write output from this tool (default: stdout)
  -h, --help                                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                                Do not output any message
  -V, --version                              Display this application version
      --ansi|--no-ansi                       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction                       Do not ask any interactive question
  -v|vv|vvv, --verbose                       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

类似 top 的模式

./reli inspector:top --help
Description:
  show an aggregated view of traces in real time in a form similar to the UNIX top command.

Usage:
  inspector:top [options]

Options:
  -P, --target-regex=TARGET-REGEX            regex to find target processes which have matching command-line (required)
  -T, --threads[=THREADS]                    number of workers (default: 8)
  -d, --depth[=DEPTH]                        max depth
  -s, --sleep-ns[=SLEEP-NS]                  nanoseconds between traces (default: 1000 * 1000 * 10)
  -r, --max-retries[=MAX-RETRIES]            max retries on contiguous errors of read (default: 10)
  -S, --stop-process[=STOP-PROCESS]          stop the target process while reading its trace (default: off)
      --php-regex[=PHP-REGEX]                regex to find the php binary loaded in the target process
      --libpthread-regex[=LIBPTHREAD-REGEX]  regex to find the libpthread.so loaded in the target process
      --php-version[=PHP-VERSION]            php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
      --php-path[=PHP-PATH]                  path to the php binary (only needed in tracing chrooted ZTS target)
      --libpthread-path[=LIBPTHREAD-PATH]    path to the libpthread.so (only needed in tracing chrooted ZTS target)
  -h, --help                                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                                Do not output any message
  -V, --version                              Display this application version
      --ansi|--no-ansi                       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction                       Do not ask any interactive question
  -v|vv|vvv, --verbose                       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

获取 EG 的地址

./reli inspector:eg --help
Description:
  get EG address from an outer process or thread

Usage:
  inspector:eg_address [options] [--] [<cmd> [<args>...]]

Arguments:
  cmd                                        command to execute as a target: either pid (via -p/--pid) or cmd must be specified
  args                                       command line arguments for cmd

Options:
  -p, --pid=PID                              process id
      --php-regex[=PHP-REGEX]                regex to find the php binary loaded in the target process
      --libpthread-regex[=LIBPTHREAD-REGEX]  regex to find the libpthread.so loaded in the target process
      --php-version[=PHP-VERSION]            php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
      --php-path[=PHP-PATH]                  path to the php binary (only needed in tracing chrooted ZTS target)
      --libpthread-path[=LIBPTHREAD-PATH]    path to the libpthread.so (only needed in tracing chrooted ZTS target)
  -h, --help                                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                                Do not output any message
  -V, --version                              Display this application version
      --ansi|--no-ansi                       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction                       Do not ask any interactive question
  -v|vv|vvv, --verbose                       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

示例

跟踪脚本

$ ./reli i:trace -- php -r "fgets(STDIN);"
0 fgets <internal>:-1
1 <main> <internal>:-1

0 fgets <internal>:-1
1 <main> <internal>:-1

0 fgets <internal>:-1
1 <main> <internal>:-1

<press q to exit>
...

附加到正在运行的进程

$ sudo php ./reli i:trace -p 2182685
0 time_nanosleep <internal>:-1
1 PhpProfiler\Lib\Loop\LoopMiddleware\NanoSleepMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/NanoSleepMiddleware.php:33
2 PhpProfiler\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/KeyboardCancelMiddleware.php:39
3 PhpProfiler\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/RetryOnExceptionMiddleware.php:37
4 PhpProfiler\Lib\Loop\Loop::invoke /home/sji/work/reli/src/Lib/Loop/Loop.php:26
5 PhpProfiler\Command\Inspector\GetTraceCommand::execute /home/sji/work/reli/src/Command/Inspector/GetTraceCommand.php:133
6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Command.php:291
7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Application.php:979
8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.php:299
9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php:171
10 <main> /home/sji/work/reli/reli:45

0 time_nanosleep <internal>:-1
1 PhpProfiler\Lib\Loop\LoopMiddleware\NanoSleepMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/NanoSleepMiddleware.php:33
2 PhpProfiler\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/KeyboardCancelMiddleware.php:39
3 PhpProfiler\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/RetryOnExceptionMiddleware.php:37
4 PhpProfiler\Lib\Loop\Loop::invoke /home/sji/work/reli/src/Lib/Loop/Loop.php:26
5 PhpProfiler\Command\Inspector\GetTraceCommand::execute /home/sji/work/reli/src/Command/Inspector/GetTraceCommand.php:133
6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Command.php:291
7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Application.php:979
8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.php:299
9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php:171
10 <main> /home/sji/work/reli/reli:45

<press q to exit>
...

执行进程必须具有 CAP_SYS_PTRACE 能力。(通常以 root 用户运行就足够了。)

守护进程模式

$ sudo php ./reli i:daemon -P "^/usr/sbin/httpd"

执行进程必须具有 CAP_SYS_PTRACE 能力。(通常以 root 用户运行就足够了。)

获取 EG 的地址

$ sudo php ./reli i:eg -p 2183131
0x555ae7825d80

执行进程必须具有 CAP_SYS_PTRACE 能力。(通常以 root 用户运行就足够了。)

在跟踪中显示当前执行的指令

如果用户想要分析一个真正占用 CPU 的应用程序,那么他或她不仅想知道哪些行是慢的,还想知道哪些指令是慢的。在这种情况下,使用 --template=phpspy_with_opcodeinspector:traceinspector:daemon

$ sudo php ./reli i:trace --template=phpspy_with_opcode -p <pid of the target process or thread>

输出将类似于以下内容。

0 <VM>::ZEND_ASSIGN <VM>:-1
1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:33:ZEND_ASSIGN
2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_DO_FCALL
3 <main> /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL

0 <VM>::ZEND_ASSIGN <VM>:-1
1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:30:ZEND_ASSIGN
2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_DO_FCALL
3 <main> /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL

当前执行的指令成为调用栈的第一帧。因此,像火焰图这样的跟踪可视化可以显示指令的使用情况。

出于信息目的,执行指令也添加到每个调用帧的末尾。除了第一帧之外,像 ZEND_DO_FCALL 这样的函数调用指令应该出现在那里。

如果目标进程启用了 JIT,则这些信息可能略有不准确。

在 Docker 容器和主机进程上使用

$ docker pull sjidev/reli
$ docker run -it --security-opt="apparmor=unconfined" --cap-add=SYS_PTRACE --pid=host sjidev/reli i:trace -p <pid of the target process or thread>

从跟踪信息生成 flamegraphs

$ ./reli i:trace -o traces -- php ./vendor/bin/psalm --no-cache
$ ./reli c:flamegraph <traces >flame.svg
$ google-chrome flame.svg

下方的 flamegraph 可视化了执行 psalm 命令的跟踪信息。

flame

从 phpspy 兼容的跟踪信息生成 speedscope 格式

$ sudo php ./reli i:trace -p <pid of the target process or thread> >traces
$ ./reli c:speedscope <traces >profile.speedscope.json
$ speedscope profile.speedscope.json

参见 #101

故障排除

我收到了错误消息 "php module not found" 并且无法获取跟踪信息!

如果你的 PHP 可执行文件使用的是非标准二进制名称,并且不以 /php 结尾,请使用 --php-regex 选项来指定包含 PHP 解释器的可执行文件(或共享对象)的名称。

我认为跟踪信息不准确。

-S 选项将提供更好的结果。使用此选项将在每次采样时暂时停止目标进程的执行,但获得的跟踪信息将更加准确。如果您在分析像基准测试程序这样的 CPU 重量型程序时没有停止 VM 的运行,您可能会误判瓶颈,因为您将错过更多非常快速转换且未被良好检测到的 VM 状态。

在 Ubuntu 21.10 或更高版本上从 ZTS 目标检索跟踪信息不工作。

尝试指定 --libpthread-regex="libc.so" 作为选项。

我无法在 Amazon Linux 2 上获取跟踪信息。

首先,尝试 cat /proc/<pid>/maps 来检查目标 PHP 进程的内存映射。如果第一个模块没有指示 PHP 二进制的位置,看起来像是一个匿名区域,请尝试指定 --php-regex="^$" 作为选项。

目标

通过这个项目,我们希望实现以下 5 个目标。

  • 能够密切观察运行中的 PHP 脚本内部发生的事情。
  • 成为 PHP 程序员创建自定义 PHP 分析器的框架。
  • 对 PHP 在 Web 之外的使用进行实验,因为 PHP 的最新改进(如 JIT 和 FFI)已经打开了大门。
  • 为 PHP 程序员提供另一个了解 PHP 内部实现的入口点。
  • 创建一个让我觉得编写起来有趣的程序。

授权协议

  • MIT(主要是)
  • tools/flamegraph/flamegraph.pl 是从 https://github.com/brendangregg/FlameGraph 复制的,并受 CDDL 1.0 授权。参见 tools/flamegraph/docs/cddl1.txt 和脚本的头部。
  • 一些定义内部结构的 C 头文件是从 php-src 提取的。它们受 Zend 引擎授权协议的许可。参见 src/Lib/PhpInternals/Headers。因此,这里有由 Zend 引擎授权协议要求的话。
This product includes the Zend Engine, freely available at
     http://www.zend.com

“Reli”这个名字意味着什么?

"Reli" 这个词没有任何意义,尽管你可以自由地将这个工具想象成可靠的、宗教的、令人愉悦的,或者任何你喜欢的 "reli-" 形式的词。

最初,这个工具的名字只是 "php-profiler"。由于许可问题(#175),这个简单的名字不得不更改。

因此,我们对原始名字进行了一次随机字符串操作。将 'php-profiler' 进行 strrev 操作后得到 'reliforp-php',它可以读作 "reli for p(php)"。因此,这个工具的名字现在是 "Reli for PH*"。你也可以简单地称之为 "Reli"。

参见