Статья может помочь вашему живому проекту не тормозить(или тормозить меньше), либо стать отправной точкой для исследования стороннего продукта.
Например, у вас стоит задача понять что же происходит внутри «Самописная система 3.14» и, опять же, помочь ей не есть по 100 мегабайт RAM на одного клиента.
О исследуемой программе
WebAsyst Shop-Script- вторая попытка ребят из ООО «Артикус» сделать хорошо. Первой попыткой был дырявый и приносящий многим и по сей день проблемы Shop-Script Premium.
Строго говоря, WebAsyst- это целый комплекс программ типа блокнота, календаря и менеджера проектов, но человеку, который не первый день в интернет-разработке или бизнесе эти решения врядли будут интересны(basecamp же).
О том увенчалась или нет их попытка успехом я могу сказать так- совсем недавно мы отпраздновали 666-ю ревизию альтернативной ветки и это не конец.
Цели
Целью является выявление наиболее ресурсоёмких операций и определение состояния системы при критических обьемах данных. Под данными я подразумеваю количество категорий и товаров. В некоторых случаях будут даваться рекомендации по оптимизации, но видя источник беды нейтрализовать её уже не так сложно.
Подготовка: структура директорий и зависимости
Совсем недавно я спрашивал на хабравопросах о компоненте для автоматической генерации зависимостей между файлами и открыл для себя inclued расширение, которое дружит с Graphviz, что и вам советую, иначе понять что практически самый главный компонент программы находится по адресу
\published\SC\html\scripts\modules\test\class.test.php
и зачем нужен
published\SC\html\scripts\modules\test\_methods\search_simple.php будет нудно и неинтересно.
Я пошел по пути grep, так как у меня было время и необходимость познать суть, но больше так не хочу и вам не советую.
Особенно учитывая их эксперименты с callback, от которых мне до сих пор дурно.
Подготовка: наполнение содержимым
Глупо было бы спорить, что тестирование(если это только не alpha стадия разработки) на малом обьеме данных не имеет смысла. Поэтому в первую очередь наполните вашу CMS содержимым под завязку. Если нет реальных данных- напишите спамера, который пишет в БД пока может или пока вы этого хотите случайности.
WebAsyst SS совершенно по-разному ведет себя на каталогах с 450-ю и 4500-ю товаров.
Подготовка: перегрузка стандартных фукнций
Я буду говорить не о перегрузки с точки зрения ООП, а перегрузке в лоб. Метод прост- проходимся по всем файлам в поисках стандартной функции, заменяем её на ov_{original_name} и тут уж все карты открыты. Хотим- логируем все запросы к базе, хотим- смотрим кто и когда стучиться на fopen,fwrite,file_get_contents или пытается воспользоваться чёрной магией, вроде eval. Самым полезным оказывается логирование именно mysql_query, так как обычно производительность упирается в бэкенд.
У меня используется что-то такое:
function ov_mysql_query($text) {
$debug=false;
if($debug){
$source=debug_backtrace();
$src_array_count=count($source);
$what=array('\r\n','=',',','?','&','+',')','(','/');
$to=array('\r\n','_','_','_','_','_','_','_','_');
$filename=str_replace($what,$to,$_SERVER['REDIRECT_URL']);
static $function_counter_m = 0;
$function_counter_m++;
$oldDir=getcwd();
chdir($_SERVER['DOCUMENT_ROOT']);
$fp = fopen('logs/'.$filename.'.log', 'a');
fwrite($fp, $function_counter_m.') '.str_replace('\r\n','',trim($text))."\r\n");
for($i=0;$i<$src_array_count;$i++) {
fwrite($fp,'DEBUG INFO:'.$source[$i]['file'].' | '.$source[$i]['line']."\r\n");
}
fwrite($fp,"\r\n");
fclose($fp);
chdir($oldDir);
}
$q=mysql_query($text);
return $q;
}
Как результат работы- в папке www/logs сохраняется файл с отладочной информацией(стек вызовов) о запросе и сам запрос.
Подготовка: xDebug
Если честно, то мне сложно назвать отладкой попытку разобраться в чужом механизме. Скорее это препарирование. Однако, то, стоит ли у вас отладчик напрямую зависит сможете ли вы выявить узкие места и оптимизировать систему. Если вы пишите программы на php, то вам необходим xDebug- он бесплатен и его поддерживают все хоть чуть-чуть уважающие себя редакторы php кода.
Отладчик генерирует в установленной вами директории специальные дампы, в которых хранятся различные данные(опционально). Так как основной ОС у меня служит Windows у вас может быть преимущество на этом шаге, так как Linux`овский kcachegrind намного удобнее wincachegrind (обе программы позволяют просматривать эти дампы, хотя по-правде это обычные txt файлы, прочесть которые можно и через блокнот при должной суровости).
Начнём же тормошить зомби.
Тестовый стенд
- Используемая версия WebAsyst: 287(чистая, без патчей и модов)*
- Количество товаров в БД: 4461
- Количество типов характеристик в БД: 144
- Количество значений характеристик в БД: 44170
- Количество категорий в БД: 354
- Количество картинок в БД: 3516
*но тем, кто следит за их changelog всё равно, потому что от 250 её отличает только цвет кнопочек в подокне окна админки
Немного о начальной конфигурации
Результаты работы чистого движка на дефолтном одном товаре и одной категории. В программе присутствуют штатные механизмы кеширования, но насколько они себя оправдывают вы можете судить сами.
Страница* | Запросов к БД с дефолтным кешем | Запросов к БД без дефолтного кеша | Скорость загрузки с дефолтным кешем** | Скорость загрузки без дефолтного кеша** |
Главная | 64 | 73 | 10,304 | 17,011 |
Категория | 83 | 90 | 10,616 | 19,457 |
Товар | 100 | 107 | 15,010 | 28,731 |
Поиск(успешный) | 69 | 76 | 10,507 | 18,209 |
*ссылка ведет на скриншот страницы, чтобы было понятно, соразмерны ли запрашиваемые обьемы данных с отображаемыми **camulative time из Wincachegrind
Если у вас еще не встали волосы дыбом, читайте дальше и не забывайте, что здесь были всего один товар и одна категория.
Конфигурация с данными
Настало время использовать наш тестовый стенд с несколькими тысячами товаров и мощной иерархией каталогов.
Страница | Запросов к БД с дефолтным кешем | Запросов к БД без дефолтного кеша | Скорость загрузки с дефолтным кешем | Скорость загрузки без дефолтного кеша |
Главная | 64 | 73 | 12,323 | 19,404 |
Категория | 186 | 193 | 20,333 | 29,881 |
Товар | 108 | 115 | 16,156 | 30,100 |
Поиск(успешный) | 69 | 76 | 20,733 | 25,162 |
Подбор по характеристикам (расширенный поиск) | 900 | 907 | 43,216 | 50,242 |
Если главная страница еще хоть как-то остаётся при своих 64-ёх запросах(констукциями IN(a,b,c,d,...,z)), то категорию немного колбасит, а подбор по характеристикам уничтожит не то что обычный хостинг, но и VPS. Но вы же не думаете, что отключение расширенного поиска вам поможет? В данном программном продукте есть несколько недокументированных возможностей, которые в руках конкурентов могут осложнить жизнь.
Об этих возможностях можно узнать, копаясь в классе, отвечающем за обработку URl(class.furl.php). Например, можно безостановочно долбить запросом store.ru/category/category_with_lot_products/all/. У меня в такой категории в верхнем уровне 113 страниц.
Табличка:
Страница | Запросов к БД с дефолтным кешем | Запросов к БД без дефолтного кеша | Скорость загрузки с дефолтным кешем | Скорость загрузки без дефолтного кеша |
Категория (/all/) | 241 | 248 | 430,049 | 439,102 |
Небольшой подитог
На текущей стадии исследования мы знаем:
- Есть возможность создавать потенциально высокую нагрузку
- Количество запросов к базе данных с кешем и без слишком велико
Также, если посмотреть на дамп, созданный отладчиком при загрузке страницы store.ru/category/category_with_lot_products, можно с уверенностью выделить две самые прожорливые операции:
foreach ($Interfaces as $_Interface){
ModulesFabric::callInterface($_Interface);
}
и
print $smarty->fetch($CurrDivision->MainTemplate);
Помимо них очень много ресурсов тратится на получение дерева категорий, 95 с лишним тысяч раз вызывается is_object, 70 тысяч раз программа спрашивает LanguagesManager::getInstance и 28 с лишним тысяч раз считает длину строки, а вызовы LanguagesManager::ml_isEmpty составляет 2/3 самой медленной операции- getExtraParametrs.
Варианты решения проблем
Лёгкий
Если у вас не так много посетителей, но программа тормозит можно воспользоваться файловым кешированием с минимальным временем интеграции.
Я предлагаю такую схему:
- Находим тяжелую функцию
- Определяем, не зависит ли она от каких-либо глобальных переменных
- Переименовываем её во что-то вроде {original_function}_cached
- Создаём {original_function}, в теле которой вызывается через специальную функцию {original_function}_cached
На ранних стадиях оптимизации, когда нужно было чтобы программа работала быстро и времени не было я использовал такое решение:
function cache_function($buildCallback, array $args = array(), $timeoutHours = 5){
$oldDir=getcwd();
chdir($_SERVER['DOCUMENT_ROOT']);
// Устанавливаем имя кеш-файла
if(is_array($buildCallback)){
$cacheKey = get_class($buildCallback[0]) .'::'. $buildCallback[1];
}
else{
$cacheKey = $buildCallback . ':' . serialize($args);
}
$cacheKey .= ':' . serialize($args);
if(!file_exists('functions_cache/'.$buildCallback.'/')) {
@mkdir('functions_cache/'.$buildCallback.'/');
}
$file_path = 'system_cache/'.$buildCallback.'/'. sha1($cacheKey);
if(!file_exists($file_path) OR filemtime($file_path) < (time() - $timeoutHours*60)){
$result = call_user_func_array($buildCallback, $args);
file_put_contents($file_path, serialize($result), LOCK_EX);
}else{
$result = unserialize(file_get_contents($file_path));
}
chdir($oldDir);
return $result;
}
Получаем:
function original_function($arg1,$arg2){
return cache_function('original_function_cached',array($arg1,$arg2),10);
}
Как результат- в дирректории www/functions_cache/original_function_cached появится сериализованный результат выполнения функции original_function_cached и будет использоваться 10 часов.
Трудный
Как бы мы не кешировали результаты выполнения функций у нас всё равно останется ресурсоёмкий fetch, который собирает из десятка шаблонов, использующих десятки контроллеров и плагинов единую страницу.
Тут я бы предложил оптимизировать количество шаблонов, создать нормальную их иерархию(по-умолчанию все шаблоны хранятся скопом) и начать двигаться в сторону блочного кеширования. Таким образом на самых посещяемых страницах мы увидем немаленький прирост скорости.
Очень трудный
Но, если у вас, как и у меня нет иного выбора, кроме как работать с WA и работать вы с ним будете долго- это всё полумеры.
Необходима оптимизация, переписывание алгоритмов(посмотрите на досуге как у них реализована пагинация) и не хаковое кеширование. Вообще, мне в этом плане проще, так как я знаю, что новый контент добавляется автоматически в определенное время и в это время я могу себе позволить сбросить весь кеш. Вам же, чтобы бороться с инвалидацией цен, характеристик, придётся настраивать группы кешей и очень много менять(от формирования URL до реструктуризации деррикторий). В большинстве проблем, конечно, можно справиться с помощью smarty, работу программы с которым придётся здорово перестроить, так как сам WebAsyst SS кажется и не собирался использовать его механизмы кешировния(всё говорит об этом).
Например: мы закешировали всю страницу с товаром, и установили время жизни- 5 часов. Предполагается, что цена может поменяться и раньше, а сбрасывать кеш не очень хочется. Можно создать smarty-plugin, который обратится к нужному методу нужной модели(скажем $productModel->getPrice($pID)) и вернёт цену. На страницу с товаром мы получаем 1 запрос к БД. Кеш представления не перестраивается.
Заключение
Какая-то длинно получилось, но кажется, всё по-существу.
Надеюсь, что готовые решения и рекомендации из данной статьи натолкнут вас на что-то новое (будь то inclued или xDebug, или правило не верить на слово разработчикам, которые говорят, что все обращения к БД у них идут через класс) или помогут развить старые идеи.