Рекомендации по профилированию PHP приложений

В этой статье я расскажу, что такое профилирование и покажу на примере конкретных профилировщиков как происходит этот процесс.

Профилирование — это процесс динамического анализа работы программы, который помогает выявить неэффективные участки, которые не могут быть выявлены на этапе статического анализа.

Для профилирования используют специальный инструмент – профилировщик. Он измеряет показатели работы программы, например использование памяти или процессорного времени.

Показатели, которые измеряются при профилировании:
  • Время исполнения программы
  • Затраченное процессорное время
  • Количество вызовов функций
  • Выделенная и используемая память
  • Операции ввода-вывода

Выбрать подходящий инструмент

Xdebug — Часть PHP-расширения Xdebug, реализующая профилирование выполнения скриптов на уровне функций. Xdebug работает как расширение для Zend Engine. Он добавляет «хуки» для вызовов функций, конструкторов, деструкторов и методов классов. Это гарантирует высокую точность, но может существенно замедлить выполнение кода (по информации из разных источников исполнение замедляется от 2х и вплоть до нескольких десятков раз), поэтому подходит только для локальной отладки.

XHProf — это легковесный профилировщик, основанный на иерархии и инструментировании. В процессе сбора данных он отслеживает количество вызовов и инклюзивные метрики вызовов программы. Эксклюзивные метрики, такие как затраченное время, время CPU и потребление памяти, рассчитываются в фазе постпроцессинга.

Blackfire — комплексное решение для непрерывного наблюдения, позволяет вам отслеживать поведение ваших приложений в реальном времени. Имеет низкие накладные расходы, так как по умолчанию профили работают на основе синтетических запросов, не создавая накладных расходов. Имеет удобное графическое представление собранных профилей. В отличие от XHProf и Xdebug является платным сервисом.

PHP-SPX — бесплатный open-source проект. Имеет неплохую встроенную визуализацию профилей. Но автор не рекомендует использовать в продакшен-среде, так как это всё ещё экспериментальная версия, и API может изменяться или проект в целом может быть заброшен.

Tideways — коммерческий наследник XHProf с поддержкой современных PHP-версий, удобной визуализацией и интеграцией в CI/CD. Подходит для staging и production.

New Relic — платформа APM для мониторинга PHP-приложений в production. Дает метрики по времени отклика, нагрузке на базу данных и сервер. Сервис является платным, хотя и имеет бесплатную версию с некоторыми ограничениями.


В ходе статьи мы будем рассматривать XHProf. Это один из самых распространённых профилировщиков, по нему достаточно много информации в открытом доступе. Есть множество готовых решений и инструментов, которые совместимы с ним.

Мы рассмотрим принципы работы XHProf и нюансы, которые будут справедливы для большинства профилировщиков. Так что даже если вы склоняетесь к другому решению, будет полезно ознакомиться с тем, как работает XHProf.

XHProf — это иерархический профилировщик для PHP. Компонент сбора необработанных данных реализован на языке C в виде расширения PHP Zend под названием xhprof. XHProf позволяет получить следующую информацию:

Какие функции запускались

Важно уточнить, что будут собираться также вызовы встроенных функций (count, max, in_array), данное поведение можно изменить с помощью флага XHPROF_FLAGS_NO_BUILTINS для функции xhprof_enable(). В таком случае профилировщик не будет собирать метрики со встроенных функций.

Время выполнения функции

Мы можем собирать 2 параметра времени:

  • Время выполнения функции (Wall Time) — включает в себя не только время выполнения кода в PHP, но и время ожидания операций ввода-вывода, работы с сетью, базами данных и других внешних сервисов
  • Процессорное время (CPU Time) — время, в течение которого процессор был занят выполнением кода вашего приложения.

Например, для вызова sleep(1)

	Wall Time ~ 1005967  
	CPU Time ~ 205

Важно заметить, что подсчёт CPU Time — довольно затратная операция. Процессорный таймер (getrusage) в Linux имеет высокие накладные расходы. Он также имеет грубую точность (с точностью до миллисекунд, а не до микросекунд), что делает его полезным на уровне функций. Поэтому искажение данных о времени работы функции при использовании режима XHPROF_FLAGS_CPU, как правило, выше.

Для подсчёта времени выполнения профилировщик использует инструкцию RDTSC (счётчик временных меток) для реализации таймера прошедшего времени с минимальными накладными расходами. Поэтому в настоящее время xhprof работает только на архитектуре x86. Кроме того, поскольку значения RDTSC могут быть не синхронизированы между процессорами, xhprof привязывает программу к одному процессору на время профилирования.

Потребление памяти

Учтите, что профилировщиком не отслеживается строго каждая операция выделения/освобождения памяти. Вместо этого отслеживается увеличение/уменьшение объёма памяти, выделенной PHP, между входом и выходом каждой функции. Он также отслеживает увеличение/уменьшение пикового объёма памяти, выделенной PHP для каждой функции. Данная метрика собирается только в режиме XHPROF_FLAGS_MEMORY.

Иерархический профилировщик

Что же скрывается за фразой "иерархический профилировщик"? Когда профилировщик получает информацию о работе программы, он считает, сколько раз функция bar (родитель) вызвала функцию foo (потомок) и сколько ресурсов на это ушло. Это накладывает некоторые ограничения, например: мы не можем отделить стек вызова car()->foo()->bar() от map()->foo()->bar(), всё что мы можем – это знать, что функция foo вызывала функцию bar n-раз и увидеть время и ресурсы, потраченные на выполнение. Поэтому важно помнить, что если у нас время выполнения функции зависит от входных параметров, то может возникнуть ситуация, когда в контексте map()->foo()->bar() функция будет выполняться быстро, а в контексте car()->foo()->bar() — медленно или потреблять большее количество ресурсов. Но профилировщик покажет нам только, что стек foo()->bar() выполнялся долго. Поэтому важно правильно локализовать проблему. Для этого нужно брать во внимание не только сырые данные, но и постпроцессинг, чтоб отделить инклюзивные показатили от екслюзивных.

Подробнее рассмотрим на примере:


function bar(Object $obj) :void{
	  // ... какие-то трудозатратные опреции c $obj. 
}

/** 
 * @param list<list<Object>>
 */
function foo(array $y) :void{

	// ... какая-то обратка $y
	
	$values = [];
	foreach($y as $value){
		array_push($values, ...$value); 
	}

	bar($values)
}

function map() :void{
	foo([ new Object(1) ])
}


function car() :void{
	$values = array_fill(0, 10_000, new Object(2))
	
	foo($values);
}

Результаты:

В стеке cat()->foo() был один вызов и wall time 100 мс.
В стеке map()->foo() был один вызов и wall time 20 мс.
В стеке foo()->bar() было 10К вызовов и wall time 60 мс.

Визуализация профиля и постпроцессинг

Сырые данные XHProf трудно анализировать вручную, а поставляемый с XHProf инструмент для визуализации сильно устарел (там ещё jQuery используется 😁). Для визуализации можно использовать Buggregator — удобный инструмент для сбора и отображения профилей.

Buggregator — это бесплатный серверный инструмент, разработанный для отладки PHP-приложений. Он предлагает 3 варианта визуализации профилей:

  • Call Graph — отображает вызовы функций в виде древовидной структуры. Цвет узлов в дереве вызовов варьируется от белого до тёмно-красного.
  • FlameGraph — это графическое представление стека вызовов функций и показывает, какие функции вызываются какими другими функциями.

Top 100 Functions — в этой таблице перечислены 100 основных вызовов функций, потребляющих больше всего ресурсов.

Это очень удобное представление профиля, но нужно учитывать особенности сбора профиля иерархическим профилировщиком.

Этот инструмент также проводит постпроцессинг данных и предоставляет следующую информацию о работе той или иной функции:

  • Inclusive Time (или Subtree Time) — включает время, затраченное в самой функции, а также во всех дочерних функциях, которые были вызваны из неё.
  • Exclusive Time / Self Time — измеряет время, затраченное только внутри самой функции. Не включает время, потраченное на вызовы дочерних функций.
  • Wall Time — общее время выполнения.
  • CPU Time — время работы процессора.

Buggregator поставляется как Docker-образ, который предоставляет вам не только визуализацию, но и сохранение профилей. У него есть свой API, а также БД, что позволяет легко настроить автоматическое сохранение профилей. Также не составляет большого труда запустить Buggregator в облаке, чтоб один экземпляр Buggregator-а мог обрабатывать сразу несколько ваших сервисов. Для облегчения поиска профиля есть возможность указать, с какого сервиса/проекта собран данный профиль.

Для лёгкого внедрения в проект и проведения профилирования есть готовые интеграции:

  • spiral-packages/profiler — подключается к приложению и автоматически отправляет профили XHProf в Buggregator.
  • iluckhack/xhprof-buggregator-bundle — интеграция с Symfony, позволяющая собирать и визуализировать профили без дополнительной ручной настройки.

Запуск профилировщика на разных окружениях

Выборочное профилирование
Запускать профилировщик на 100% трафика не имеет смысла, так как вы просто утонете в огромном количестве информации и рискуете "не увидеть лес из-за деревьев". Достаточно будет собрать профили с небольшого процента трафика — так вы получите всю нужную информацию, и большинство пользователей не заметит, что сервис стал отвечать дольше.

Правильная конфигурация
Как мы говорили выше, XHProf хорошо справляется с тем, чтобы измерять время выполнения, и хуже — с измерением процессорного времени. При планировании оптимизаций подумайте, а нужно ли вам собирать тот или иной профиль нагрузки. Если ваши метрики показывают, что вы не потребляете слишком много процессорного времени и памяти, но при этом у вас повышено время ответа от сервера, возможно, для начала стоит собрать только Wall Time профиль.

Сетевые задержки и кэши
При анализе профиля убедитесь, что кейс, который вы пытаетесь оптимизировать, повторяем. Стоит проверить, что запрос не был долгим из-за "холодного кэша" или сетевого лага. Например, вы видите, что при вызове метода, который делает запрос в БД, был долгий ответ:

  • Проверьте, не было ли проблем с сетью.
  • Не была ли перегружена база в данный момент.
  • Возможно, 99.99% запросов для получения этих данных ходят в кэш, а вам повезло собрать профиль именно с этой сотой процента.

В процессе оптимизации кода, для тестирования вы, скорее всего, будете запускать профилировщик локально или в dev-окружении для тестирования внесённых изменений. Стоит помнить, что выполнение кода в dev или test окружениях может сильно отличаться от работы в продакшене. Стоит убедиться, что вы не пытаетесь оптимизировать сборку CI-контейнера, например, которая для тестов может запускаться каждый раз, а на продакшен — только во время релиза. Если вы запускаете код локально, а тестовая база у вас в облаке, стоит помнить, что у вас будут высокие сетевые задержки, которых может не быть в продакшене.

Подписаться на новые выпуски блога

Не пропустите последние обновления.
i.ivanov@yandex.ru
Подписаться