Советы по оптимизации веб приложений, настройка сервера

Unix

Автор: Александр Степанов

1 нояб. 2011 г., 09:15:10  1521


Доброго времени суток.
В этой статье я немного расскажу про оптимизацию PHP, начиная от железа и настроек веб-сервера и заканчивая приёмами в самом коде.
Итак, начнём.

Сеть

Пожалуй, самое узкое место любого крупного веб-ресурса — это сеть. Представим картину — сервер с каналом 10Мбит/с и страница, размером в 10КБ. Такой сервер сможет обработать чуть больше 100 запросов в секунду (учитывая HTTP заголовки). Избегайте в вебсервере и в скриптах лишней работы с сетью.
Настоятельно рекомендую выключить опцию HostNamesLookup в настройках Apache. При этом сервер будет в логи записывать IP адреса клиентов, не пытаясь получить обратную DNS запись для них (имя хоста).
Так же, порой, лучше применять сжатие (GZIP, Deflate) для передаваемых страниц. В случае с хорошим процессором и медленной сетью, это существенно поднимет производительность.
P.S. Избегайте работы с базами данных по сети (для этого есть опция skip-networking в конфигурационном файле MySQL). Работа с базой должна происходить через UNIX Socket.

Процессор

Процессор для сервера нужно выбирать исходя из типа его загрузки. Если запускается много разных процессов (или потоков) Apache, которые более или менее равномерно нагружают процессор, то в такой ситуации себя лучше покажет многоядерный процессор и система сконфигурированная с SMP. Если наблюдается иная картина — один процесс (например, MySQL) нагружает процессор на 80% и более — то в такой ситуации лучше себя покажет одноядерный процессор, т.к. у него частота больше, чем у одного ядра двухъядерного процессора, а система не сможет распараллелить один процесс между ядрами.
 

Оперативная память

Настройки веб сервера, касаемые количества одновременно запущенных процессов и максимального количества одновременно обрабатываемых подключений должны подбираться экспериментально. Должен быть оценен объём памяти, расходуемый на обработку одного запроса и общее количество памяти должно быть разделено на этот объём. В результате, получаем число, равное количеству одновременных запросов, которое может обработать сервер. Крайне не рекомендуется разрешать веб серверу обрабатывать большее количество подключений, т.к. в случае перегрузки сервера запросами (например, ДДоС атаки), данные будут храниться в SWAP области на диске, что приведёт к значительному возрастанию нагрузки на процессор и жёсткий диск.
 

Настройки веб сервера

Первое, и самое главное — веб сервер должен работать только в StandAlone режиме. Забудьте про inetd для веб сервера. При inetd режиме серверу приходится каждый раз читать конфигурации и т.п., на что уходит достаточно много драгоценного процессорного времени. Так же, существенно поднимает производительность использование Keep-Alive режима. При этом несколько HTTP запросов могут обрабатываться сервером в пределах одного соединения с клиентом. Т.е. не нужно будет тратить время на установку нового соединения для каждого запроса.
Некоторые параметры Apache:
MaxClients — максимальное количество одновременно обрабатываемых запросов.
StartServers — количество процессов сервера, запускаемых при его включении.
MinSpareServers — устанавливает желательное минимальное число неиспользуемых дочерних процессов сервера.
MaxSpareServers — устанавливает желательное максимальное число неиспользуемых дочерних процессов сервера. Если запущено больше чем MaxSpareServers неиспользуемых процессов, то родительский процесс убьет лишние.
MaxRequestsPerChild — максимальное количество запросов, которое может обработать один процесс (после чего он будет перезапущен).
KeepAlive — включает или выключает поддержку Keep-Alive соединений.
KeepAliveTimeout — время в секундах, по истечению которого соединение Keep-Alive будет прервано (при неактивности клиента).
MaxKeepAliveRequests — максимальное количество запросов, которое может быть обработано в пределах одного Keep-Alive соединения.
TimeOut — таймаут неактивности (в секундах). Соединение с сервером будет прервано, если за это время не было передано ниодного байта ни в одну сторону.
 

Настройки PHP

Для начала, необходимо правильно настроить memory_limit — ограничение памяти для скрипта. Как правило, редко нужно больше 16МБ памяти для одного скрипта. Ограничение времени работы скрипта (max_execution_time) служит, в основном, для предотвращения зацикливаний в скриптах. Разумно было бы оставить стандартное значение в 30 секунд, а в скриптах, которые должны работать дольше — вручную задать новый лимит функцией set_time_limit().
С осторожностью стоит относиться к безопасному режиму (safe_mode) и open_basedir. При правильно настроенном веб сервере (с разграничением пользователей) эти настройки не нужны, а скорее и наоборот, мешать будут. А если все скрипты выполняются от одного имени, то open_basedir можно обойти и получить доступ к чужим файлам. Т.к. при условии что скрипты выполняются от разных пользователей, никто не сможет сделать в системе ничего большего, чем то, на что у него есть права. Классическим примером разделения пользователей является mpm_itk в Apache 2.x.
Есть ещё один интересный параметр — disable_functions, служит для запрета вызова тех или иных функций. Это уже на ваше усмотрение. Лично я предпочёл оставить этот параметр пустым. Не вижу смысла запрещать пользователям функции, если скрипты выполняются от их имени.
magic_quotes… Сколько о них разговоров было. В принципе, для граммотно написанного скрипта этот параметр ничего не меняет. magic_quotes_gpc автоматически экранирует «опасные символы» в параметрах, переданных скрипту от клиента, magic_quotes_runtime — экранирует «опасные символы» при чтении данных из внешних источников (файлы, сокеты, база данных). Крайне советую избегать этой функции в PHP.
output_buffering — тоже интересная настройка. Указанное здесь количество байт будет буфферизироваться перед отправкой клиенту. Оптимальное значение — 2048-4069 байт.
expose_php — указывает на необходимость передавать в HTTP заголовках информацию о том, что страница сгенерирована с помощью PHP. По умолчанию включен, однако я предпочёл выключить передачу этой информации. На производительность и безопасность, в общем, не влияет.
error_reporting — уровень вывода предупреждений и ошибок. По умолчанию равен E_ALL&~E_NOTICE, т.е. выводятся все ошибки, кроме Notice (думаю, что логический опреатор «И НЕ» знаком всем...). Для продуктивных целей и лучшей безопасности лучше отключить вывод всяческих ошибок, однако сохранять ошибки в лог. Я, например, использую syslog для этого.
register_globals — регистрировать ли в скрипте переданные параметры как переменные? Отрицательный ответ напрашивается сам собой. Это может нарушить безопасность скриптов (однако, если скрипт написан граммотно, то этот параметр ни на что не влияет). Но, на удивление, многие скрипты написаны таким образом, что требуют, чтобы эта функция была включена...
Касаемо других оптимизаций — крайне желательно использовать оптимизаторы для PHP, такие как eAccelerator, Zend Optimizer и другие. Самым популярным является Zend Optimizer. Тесты производительности я не проводил.
 

Оптимизации PHP кода

Много статей было на эту тему… Выделю самое главное, на чём реально можно поднять производительность и то, что меньше всего описанного в других статьях влияет на производительность.
Первое, что я хочу выделить:
1) Необходимо задавать максимальное количество проходов цикла for до цикла, а не во время его выполнения. В противном случае, как на примере, функция strlen() будет вызвана 20 раз:
 

$str = 'abcdefghijklmnopqrst';
for ($i = 0; $i < strlen($str); $i++) echo ord($str[$i])." ";


Правильнее было бы делать так:

$str = 'abcdefghijklmnopqrst';
$n = strlen($str);
for ($i = 0; $i < $n; $i++) echo ord($str[$i])." ";

Если, конечно, в цикле не меняется исходная строка...
2) По возможности, стоит избегать регулярных выражений. Стандартные функции работы со строками работают гораздо быстрее. Но при необходимости регулярных выражений, стоит использовать PCRE вместо POSIX. Причин на то две — они работают быстрее и безопасны для обработки бинарных данных.
3) Не следует использовать лишние переменные, там где они не нужны. Например:

$sql = "SELECT `a`,`b`,`c` FROM `table` WHERE `b` IN ('1','2','3')";
$q = mysql_query($sql);

Гораздо разумнее было бы сделать так:

$q = mysql_query("SELECT `a`,`b`,`c` FROM `table` WHERE `b` IN ('1','2','3')");

Если, конечно, не нужно после запроса выводить его.
4) Очищайте память от ненужных переменных вызовом unset(). Используйте mysql_free_result() для очистки памяти после SQL запроса (вызов этой функции можно опустить, если это конец скрипта, т.к. после выполнения скрипта память освобождается автоматически).
5) Никогда не используйте конструкции вида $array[index] (если index — не константа, а название индекса массива). Подобные конструкции замедляют код и вызывают ошибки Warning...
Следующие пункты незначительно влияют на оптимизации
1) echo быстрее, чем print. Полностью согласен, т.к. print — это функция и ей нужно дополнительное время для возвращения результата выполнения. Однако, сильно на производительность это не влияет. И, кроме этого, желательно вызывать echo один раз при отдаче данных клиенту, а до этого собирать данные в строку.
2) Конструкции вида echo $var.' some string'; работают немного быстрее, чем echo "$var some string";
Так же, строки, заключённые в одинарные кавычки обрабатываются немного быстрее, чем в двойных кавычках, т.к. PHP не выполняет поиск переменных и спец. символов (\r \n \t \0 и символов в виде \x[NN], где [NN] — шестнадцатеричный код символа). Больше всего это касается старых версий PHP. На мой взгляд, здесь выиграш процессорного времени будет минимальным.
3) Подавление ошибок символом @ в коде медленнее, чем error_reporting(0). В общем, вполне разумный совет и по другим причинам. Не нужно задумываться, какие строки в коде могут вызвать ошибку (например, fopen при попытке открыть на чтение несуществующий файл или fsockopen при таймауте).
4) Прединкремент и декремент выполняются немного быстрее, чем постинкремент и декремент, соответственно.
5) Пожалуй, самый нелепый приём оптимизации, который я встречал.

if (strlen($foo) < 5) { echo "Foo is too short"; }

медленнее, чем

if (!isset($foo{5})) { echo "Foo is too short"; }

Может, второй вариант и будет работать быстрее, но и код скрипта должен быть читаемым… Без комментариев, думаю...
Несколько приёмов, которые можно делать при запуске скрипта:
error_reporting(0); — выключить вывод ошибок (или задать новый уровень вывода ошибок).
set_time_limit(0); — убрать ограничение по времени для скрипта (если скрипт должен выполняться долго). Однако, не стоит злоупотреблять этим. Лучше расчитать по максимуму, сколько скрипт может выполняться и задать нужный лимит. Это не даст положить сервер в случае случайного зацикливания в скрипте.
ini_set('memory_limit','-1'); — выключить лимит памяти для скрипта. Так же, вместо -1 можно задать выделенный объём памяти с суффиксами K и M (килобайты и мегабайты, соответственно)
set_magic_quotes_runtime(0); — выключить magic_quotes_runtime в пределах одного скрипта.
ignore_user_abort(1); — предотвратит прекращение выполнения скрипта, в случае если пользователь остановил загрузку страницы. Весьма полезная вещь, т.к. бывает нужно, чтоб скрипт отработал до конца, записал все данные в логи, удалил все временные файлы и т.п.
Так же, против magic_quotes_gpc можно использовать такой код: 

if(get_magic_quotes_gpc() || (ini_get("magic_quotes_sybase") && (strtolower(ini_get("magic_quotes_sybase")) != "off"))) {
    $_GET = array_map("stripslashes",$_GET);
    $_POST = array_map("stripslashes",$_POST);
    $_COOKIE = array_map("stripslashes",$_COOKIE);
}

Если через GPC не передаются массивы. В противном случае, можно использовать array_walk_recursive (доступно только в PHP5) или написать свою маленькую функцию, для рекурсивной обработки элементов массива.

За сим, пожалуй, всё. Желаю удачи в ваших веб разработках.
С вами был Eugen && gibson.