Набираем обороты

12.02.2009

Введение

А зачем что-то оптимизировать? Ведь скорость интернет-трафика куда меньше, чем выполнение РНР программ… Да, действительно, протестировав сайт на хостинге, мы приходим к выводу, что все нормально. Но не тут-то было. Когда на сайт заходят несколько пользователей, уже чувствуется некоторый дискомфорт. А если их будет несколько тысяч? Нет, с этим нужно что-то делать, не ждать же по минуте загрузки одной страницы. Ведь дешевле сделать сразу качественный продукт, чем покупать новый сервер?

Итак, я расскажу общие принципы оптимизации, немножко затронута будет и MySql, как наиболее распространённая база данных для создания сайтов на сегодняшний день. Статья рассчитана на людей, знакомых с веб-программированием.

Замена медленных функций PHP

В арсенале PHP есть огромное количество функций, глядя на которые глаза разбегаются, особенно у начинающих. Самое непростое — это выбрать из них нужные под конкретную задачу. В интернете уже есть статья, посвященная проблемам скорости в РНР, я постараюсь обобщить и освежить достаточно устаревшие данные.

Работа со строками. Отмечу, что РНР 5 работает со строками гораздо быстрее. И, как это ни странно, двойные кавычки показывают самое быстрое время выполнения. То есть, написав $x="123 $test 123"; вы получаете самую высокую скорость. Очевидно, тут срабатывает PHP акселератор, который предварительно откомпилировав PHP-код, выдает уже отпарсенную строчку. Реальный вклад от таких манипуляций крайне невелик — около 1% (время обработки строки — десятитысячные доли секунды).
Вывод: программируйте так, как вам удобно. Исключениями могут стать ситуации, где строчка обрабатывается внутри тела цикла.
Правда, это не означает, что можно забить на оптимизацию функций обратотки вывода:). Вот несколько реальных советов, которые помогут писать быстрые скрипты, особенно если Вы часто пользуетесь функциями вывода (echo, printf и так далее):

  1. Используйте одинарные кавычки вместо двойных. Они работают быстрее, так как там не тратится время на поиск переменных, кроме того в одинарных кавычках нужно экранировать всего-навсего один символ: одинарную кавычку.Обратите внимание, что \n в одинарных кавычках тоже не работает.
  2. Функция echo работает быстрее своего аналога printf, так как в функциях printf и sprintf используется дополнительный парсер переменных.
  3. Если Вы выводите при помощи echo длинную строку, да еще и вперемешку с переменными, то гораздо разумнее перечислить все элементы через запятую, нежели разделяя их точкой. То есть лучше echo 'var: ', $var, '<br />', чем echo "var: $var<br />" (с точки зрения производительности, конечно).
  4. Не стоит выводить большие куски HTML кода при помощи функций вывода PHP. Лучше закрыть тег PHP, вывести HTML, затем снова открыть PHP. А ещё лучше использовать шаблонизатор (либо чужой, скажем, смарти, либо свой, но хороший).

Регулярные выражения. В большинстве случаев использовать перловые реги гораздо лучше (то есть preg_match и preg_replace вместо ereg), это как минимум двойной прирост производительности. А лучше совсем без них. Например, замена подстроки в строке гораздо быстрее выполняется функцией str_replace. Конечно, бывают задачи, в которых без регулярных выражений не обойтись, но я рекомендую сводить их применение к минимуму. Однако, не стоит изобретать велосипед, пытаться написать свои функции, заменяющие регулярные выражения — будет работать гораздо медленнее и потеряете кучу времени напрасно.
Вывод: для требовательных операций сложной обработки текста лучше использовать preg_replace.
Примечание. В последних версиях РНР больше нет функций ereg. Будьте внимательны и проверьте свои приложения, где эти функции использовались — теперь они работать не будут.

Перебор массива. Это очень важный момент в оптимизации, так как при переборе массива в цикле обычно выводят на экран содержимое страницы. И как это ни печально, самый главный ваш враг в циклах — это foreach и прочие each. Они тормозят достаточно сильно, в этом случае я рекомендую использовать цикл for, предварительно вычислив размер массива функцией sizeof. Однако, для ассоциативных массивов лучше использовать foreach (да и выхода-то другого нет:). Конечно, сильного прироста это не дает, так как основная доля времени уходит на echo и прочие операции в цикле, но чем их меньше, тем сильнее разница.
Выводите сложные функции из тела цикла. Лучше вычислить значение переменной вне цикла, а потом её просто подставлять. То есть предпочтительно написать так:

$max = sizeof($arr);
for ($i=0;$i<$max;$i++){...}

чем так:

for ($i=0;$i<sizeof($arr);$i++){...}

Чтение файлов. Тут многим придется пересмотреть свои взгляды на чтение файла. Конкретный пример — есть статья, расположенная в файле, нужно считать ее, выполнить какие-то действия, а затем вывести на экран. Способов три. Первый — банальный fopen + считывание построчно. Второй — file+implode, то есть считываем в массив затем проводим слияние. Третий — file_get_contents. Я честно признаюсь сам не ожидал, но file_get_contents работает в десятки раз быстрее любого другого варианта. Функция считывает файл в строку, очень удобна для подобных случаев. Про fopen пока придется забыть. При помощи file удобно работать со словарями, где в каждой строке находится словарная статья (или какая-то запись). Если в вашем коде очень много обращений к файлам, которые нужно просто прочитать и вывести на экран — используйте file_get_contents. Если нужно сразу разбить файл на массив построчно — хорошо подойдет file.
Однако, почему же file_get_contents работает значительно быстрее? Ответ прост: она использует системные вызовы и системный кэш файлов, таким образом файл (причастом к нему обращении) запишется в оперативную память и будет считываться оттуда.
Что касаетсязаписи в файл, то тут то же самое. Функция file_put_contents использует системый кэш, и грех нам им не воспользоваться тоже. Эта функция запишет файл сначала в оперативную память сервера, а сервер уже сам решит, когда ему сбросить файл на диск. Гораздо быстрее и лучше, нежели запись на диск напрямую.
Примечание. Автор может и ошибаться насчет функции fopen, возможно, она и использует кэш в некоторой конфигурации, но на моем сервере такого не наблюдалось точно.

А вот для считывания всего одной строчки из файла функции file и file_get_contents я использовать не рекомендую по простой причине: зачем читать весь файл, если нам нужны только некоторые данные? А файл может быть очень большим — только зря загружаем память сервера. Гораздо разумнее использовать fseek+fopen. Вот пример функции random_string, которая считывает нуагад одну строку из файла (пример взят из исходников CMS Scriptum):

function random_string($file)
{
//проверка файла на наличие на диске
if(file_exists($file))
{
//размер файл нужен, чтобы определить границы, выдаваемые функцией random
$size=filesize($file);
//генерируется случайная позиция в файле (не строка!)
$pos=rand(0,$size-1);
if($f=fopen($file,"rt"))
{
//заходим на позицию
fseek($f,$pos);
$str=fgets($f);
//если вдруг нас занесло в конец файла - перемещаем указатель на начало файла
if(feof($f))
rewind($f);
$str=fgets($f);
fclose($f);
return  $str;
}
}
else
return 'Файл с фразами не найден!';
}

Минусом такого алгоритма будет то, что строки будут выбираться чаще те, который длиннее. Плюсом - непревзойденная скорость.

Уменьшение размеров картинки. «Хитрый» метод резайса. Любая уважающая себя фотогалерея должна уметь сжимать картинки до определенного размера. В нашем распоряжении есть 2 функции из GD библиотеки — imagecopyresampled и imagecopyresized, причем из двух зол приходится выбирать меньшее: одна выдает качественную картинку, другая — работает раза в 3 побыстрее. Я не рассматриваю ImageMagick из-за тормозов во-первых и большого размера полученных jpeg-картинок во-вторых. Когда потребовалось создать фотогалерею, естественно, с резайсом картинок, я столкнулся с немалыми задержками при загрузке больших фотографий — около 0.5 сек на фото (тестирование проводилось на домашнем компьютере — Pentium 4 3GHz, 1 Gb RAM). Конечно, для одного человека это не так заметно, но предполагается еще и загрузка фотографий пользователями, а значит, при высокой посещаемости, сервер будет подвисать. Сам собой напрашивается вывод: использовать imagecopyresized. Эта функция не дает сглаживания, зато работает очень быстро. Один минус — выглядят полученные картинки не очень презентабельно, не хватает антиалиасинга.

Однако, из любой ситуации есть элегантный выход. Мы ведь можем использовать imagecopyresized только наполовину. То есть дана исходная фотография шириной, скажем, 3000 точек, а нужно получить миниатюру с шириной 150 точек. Сама идея заключается в том, чтобы «быстрой функцией» сжать ее до 300 точек (на эту операцию уйдет около 0.2 сек), а затем с получившимся изображением с легкостью расправится imagecopyresampled (менее 0.05 сек). В итоге, разбив операцию на 2 шага, я смог ускорить загрузку картинки в 2 раза. Потом можно настроить скрипт так, чтобы он слишком большие фотографии сжимал в 2 прохода, а маленькие — в один, так как при сжатии мелких фотографий эффективность вышеизложенного алгоритма падает. Минус: немного падает качество, но на мой взгляд не смертельно.

Грамотное использование базы данных

Все меньше сайтов хранят данные в текстовых файлах. Это естественно, ведь хостинг с MySQL стоит не так дорого, зато вы получаете богатые возможности и приличную скорость работы. Быстрее и удобнее хранить текстовые записи именно в БД. Одной из самый популярных БД для сайтов является MySQL, объясняется это просто: за такие деньги (бесплатно) вы получаете очень приличные возможности в совокупности с удобством использования, которых с лихвой хватает даже для крупных сайтов.

Использование ключей. Для быстрого поиска в базе данных обязательно используйте ключи. Если это id — то primaty key, если это некоторое поле, по которому нужно сортировать данные — то index. Чем больше ваша база тем острее необходимость в ключах. Однако, не стоит забывать, что много ключей в таблице приведет к низкой скорости добавления записей в БД.

Добавляйте INDEX к полям, которые будут использоваться для поиска значения сравнением на равенство. При использовании LIKE эффекта практически никакого не наблюдается. Второй случай, когда уместно использовать INDEX — сортировка или группировка. Если Вы не хотите, чтобы в каком-то поле записи не повторялись, создайте там ключ UNIQUE. Ключ можно задавать на несколько полей сразу, в качестве примера можно привести сортировку записей в базе по времени и дате добавления одновременно: создаем INDEX на оба поля сразу и все SELECT'ы, в которых есть сортировка по времени и дате работают значительно быстрее. Если в таблице используется полнотекстовый поиск — желательно поставить на текстовые поля индекс FULLTEXT, это будет работать намного быстрее лайка, когда база сильно вырастет. Расстановка ключей в таблице — целое искусство, которое нужно постигать чтением документации и самостоятельными экспериментами.

CHAR vs VARCHAR. Действительно, а чем они отличаются? Настало время раскрыть тайну. CHAR работает быстрее при UPDATE базы, но занимает много места (сразу резервируется место для хранения данных), а VARCHAR имеет переменную длину (то есть занимает меньше места) но работает медленнее. VARCHAR можно рассматривать как очень маленькое поле TEXT. Палка о двух концах. Если планируется частое обновление и место на диске не критично — лучше пользоваться CHARом. Обращаю Ваше внимание также на то, что если таблица переменной длины (присутствуют поля VARCHAR, BLOB или TEXT), то все CHAR будут типа VARCHAR. CHAR имеет смысл использовать только для таблиц постоянной длины (нет полей VARCHAR, BLOB или TEXT). Чтобы построить действительно статичную таблицу с CHAR'ами, нужно исключить из неё все поля типа BLOB, TEXT, VARCHAR. Их, например, можно вынести в другую таблицу.

Хранение файлов в БД. Это самая грубая ошибка. MySQL просто не приспособлена к хранению файлов в базе, особенно больших. Тормозит страшно, крайне не рекомендую пользоваться такой возможностью, хоть она и есть. Для хранения файлов используется специальный формат BLOB (для хранения бинарных данных).

Поиск в базе данных. Злой LIKE и тормозной REGEXP. Честно скажу, меня удивляет тенденция многих программистов палить из пушки по воробьям. Особенно был поражен, когда узнал что кто-то использует для поиска в базе REGEXP. Эта функция в MySQL осуществляет проверку по регулярному выражению, она проигрывает в скорости всем возможным методам поиска в БД. Так что не берите грех на душу, используя для поиска REGEXP. Вторая нехорошая функция — это LIKE. Да, знаю, ее везде используют. Даже на больших сайтах. Да, работает он достаточно быстро, но при небольших размерах базы. Но в любом случае — LIKE ваш враг номер два после REGEXP. Во-первых он умеет искать по маске (не зря у него такое название, переведите с английского). Это означает, что пользователь может в строку поиска забить что-то типа *abc*1_z, а * и _ для LIKE — служебные символы и он использует их как маску. Итог: зависший сервер. Запрос вида *_a_ на базе приличных размеров будет выполняться около минуты. Поэтому для LIKE нужно писать отдельно проверку, чтобы пользователь не начал составлять запросы по маске и ненароком не вогнал машину в ступор. Я лично использую функцию LOCATE, почему-то всеми забытую. Работает она порой даже быстрее и там нет спецсимволов для сравнения по маске - можно спать спокойно. Еще лучше простое сравнение =, но это только для поиска конкретного значения, а если нужно просто найти подстроку — для этих целей хорошо подходит LOCATE, описание этой функции можно найти на mysql.ru.

Но самый лучший способ что-либо найти на сайте — это использование MATCH на таблице с предварительно установленным индексом FULLTEXT. К Вашим услугам и подсчет релевантности, и скорость, и безопасность. У этого метода минус всего один: не учитываются словоформы русского языка, но это лечится патчами. Пример такого запроса: SELECT *, MATCH field AGAINST ('$searchwords') as relev FROM table ORDER BY relev DESC

Ограничение больших запросов. Если в результате запроса вы получаете слишком много результатов, это также существенно замедляет работу. Используйте LIMIT для разбиения слишком больших запросов на страницы, хороший тому пример — поисковые машины. Но следует учитывать, что показ десятка записей, начиная с тысячной будет также работать медленно. Лучше просто запретить просмотр «глубоко» спрятанных страниц, потому что рядовой пользователь их смотреть вообще не будет. Я позволяю смотреть только 100 первых записей, найденных в базе, а если результатов больше, просто прошу ввести более точный запрос. Если у вас получается очень много статей, то лучше их разбить по категориям. Помните: чем дальше записи к концу таблицы, тем медленнее они будут обрабатываться, это особенность БД MySQL.

Хранение одних и тех же записей в двух таблицах. Представьте себе обычную ситуацию: форум, несколько тысяч сообщений. Ежедневно добавляются сотни сообщений, просматриваются тысячи страниц, огромное количество запросов к базе данных. Выше уже было отмечено, что с ростом размеров таблицы, увеличивается время работы с ней. Иными словами, когда форум растет, он начинает все медленнее и медленнее работать. Значит, нужно спроектировать таблицы таким образом, чтобы это замедление не происходило.

Решение такой проблемы не ново и весьма эффективно. В нашем случае можно сыграть на том, что в форумах по большей части активно открываются только новые темы, то есть те, в которых появились новые ответы. Для этого создаем еще одну таблицу, которая дублирует основную, но в ней содержатся только темы с новыми ответами. Задается таймаут, скажем, неделя, в течение которого тема будет висеть во второй таблице. Если новых сообщений в течение недели не было, она автоматически удаляется из второй таблицы и переносится в основную. А при просмотре тем форума будет опрашиваться вторая таблица, если тема свежая или основная таблица, если тема старая. Так как старые темы мало кто читает, получаем очень замечательный эффект - с ростом базы данных форума не будет расти время ожидания загрузки страницы. Это можно применять не только для форума, неплохо будет так хранить новости, которые активно читаются.

Кэширование и сжатие

Кэш — самое мощное средство для борьбы с тормозами. Его придумали очень давно, еще задолго до появления Интернета, люди поняли, насколько кэш облегчает жизнь машине. Что касается РНР-приложений, то для кэша тут найдется много областей применения.
Например, php-eaxelerator. Замечательная маленькая программка, обычно уже стоит на всех серверах. Может ускорить сайт работу в 2 раза благодаря кэшированию функций.

Разумеется, для нединамических страниц очень хорошо подходит кэширование html-текста целиком. Идея состоит в том, что по md5 хэшу адреса страницы на сервере создавать файл, в котором хранится html код, и затем черпать его оттуда. Плюс: ускорение раз в 10. Минус: невозможно вставлять динамический контент, который обновляется при каждой перезагрузке страницы. Тут уж придется выкручиваться при помощи javascript и фреймов. Также небольшой минус: обновления появляются на сайте не сразу. Использовать такой «топорный метод» нужно с умом, а по возможности не использовать вообще.

На самом деле кэшировать можно и работу модулей, которые обновляются очень редко. Наверное, это — единственный выход при динамической генерации страницы.
Схема довольно похожа. Допустим, есть у нас на сайте новости. Новостей много, добавляются они не так уж и часто, зато при разбивке на страницы будут в любом случае возникать тормоза из-за некоторых особенностей MySQL. Решение: каждый сгенерированный список новостей сохраняем в виде HTML-кода на диск. Потом, при последующих обращениях, скрипт будет брать этот сгенерированный HTML-код и подключать его внутрь страницы. Таким образом реализовано кэширование конкретного модуля: новости. Кстати говоря, не нужно пытаться закэшировать модули, которые практически не замедляют работы сайта.

Запросы к БД сами по себе кэшируются, но можно объединять сложные запросы и заводить их в кэш, что сулит также немалое увеличение скорости.

Сжатие страниц — очень хороший способ уменьшить трафик между клиентом и сервером. Речь идет о GZip, библиотека входит в стандартный комплект РНР и молниеносно уменьшает размер страничек раз в 5.

Загружаем работой компьютер клиента, а не сервер

JavaScript также имеет право на жизнь, и позволяет он очень многое. Во всяком случае достаточно, чтобы заменить им некоторые функции PHP. Чем больше Вы загрузите работой компьютер клиента, тем лучше будет серверу и он вам только спасибо скажет. Например, не стоит с помощью РНР писать календарик, так как много циклов сразу затормозят работу. Смену баннеров тоже можно нагрузить на javascript. Не стоит писать на РНР игры, где требуется много вычислений, с которыми прекрасно справится и js.

© RPG

Назад
blog comments powered by Disqus