- PVSM.RU - https://www.pvsm.ru -
Добрый день, сообщество!
Наверняка кто-то из вас сталкивался с такой проблемой — медленно работает сайт на реальном сервере.
Важно оперативно выяснить в каких местах возникли сложности. Использовать для этого xdebug нельзя, так как он создает большую нагрузку на сервер и сам вносит погрешность в измерения. Для решения этой задачи мы выбрали систему, которая позволяет очень быстро собирать древовидную статистику по работе сайта — pinba.
На хабре уже рассказывали о специфике работы с pinba [1]. Если вы не читали, можете ознакомиться по ссылке.
Для нетерпеливых сразу дам ссылку на результат [2].
Plus1 WapStart [3] работает в штатном режиме при нагрузке около 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, каждый из которых имеет свое дерево вызова функций (таймеров). Нам нужно из этих деревьев собрать одно обобщенное дерево.
Алгоритм объединения следующий:
Для каждого узла считаем сумму времен выполнения этого узла по всем деревьям.
Написав функцию для объединения двух деревьев, можно пройтись циклом по всем и получить сумму.
Но тут нас ждет неприятный сюрприз — медленное время работы.
Как видим из картинки, сложность объединения 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 деревьев).
Полезные файлы:
Скрипт для отображения дерева: index.php [4]
MySQL скрипт для преобразования данных cron.sql [5]
PinbaClient.class.php [6] — обертка над pinba для более удобного использования с автоматическим добавлением tree_id, tree_parent_id
Так же хочется упомянуть фреймворк onphp [7], в котором есть нативная поддержка pinba
https://github.com/ents/pinba-php-profiler/ [8] — исходные файлы, чтобы поднять все у себя
http://pinba.org/ [9] — тут можно скачать pinba
Дисклаймер: Данная статья носит популярный характер и не может рассматриваться как руководство к действию. Все действия, описанные ниже не есть истина в последней инстанции, а скорее один из немногих способов сделать визуализацию информации из pinba
Автор: Ents
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/12365
Ссылки в тексте:
[1] pinba: http://habrahabr.ru/post/129042/
[2] результат: http://habrahabr.wapstart.ru/pinba/?script_name=page_article
[3] Plus1 WapStart: https://plus1.wapstart.ru/
[4] index.php: https://github.com/ents/pinba-php-profiler/blob/master/index.php
[5] cron.sql: https://github.com/ents/pinba-php-profiler/blob/master/cron.sql
[6] PinbaClient.class.php: https://github.com/onPHP/onphp-framework/blob/master/main/Monitoring/PinbaClient.class.php
[7] onphp: https://github.com/onPHP/onphp-framework
[8] https://github.com/ents/pinba-php-profiler/: https://github.com/ents/pinba-php-profiler/
[9] http://pinba.org/: http://pinba.org/
Нажмите здесь для печати.