makinacorpus / profiling
轻量级且灵活的性能分析工具箱,使用高分辨率计时器。
Requires
- php: >= 8.0
Requires (Dev)
- makinacorpus/goat-query: ^3.0.7
- phpbench/phpbench: ^1.2
- phpunit/phpunit: ^9.2
- sentry/sentry: ^3.8
- symfony/config: ^5.4 || ^6.0
- symfony/dependency-injection: ^5.4 || ^6.0
- symfony/http-client: ^5.4 || ^6.0
- symfony/stopwatch: ^5.4 || ^6.0
- symfony/yaml: ^5.4 || ^6.0
README
性能分析和度量工具箱。
包含许多功能
-
计时器API,非常类似于
symfony/stopwatch
,具有时间和内存消耗测量。计时器可以在计时器树内启动,从而允许对计时器进行多维视图。 -
与Prometheus兼容的各种度量收集,包括计数器、仪表、摘要和直方图。这些度量可以通过Prometheus兼容的scraping端点公开。
-
当处于开发模式时,与
symfony/stopwatch
集成,用于Symfony网络分析器。
如果可用,计时器使用PHP \hrtime()
函数的恒定高分辨率计时器进行计时,这提供了更高的精度,并且对系统时钟变化具有弹性,与\microtime()
相反。使用\hrtime()
函数使得此API适合在生产中独立运行。
设置
安装
只需运行
composer require makinacorpus/profiling
要注册Symfony捆绑包,将以下内容添加到您的config/bundles.php
return [ // ... Your other bundles. MakinaCorpus\Profiling\Bridge\Symfony\ProfilingBundle::class => ['all' => true], ];
通过设置此环境变量来启用它
PROFILING_ENABLED=1
也可以启用Prometheus支持
PROFILING_PROMOTHEUS_ENABLED=1
然后复制此包中的src/Bridge/Symfony/Resources/packages/profiling.yaml
到config/packages/
目录。您可以阅读并根据自己的需求进行修改。所有配置选项都在示例配置文件中进行了文档说明。
存储配置
默认存储是使用makinacorpus/query-builder
编写SQL查询的PostgreSQL数据库实现。
它可以在运行时自动创建缺少的表,尽管这可能会影响性能,默认情况下是禁用的。为了允许自动表创建,您可以设置环境变量PROFILING_PROMETHEUS_SCHEMA_AUTOCREATE=1
。
推荐的方法是至少运行一次profiling:prometheus:create-schema
控制台命令
php bin/console profiling:prometheus:create-schema
使用方法
重要注意事项
-
对于每个传入请求,必须有一个唯一的
MakinaCorpus\Profiling\Profiler
实例。 -
“传入请求”意味着单个工作负载,在消息总线消费者的上下文中,可以是一条消息的处理。
计时器
基本用法
以下是代码示例
use MakinaCorpus\Profiling\Profiler\DefaultProfiler; // First, create a profiler. If you are using a framework, you should // inject in your dependency injection container a global instance. $profiler = DefaultProfiler(); // Start a new top-level timer, which has no parent. // Please note that name is optional, it's purely informational. // A unique identifier will be generated if you don't pass one. // You need a name later if you wish to stop one timer without // stopping all the others. $timer = $profiler->start('doing-something'); // Each time you start a new top-level profiler, it is decoupled from // the other one, they won't interact with each-ohter. $otherTimer = $profiler->start('unrelated-other-something'); // From your first timer, you can start children. $timer1 = $timer->start('1'); $timer2 = $timer->start('2'); // Then subchildren. $timer21 = $timer2->start('2.1'); $timer22 = $timer2->start('2.2'); // From a parent timer, you can choose stopping only one child. // You can stop the child directly as well. // The following two lines are equivalent gives a strictly identical result. $timer2 = $timer2->stop('2.2'); $timer22->stop(); echo $timer2->isRunning(); // true echo $timer21->isRunning(); // true echo $timer22->isRunning(); // false // When you close a timer, all the children will be stopped as well. $timer2 = $timer->stop(); echo $timer2->isRunning(); // false echo $timer21->isRunning(); // false echo $timer22->isRunning(); // false // You can fetch timings. // All given numbers are float, reprensenting a number of milliseconds. echo $timer2->getElapsedTime(); // 2.2124548 echo $timer21->getElapsedTime(); // 1.88878889 echo $timer22->getElapsedTime(); // 0.98897574 // You can fully reset the global state, which will also free the // memory it took. // This is precious for long running deamons, such as message bus // consumers which will remain alive for hours. $profiler->flush();
计时器高级用法
MakinaCorpus\Profiling\Timer
接口上有许多方法,所有方法都有文档说明。
计时器跟踪处理器
计时器跟踪处理器是监听所有发出的计时器的组件,然后可以记录信息。默认提供了一些处理器
-
MakinaCorpus\Profiling\Timer\Handler\SentryHandler
(sentry
)可以将您的计时器发送到Sentry实例。 -
MakinaCorpus\Profiling\Timer\Handler\StoreHandler
(store
)可以将您的计时器发送到本地存储实现。目前只有一个实现,它将数据发送到SQL数据库表。 -
MakinaCorpus\Profiling\Timer\Handler\StreamHandler
(file
)将您的计时器发送到日志文件,每个计时器占一行。 -
MakinaCorpus\Profiling\Timer\Handler\SymfonyStopwatchHandler
(stopwatch
)将计时器发送到symfony/stopwatch
组件。这在开发模式下安装了网络分析器调试工具栏时很有用。
处理器可以配置为接受来自所有通道、某些通道或所有通道但某些通道的计时器。
有关处理器配置的更多信息,请参阅示例配置文件。
Prometheus度量
设置
首先,使用环境变量启用它
PROFILING_PROMOTHEUS_ENABLED=1
然后使用您选择的方法计算一个随机访问令牌,并将其设置到您的环境变量中
PROFILING_PROMETHEUS_ACCESS_TOKEN: SuperSecretToken
为了设置Prometheus HTTP端点,请将以下内容添加到config/routes.yaml
prometheus: resource: "@ProfilingBundle/Resources/config/routes.prometheus.yaml" prefix: /prometheus
然后为了获取指标,只需访问以下URL
curl http://yoursite/prometheus/metrics/?access_token=SuperSecretToken
此外,请注意,如果您配置了某些防火墙,您可能需要将^/prometheus/
路径放入非安全防火墙中。
默认配置简单有效,除了需要配置的驱动程序。默认为in_memory
,这意味着它根本不存储任何内容。
定义您自己的指标
每个样本必须在schema
配置部分中定义。如果在此文件中没有定义样本,那么如果您尝试收集它,它将简单地不执行任何操作。
编辑您的config/packages/profiling.yaml
文件
profiling: prometheus: # # Namespace name will prefix all sample names when exported to # Prometheus. For example, if you define "some_counter", the final # sample name will be "symfony_some_counter". # namespace: symfony # # For all sample type, you may set the "labels" entry. # # Labels are some kind tags, whose values are required when # measuring. This is important, please read Prometheus documentation # for more information. # # In all cases, you always should add the [action, method] which then # should be populated using respectively the current route name and # the HTTP method. There are of course some use cases where you may # not need it. # schema: # Gauge is a float value, no more no less, each new measure # will erase the existing value. some_gauge: type: gauge help: This is some gauge labels: [action, method, foo, bar] # A counter is a static value that gets incremented over time. # It never gets reseted, always incremented. # If you drop the data from your database, the next Prometheus # scrap will see the value going down and give invalid data, # but as time will pass, data will get eventually consistent # soon enough. some_counter: type: counter help: Some counter labels: [action, method, foo] # Summary is statistical distribution analysis of some value using # percentiles. Summaries are computed on the client side (ie. # your site) in opposition to histograms which are computed on # the service (ie. in Prometheus). # You may or may not define quantiles, in this example we define # the defaults being applied if none providen. some_summary: type: summary help: Some summary labels: [action, method, foo] quantiles: [0.01, 0.05, 0.5, 0.95, 0.99] # Histogram is a statistical distribution analysis of some value # using buckets. Buckets are supposed to be pre-defined in this # schema. Histograms are computed in the server side (ie. in # Prometheus) in opposition to summaries which are computed on # the client side (ie. your site). # You may or may not define buckets, in this example we define # the defaults being applied if none providen. # It is strongly recommended, for histogram usage, that you define # explicitely your own buckets depending on expected values for # each sample definition. some_histogram: type: histogram help: request durations in milliseconds labels: [action, method, foo] buckets: [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0]
然后,在您需要性能分析的代码点处,注入MakinaCorpus\Profiling\Profiler
服务并使用它。
仪表
使用gauge()
方法
\assert($profiler instanceof \MakinaCorpus\Profiling\Profiler); $timer = $profiler->gauge( // Name in your schema. 'some_gauge', // Label values, considering you kept "action" and "method" // in the (action, method, foo, bar) list: [ $profiler->getContext()->route, $profiler->getContext()->method, 'some_value', 'some_other_value', ], // Arbitrary value you actually measure. 123.456 );
计数器
使用counter()
方法
\assert($profiler instanceof \MakinaCorpus\Profiling\Profiler); $timer = $profiler->counter( // Name in your schema. 'some_counter', // Label values, considering you kept "action" and "method" // in the (action, method, foo) list: [ $profiler->getContext()->route, $profiler->getContext()->method, 'some_value', ], // Arbitrary increment value you actually measure. // You can omit this parameter and increment will be 1. 3, );
摘要
使用与计时器一起的summary
方法
\assert($profiler instanceof \MakinaCorpus\Profiling\Profiler); try { $timer = $profiler->timer(); // Something happens, then... } finally { if ($timer) { $duration = $timer->getElapsedTime(); $profiler->summary( 'something_duration_msec', // Label values, considering you kept "action" and "method" // in the (action, method, foo) list: [ $profiler->getContext()->route, $profiler->getContext()->method, 'some_value', ], // Arbitrary value you actually measure. $duration, ); } }
当然,您可以测量除了持续时间之外的其他内容,任何对您有意义的值都可以添加到摘要中。
重要提示:直方图在输出渲染和存储方面比摘要更高效。如果您事先知道期望值的范围,请避免使用摘要。
直方图
使用与计时器一起的histogram
方法
\assert($profiler instanceof \MakinaCorpus\Profiling\Profiler); try { $timer = $profiler->timer(); // Something happens, then... } finally { if ($timer) { $duration = $timer->getElapsedTime(); $profiler->histogram( 'something_duration_msec', // Label values, considering you kept "action" and "method" // in the (action, method, foo) list: [ $profiler->getContext()->route, $profiler->getContext()->method, 'some_value', ], // Arbitrary value you actually measure. $duration, ); } }
当然,您可以测量除了持续时间之外的其他内容,任何对您有意义的值都可以添加到摘要中。
重要提示:直方图在输出渲染和存储方面比摘要更高效。如果您事先知道期望值的范围,请始终首选直方图而不是摘要。
公开的指标
HTTP请求
NAMESPACE_http_exception_total
(counter
),{action: ROUTE, method: HTTP_METHOD, type: EXCEPTION_CLASS}
NAMESPACE_http_request_duration_msec
(summary
),{action: ROUTE, method: HTTP_METHOD, status: HTTP_STATUS_CODE}
NAMESPACE_http_request_total
(counter
),{action: ROUTE, method: HTTP_METHOD}
NAMESPACE_http_response_total
(counter
),{action: ROUTE, method: HTTP_METHOD, status: HTTP_STATUS_CODE}
NAMESPACE_instance_name
(gauge
),{instance_name: HOSTNAME}
控制台命令
NAMESPACE_console_command_total
(counter
),{action: COMMAND_NAME}
NAMESPACE_console_duration_msec
(summary
),{action: COMMAND_NAME, method: "command", status: EXIT_STATUS_CODE}
NAMESPACE_console_exception_total
(counter
),{action: COMMAND_NAME, method: "command", type: EXCEPTION_CLASS}
NAMESPACE_console_status_total
(counter
),{action: COMMAND_NAME, method: "command", status: EXIT_STATUS_CODE}
消息传递
尚未实现。它可能将是
NAMESPACE_message_consumed_total
(counter
),{action: MESSAGE_CLASS, method: "message"}
NAMESPACE_message_duration_msec
(summary
),{action: MESSAGE_CLASS, method: "message"}
NAMESPACE_message_exception_total
(counter
),{action: MESSAGE_CLASS, method: "message", type: EXCEPTION_CLASS}
Monolog
尚未实现。它可能将是
NAMESPACE_monolog_message_total
(counter
),{action: ROUTE|COMMAND_NAME|MESSAGE_CLASS, method: "message"|"command"|HTTP_METHOD, severity: MONOLOG_LEVEL, channel: MONOLOG_CHANNEL}
系统信息
如果您没有系统监控,此捆绑包可以收集一些系统信息样本。
请参阅src/Bridge/Symfony/Resources/config/packages/profiling.yaml
文件中的完整文档。
CPU负载平均样本
NAMESPACE_sys_load_avg
(gauge
),{span: INT}
- 以分数形式表示的值,为过去span
分钟。
内存使用样本
NAMESPACE_sys_mem_total
(gauge
) - 字节值。NAMESPACE_sys_mem_free
(gauge
) - 字节值。NAMESPACE_sys_mem_used
(gauge
) - 字节值。NAMESPACE_sys_mem_available
(gauge
) - 字节值。NAMESPACE_sys_mem_buffers
(gauge
) - 字节值。NAMESPACE_sys_mem_cached
(gauge
) - 字节值。NAMESPACE_sys_mem_swap_total
(gauge
) - 字节值。NAMESPACE_sys_mem_swap_free
(gauge
) - 字节值。NAMESPACE_sys_mem_swap_used
(gauge
) - 以字节为单位的值。
磁盘使用情况样本
NAMESPACE_sys_disk_total
(gauge
),{name: NAME}
- 以字节为单位的值。NAMESPACE_sys_disk_free
(gauge
),{name: NAME}
- 以字节为单位的值。NAMESPACE_sys_disk_used
(gauge
),{name: NAME}
- 以字节为单位的值。
默认情况下,如果您没有配置任何磁盘,则目标为当前项目目录的挂载点,并且 NAME
为 app
。
警告:因为探测器仅在发生请求时收集数据,您可以选择运行 profiling:prometheus:sys-info
控制台命令以作为 cron 作业定期收集系统信息。
将分析器注入到您的服务中
当在 Symfony 项目中工作时,将分析器注册到服务中的推荐方法是以下
namespace MyVendor\MyApp\SomeNamespace; use MakinaCorpus\Profiling\ProfilerAware; use MakinaCorpus\Profiling\ProfilerAwareTrait; /** * Implementing the interface allow autoconfiguration. */ class SomeService implements ProfilerAware { use ProfilerAwareTrait; }
通过使用 \MakinaCorpus\Profiling\ProfilerAwareTrait
,您可以使您的代码在初始化错误的情况下更加健壮
-
如果自动配置失败,它将创建一个默认的空实例,不执行任何操作,这将对性能的影响几乎为零。
-
如果组件被禁用,它将创建一个默认的空实例,不执行任何操作,这将对性能的影响几乎为零。
然后您可以使用分析器
namespace MyVendor\MyApp\SomeNamespace; use MakinaCorpus\Profiling\ProfilerAware; use MakinaCorpus\Profiling\ProfilerAwareTrait; /** * Implementing the interface allows autoconfiguration. */ class SomeService implements ProfilerAware { /** * Using the trait provides a default working implementation. */ use ProfilerAwareTrait; public function doSomething() { $timer = $this->getProfiler()->start('something'); try { $timer->start('something-else'); $this->doSomethingElse(); $timer->stop('something-else'); $timer->start('something-other'); $this->doSomethingElse(); $timer->stop('something-other'); $timer->start('something-that-fails'); throw new \Exception("Oups, something bad happened."); $timer->stop('something-that-fails'); } finally { // We do heavily recommend that use the try/finally // pattern to ensure that exceptions will not betry // your timers. // The last stop() call within the try block will never // be called, by stopping the parent timer here, it // stops the child as well. $timer->stop(); } } }
这就差不多了。
内存使用情况
计时器类也测量内存使用情况,但请注意,这些结果将受到该 API 本身消耗内存的偏差。
CLI 杀死开关
如果您正在 CLI 中工作,并且想禁用长时间运行的任务或迁移批次的分析,只需在您的命令行中添加 PROFILING_ENABLE=0
环境变量。
这不会完全禁用组件,这是一个软禁用,只会防止在此运行时创建分析器,例如
PROFILING_ENABLE=0 bin/console app:some-very-long-batch
路线图
- 允许配置流处理程序的格式化程序。
- 实现 Prometheus 度量指标的 Redis 存储。
- 合并度量和分析器接口/类。
- 将计时器连接到 Prometheus 总结和直方图。
- Prometheus doctrine SQL 查询计数度量。
- Prometheus messenger 度量。
- Prometheus monolog 度量。
- Prometheus 响应大小度量。
- Prometheus 意外度量记录到 monolog。
- 使用日期清除控制台命令。
- 查看控制台命令。
- 编写文档网站。