История одного рефакторинга, или о сказ о том как не надо разрабатывать на PHP…
Началось все как всегда, откуда-то появляется заказчик весь в слезах и
трансе, орет не своим голосом: «Спасите, помогите, мой проект тормозит,
от меня пользователи бегут. У вас неделя...»
Развернули проект на нашем сервере — н-да уж… Дальше был анализ сего творения фирмы «Q», смех сквозь слезы, и истерика в
течении недели. Ну для начала был взят XDEBUG и посмотрели профайлером
что там и как (кликабельно):
Такую клевую штуку рисует Webgrind, удобно и наглядно
Т.е. у нас страница, которая выводит приветствие + выбор языка + выбор
категории для просмотра (захардкоденных кстати) генерировалась за 4,6
секунды на сервере с Core2 Quad CPU Q6600@2.40GHz/16Gb (истерика и лучи
ненависти конторе «Q»).
Куда более жутка картина ожидала нас на главной странице:
До применения Zend Framework'a нас отделяло еще несколько недель оптимизации г… кода…
FastTemplate такой «Fast»
Если присмотреться, то на предыдущем скрине видно, что очень дорого нам обходится некая функция parse_body:
Т.е. в самом начале мы ищем в шаблонах что-то вроде $HTTP_PATH,
заменяем это на @HTTP_PATH@, потом перебором пытаемся заменить все
известные переменные. Есть одна проблема — шаблонов у нас много, в них
используется совсем чуть-чуть из пары сотен переменных. Простая замена
str_replace на preg_replace_callback дала прирост в пару секунд (т.е.
около 10%).
Примеры кода
Слабонервным лучше пропустить данный абзац.
Классика плохого PHP кода, встречается у индусов и наших студентов:
function n() {
global $db, $CONSTANTS, $user, ...; // много одним словом
echo $CONSTANTS[HOMEPAGE_BANNER1_ID]; // думаете это константа?
echo $CONSTANTS[HOMEPAGE_BANNER2_ID]; // а знаете, что внутри?
echo $CONSTANTS[HOMEPAGE_BANNER3_ID]; // 1, 2, 3, которые меняются совсем не там где объявляются О_о
}
Использование файловой системы вместо системы контроля версий:
С базой данных тот же номер — таблицы categories_old, items_1 и т.д.
Если мальчик любит труд
тычет в книжку пальчик,
про такого пишут тут:
он хороший мальчик.
Это, как вы понимаете, не про наших «мальчиков», у наших 9 000 notices на главной. Да и manual'ами пользуются только слабаки:
// мы не читаем мануалов
while ($row = mysql_fetch_array($res)) {
if ($row) {
foreach ($row AS $key => $field) {
if (ereg("^[0-9]+", $key)) {
unset($row[$key]);
}
}
}
$rows[] = $row;
}
// если чуть-чуть допилить
// мелочь, конечно, но прирост ~0,1 сек т.к. имеет место 23162 вызовов
while ($row = mysql_fetch_array($res, MYSQL_ASSOC)) {
$rows[] = $row;
}
Далее просто гениальное решение, тайный смысл этого творения я не осилил:
$sqls[] = $sql;
if (is_array($sqls)) {
foreach ($sqls AS $ssql) {
if ($ssql) {
$res = mysql_db_query($db['name'], $ssql, $db['id']);
if (!$res) {
return 0;
}
}
}
} else {
return 0;
}
Подсчет результатов поиска, что может быть проще:
// перед выполнением запроса можно подсчитать кол-во результатов
// воспользовавшись функцией db_count (вызывается в 96 разных местах)
function db_count($sql) {
global $db;
$res = mysql_db_query($db['name'], $sql, $db['id']);
$result = mysql_num_rows($res);
return $result;
}
Постраничная навигация, и это тоже можем:
// пошел запрос к БД
$res = mysql_db_query($db['name'], $sql, $db['id']);
// подсчитали итого (хотя до этого уже был вызван db_count)
$row_count = mysql_num_rows($res);
// подсчитали сколько у нас страниц получается
$page_count = floor($row_count / $pager['per_page'] + 1);
// это offset
$bi = ($pos - 1) * $pager['per_page'];
// теперь выбираем только нужные записи
for ($i = $bi; $i < $bi + $pager["per_page"]; $i++) {
if ($i >= $row_count) {
break;
}
if (!mysql_data_seek($res, $i)) {
break;
}
if (!($row = mysql_fetch_assoc($res))) {
break;
}
// складируем результат
$new_rows[] = $row;
}
Повторение строк — мы не ищем легких путей (str_repeat):
// в файле categories.sql.php
// функция которая строит select для HTML
$offset_string = '';
for ($i = 1; $i < $rec['level']; $i++) {
$offset_string .= ' ';
}
Если нам надо обрезать строку на 100 символов, и при этом не кромсать слова то вот оно решение:
if ($i < strlen($description)) {
$data['row']['description'] .= "...";
}
У нас так много глобальных переменных, там есть конечно $db, и она же передается во все функции которые работают с БД:
function db_query($db, $sql) {}
function db_sql_query($db, $sql) {}
function db_count($db, $sql) {}
// и т.д.
// но почему не так, ведь у нас одна БД
function db_query($sql) {
global $db;
}
Пусть на море качка, но мы всегда прибережем обходные пути:
$sql = "SELECT id, name, pasw from users where name = '$_POST[username]'";
Про агрегирование в SQL мы не знаем:
// $rows - записи из БД
foreach ($rows as $value) {
$total += $value["price"];
}
Необходимо SEO URL? Не проблема:
switch ($params[1]):
case "usageagreement":
$page_id = 13;
break;
case "privacypolicy":
$page_id = 14;
break;
case "termsandconditions":
$page_id = 15;
break;
case "affiliates":
$page_id = 22;
break;
case "aboutus":
$page_id = 19;
break;
endswitch;
Ладно с PHP, но HTML то можно было подучить:
<!-- id такой id -->
<div id="banner">...</div>
<div id="banner">...</div>
<div id="banner">...</div>
<div id="banner">...</div>
<!-- class это почти style -->
<li class="padding-left:15px;">...</li>
<!-- табличная верстка -->
<!-- хотя не стоит 10 вложенных таблиц расписывать -->
<!-- margin, что такое margin? -->
Применение Zend_Cache
Кеш спасет мир, подумали мы и прикрутили его для всех SQL запросов (благо кто-то догадался написать единую функцию db_sql_query) и всех вызовов parse_body.
Для начала попробовали кешировать в файлы, на тестовом сервере это
помогло, на живом — нет. Причина — у нас так много мелких шаблонов
(~200 для главной), что операции с файловой системой свели на нет
прирост кеширования.
Вторая попытка оказалась более удачной, решили применить memcache —
прирост скорости ~180%. Какой клевый показатель, но верен лишь в 100%
попадании в кеш, таким образом перед нами вырисовывалась перспектива
полного рефакторинга системы.
Немного клиентской оптимизации
Ну что тут можно рассказать, простое добавление следующих правил в
.htaccess сильно облегчило навигацию пользователям, которые хоть раз
заходили на сайт:
<ifModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 seconds"
ExpiresByType text/html "access plus 1 seconds"
ExpiresByType image/x-icon "access plus 2592000 seconds"
ExpiresByType image/gif "access plus 2592000 seconds"
ExpiresByType image/jpeg "access plus 2592000 seconds"
ExpiresByType image/png "access plus 2592000 seconds"
ExpiresByType text/css "access plus 604800 seconds"
ExpiresByType text/javascript "access plus 216000 seconds"
ExpiresByType application/x-javascript "access plus 216000 seconds"
А далее все по порядку:
Оптимизация изображений для web (есть такой пункт в Photoshop) — 40% на всех JPEG фалах
Спрайты — кропотливая работа, десятки обращений к серверу можно свести к единицам
Жмем Javascript и CSS — ~50%
И последним пунктом — nginx для всего этого добра
Zend Framework
А теперь расскажу о том, как проект медленно переезжает на Zend
Framework. Начинается всё с простой проверки в index.php (о да в нашей
системе одна точка входа):
// список модулей, которые уже отрефакторили
$modules = array (
'/search/',
'/about/'
);
$path = $_SERVER['REQUEST_URI'];
// нас устроила такая простая проверка,
// но запрос вида /search/?... уже не будет обрабатываться
if(in_array($path, $modules)) {
// подключаем ZF (внутри стандартный код из сгенерированного public/index.php)
require 'loader.php';
exit();
}
// а эта будет
foreach ($modules as $module) {
if (strpos($path, $module) === 0) {
require 'loader.php';
exit();
}
}
Если у нас не одна точка входа, то в каждом файле, которые были затронуты делаем простую вставку:
Что-бы забыть «глобальный» ужас, жизненно-необходимые переменный были
закинуты в Zend_Registry (а в дальнейшем закинуты в конфигурационный
файл application.ini, где им самое место).
Так же Zend_Translate была скормлена таблица с переводами (см. адаптер array)
Результат
Сложно судить о результате, проект находится в разработке, но вот несколько сравнительных замеров:
UPDATE: Извините за тон статьи, но как-то накипело-то. Если
кому-то кажется, что цель статьи показать кто тут белый и пушистый, а
кто иной — то нет, основная задумка — поделиться опытом, показать
примеры «плохого» кода, и не имеет значения кто его написал, главное
чтобы каждый извлек для себя урок, и подобного кода становилось меньше…