Как стать автором
Обновить

Подход к оптимизации приложения на примере популярной CMS

PHP *

Статья может помочь вашему живому проекту не тормозить(или тормозить меньше), либо стать отправной точкой для исследования стороннего продукта.
Например, у вас стоит задача понять что же происходит внутри «Самописная система 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.

Варианты решения проблем


Лёгкий


Если у вас не так много посетителей, но программа тормозит можно воспользоваться файловым кешированием с минимальным временем интеграции.
Я предлагаю такую схему:
  1. Находим тяжелую функцию
  2. Определяем, не зависит ли она от каких-либо глобальных переменных
  3. Переименовываем её во что-то вроде {original_function}_cached
  4. Создаём {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, или правило не верить на слово разработчикам, которые говорят, что все обращения к БД у них идут через класс) или помогут развить старые идеи.
Теги:
Хабы:
Всего голосов 54: ↑36 и ↓18 +18
Просмотры 3.5K
Комментарии Комментарии 70