Профилирование PHP скриптов на живом сервере

в 10:36, , рубрики: php, pinba, profile, wapstart, Блог компании WapStart, метки: , , ,

Добрый день, сообщество!

Наверняка кто-то из вас сталкивался с такой проблемой — медленно работает сайт на реальном сервере.
Важно оперативно выяснить в каких местах возникли сложности. Использовать для этого xdebug нельзя, так как он создает большую нагрузку на сервер и сам вносит погрешность в измерения. Для решения этой задачи мы выбрали систему, которая позволяет очень быстро собирать древовидную статистику по работе сайта — pinba.

На хабре уже рассказывали о специфике работы с pinba. Если вы не читали, можете ознакомиться по ссылке.

Для нетерпеливых сразу дам ссылку на результат.
Plus1 WapStart работает в штатном режиме при нагрузке около 1000 запросов в секунду на один инстанс.

Как это все работает?

Сбор данных

Pinba отправляет на свой сервер (по UDP, очень быстро) метки начала и конца отрезка времени (далее — таймеры) и складывает данные в MySQL таблицы (легко прочитать). Например

$timer = pinba_timer_start(array('tag' => 'some_logic'));
....
pinba_timer_stop($timer);

Для построения древовидной структуры мы добавляем 2 дополнительных тега — tree_id (каждый раз уникальный id) и tree_parent_id — это tree_id от того таймера, в который вложен текущий. Например

$parent_timer = pinba_timer_start(array('tag' =>'some_logic', 'tree_id' => 1, 'tree_parent_id' => 'root'));

   $child_timer = pinba_timer_start(array('tag' =>'child_logic', 'tree_id' => 2, 'tree_parent_id' => 1));
   pinba_timer_stop($child_timer);

pinba_timer_stop($parent_timer);

Таким образом, на сервере можно воспроизвести вложенность таймеров и построить удобочитаемое дерево.
Мы разместили во всех интересных местах проекта таймеры, чтобы засекать время (например, при sql запросах, при записи в файлы и т.д.).

Подготовка данных

К сожалению, pinba не использует индексы для запросов (кроме PRIMARY), так как используется свой pinba ENGINE (таблицы фактически хранятся в memory, и данные старше N минут удаляются, в нашем случае — 5 минут). Но нельзя сетовать на pinba, так как она предназначена не для запросов по индексам.
Для нас индексы важны, потому мы копируем все данные из таблиц pinba в обычные MyISAM таблицы.

truncate table pinba_cache.request;
truncate table pinba_cache.tag;
truncate table pinba_cache.timer;
truncate table pinba_cache.timertag;

insert ignore into pinba_cache.request select * from pinba.request;
insert ignore into pinba_cache.tag select * from pinba.tag;
insert ignore into pinba_cache.timer select * from pinba.timer;
insert ignore into pinba_cache.timertag select * from pinba.timertag;

Как видно из запросов, у нас система работает в базе pinba, а копия — в базе pinba_cache.

Так же для работы нам понадобится ещё одна таблица, в которой будут поля tree_id и tree_parent_id.

truncate table pinba_cache.timer_tag_tree;
insert ignore into pinba_cache.timer_tag_tree
SELECT * FROM (
	SELECT null, timer_id, request_id, hit_count, timer.value, GROUP_CONCAT(timertag.value) as tags
	, (select timertag.value from pinba_cache.timertag where timertag.timer_id=timer.id and tag_id = (select id from pinba_cache.tag where name='treeId')) as tree_id
	, (select timertag.value from pinba_cache.timertag where timertag.timer_id=timer.id and tag_id = (select id from pinba_cache.tag where name='treeParentId')) as tree_parent_id
	FROM pinba_cache.timertag force index (timer_id)
	LEFT JOIN pinba_cache.timer ON timertag.timer_id=timer.id
	where not tag_id in ((select id from pinba_cache.tag where name='treeId'), (select id from pinba_cache.tag where name='treeParentId'))
	group by timertag.timer_id
	order by timer_id
) as tmp
GROUP BY tree_id;

Структура таблицы timer_tag_tree приведена ниже. Структура остальных таблиц такая же как в pinba.

CREATE TABLE `timer_tag_tree` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`timer_id` INT(10) NOT NULL DEFAULT '0',
	`request_id` INT(10) NULL DEFAULT NULL,
	`hit_count` INT(10) NULL DEFAULT NULL,
	`value` FLOAT NULL DEFAULT NULL,
	`tags` VARCHAR(128) NULL DEFAULT NULL,
	`tree_id` VARCHAR(35) NOT NULL DEFAULT '',
	`tree_parent_id` VARCHAR(35) NOT NULL DEFAULT '',
	PRIMARY KEY (`id`),
	INDEX `timer_id` (`timer_id`),
	INDEX `tree_id_tree_parent_id` (`tree_id`, `tree_parent_id`),
	INDEX `tree_parent_id_tree_id` (`tree_parent_id`, `tree_id`)
)
COLLATE='utf8_general_ci'
ENGINE=MyISAM
Анализ данных

Теперь — самое интересное. Мы собрали данные, сложили их так, как нам необходимо для последующей работы. Далее необходимо написать скрипт, который все эти данные выдаст в удобном виде.
Как вывести одно дерево (от одного запроса к сайту) — писать не буду, так как это тривиальная задача.
Проблема в том, что для оценки узких мест нужно проанализировать сотни запросов к php, каждый из которых имеет свое дерево вызова функций (таймеров). Нам нужно из этих деревьев собрать одно обобщенное дерево.

Алгоритм объединения следующий:
Профилирование PHP скриптов на живом сервере
Для каждого узла считаем сумму времен выполнения этого узла по всем деревьям.
Написав функцию для объединения двух деревьев, можно пройтись циклом по всем и получить сумму.
Но тут нас ждет неприятный сюрприз — медленное время работы.
Как видим из картинки, сложность объединения 2 деревьев — O(N*N) (внимательные мне подскажут что можно сделать это за N*log(N), но далее будет более простой метод оптимизации, в 3 строчки), где N — к-во узлов в дереве. Соответственно выгодно объединять маленькие деревья, и очень невыгодно большие.
Постараемся эту особенность использовать. Давайте определим дерево выполнения одного скрипта как дерево 1 уровня, сумма двух деревьев первого уровня — дерево второго уровня и т.д. В таких терминах нам нужно объединять побольше деревьев первого уровня, и минимум большого уровня. Делать это будем так:

как видим, суммарное к-во объединений было N-1, из которых N/2 — первого уровня, N/4 — второго уровня, N/8 — третьего и т.д.
Реализуется эта хитрость крайне просто с помощью рекурсии (при желании её можно разложить в цикл, но для большей понятности я этого делать не буду).

//принимает на вход массив деревьев, на выход - объединенное дерево
function mergeTreeList(array $treeList) {
    if (count($treeList) > 2) {
        return mergeTreeList( половина($treeList), вторая_половина($treeList));
    }
    //...
    //тут идет код объединения
}

Таким образом, мы сначала объединим изначальные деревья в 2х, а потом уже их будет объединять в деревья побольше. Выигрыш по времени у нас составил в ~10 раз (1000 деревьев).

Итого

  • Мы разместили pinba-таймеры в нашем приложении, где посчитали нужным
  • Мы сформировали агрегированное дерево выполнения на основании многих запросов к скриптам
  • По построенному дереву можно анализировать узкие места проекта, можно строить графики скорости выполнения отдельных кусков проекта
  • Все это происходит прямо на живом сервере с большой нагрузкой

Подводные камни и минусы

  • На нашем проекте пинба так быстро пишет (и удаляет старое), что запрос insert into table_copy select * from table копирует в 2-3 раза больше данных, чем изначально было в таблице. Потому на время копирования таблиц нужно останавливать запись в pinba (я останавливал сеть на сервере фаерволом)
  • Pinba потребляет много памяти (у нас — 2 Gb чтобы хранить данные за 5 минут), так как мы вместо одного тега пишем 3 (+tree_id, +tree_parent_id)
  • При копировании приходится отключать сеть, чтобы остановить запись в таблицы (на 5-10 секунд), из-за чего теряются данные за эти 5-10 секунд

Полезные файлы:
Скрипт для отображения дерева: index.php
MySQL скрипт для преобразования данных cron.sql
PinbaClient.class.php — обертка над pinba для более удобного использования с автоматическим добавлением tree_id, tree_parent_id
Так же хочется упомянуть фреймворк onphp, в котором есть нативная поддержка pinba
https://github.com/ents/pinba-php-profiler/ — исходные файлы, чтобы поднять все у себя
http://pinba.org/ — тут можно скачать pinba

Дисклаймер: Данная статья носит популярный характер и не может рассматриваться как руководство к действию. Все действия, описанные ниже не есть истина в последней инстанции, а скорее один из немногих способов сделать визуализацию информации из pinba

Автор: Ents

* - обязательные к заполнению поля