Итого, мы имеем 3 узла в нашей схеме:
1. Web-сервер;
2. PHP;
3. СУБД.
Каждый узел может быть тем "угольным ушком", из-за которого будет "тормозить" вся система. Вдобавок ко всему, взаимодействие между узлами также может существенно влиять на производительность.
Важно помнить, перефразируя феномен "Hockey Stick", 30% "угольных ушек", создают 70% проблем с производительностью!
Итак, мы имеем множество проблемных точек, выявление которых на "боевом" сервере является нетривиальной, а порой, невозможной, бьющей по карману и нервам, задачей. Чтобы избежать такую ситуацию, оптимизация всех уровней системы должна начинаться еще на этапе проектирования и продолжаться "всю жизнь" системы.
В данном цикле статей будут максимально полно, с примерами реально работающих конфигураций, освещены все (известные автору) аспекты оптимизации.
Оптимизация Web-сервера
Если Вы задумались об оптимизации на этапе проектирования, значит, что, согласно бизнес-плану, на проектируемый ресурс планируется большой наплыв пользователей. Мало того, скорее всего, известно примерное количество одновременных посетителей. Если количество последних измеряется сотнями, то рекомендую начать с оптимизации ядра/стека TCP/IP.
Ниже дается список команд с пояснениями.
Если ядро собрано с CONFIG_SYNCOOKIES для защиты от syn флуда (net.ipv4.tcpsyn_cookies)
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
Увеличиваем размер backlog очереди (аналог sysctl net.ipv4.tcp_max_syn_backlog).
echo 1280 > /proc/sys/net/ipv4/tcp_max_syn_backlog
Какие порты использовать в качестве локальных TCP и UDP портов (sysctl net.ipv4.ip_local_port_range).
echo "16384 61000" > /proc/sys/net/ipv4/ip_local_port_range
Сколько секунд ожидать приема FIN до полного закрытия сокета.
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
Как часто посылать сообщение о поддержании keep alive соединения.
echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time
Сколько пакетов проверки keepalive посылать, прежде чем соединение будет закрыто.
echo 2 > /proc/sys/net/ipv4/tcp_keepalive_probes
Запрещаем TCP window scaling (net.ipv4.tcp_window_scaling)
echo 0 > /proc/sys/net/ipv4/tcp_window_scaling
Запрещаем selective acknowledgements, RFC2018 (net.ipv4.tcp_sack).
echo 0 > /proc/sys/net/ipv4/tcp_sack
Запрещаем TCP timestamps, RFC1323 (net.ipv4.tcp_timestamps)
echo 0 > /proc/sys/net/ipv4/tcp_timestamps
Увеличиваем размер буфера для приема и отправки данных через сокеты.
echo 1048576 > /proc/sys/net/core/rmem_max
echo 1048576 > /proc/sys/net/core/rmem_default
echo 1048576 > /proc/sys/net/core/wmem_max
echo 1048576 > /proc/sys/net/core/wmem_default
Если не требуется форвадинг пакетов (машина не рутер) выключаем.
(net.ipv4.ip_forward и net.ipv4.conf.all.forwarding)
echo 0 /proc/sys/net/ipv4/ip_forward
echo 0 /proc/sys/net/ipv4/conf/all/forwarding
Через какое время убивать соединеие закрытое на нашей стороне (net.ipv4.tcp_orphan_retries)
echo 1 > /proc/sys/net/ipv4/tcp_orphan_retries
Очень хорошая и познавательная статья http://www.kegel.com/c10k.html поможет разобраться в проблематике обслуживания нескольких тысяч соединений.
Из железа еще хочется обратить внимание на скорость чтения/записи RAID-массива и на количество оперативной памяти. Долго останавливаться не хочу на этом, просто
отмечу, что RAID уровней 0 и 1 практически сравнялись по быстродействию, а оперативной памяти много не бывает.
Реально работающая конфигурация: RAID 10, 2 гигабайта RAM (на каждом сервере Web кластера).
На этом закончим подготовительный этап и займемся установкой и настройкой ПО.
Как Вы знаете, наиболее распространённый веб-сервером на сегодня является Apache (по данным компании Netcraft http://survey.netcraft.com/Reports/200806/). Основными достоинствами Apache считаются надёжность и гибкость конфигурации. Он позволяет подключать внешние модули для предоставления данных, использовать СУБД для аутентификации пользователей, модифицировать сообщения об ошибках и т. д. Его мощь и есть его самый главный неостаток. Т.е. при обработке одного обращения от браузера (в рамках одного обращения к одной странице может быть бесконечное множество обращений от браузера к серверу) задействуется один процесс Apache, который "подтягивает" за собой все модули которые нужны и не нужны. Вследствие этого увеличивается время отдачи одного элемента страницы, расходуется оперативная память и расходуется пул свободных процессов на задачи далекие от тех, которые должен решать Apache.
Например: на странице 50 маленьких и не очень картинок, после загрузки HTML-кода, браузер загружает контент. Он 50 раз запросит Apache о картинках, чем займет минимум (зависит от настроек) один процесс. В лучшем случае, настроено кэширование, картинка уже закэширована на стороне клиента и сервер вернет ответ 304 Not Modified. В худшем, клиент начнет с медленного терминала (диалап или GPRS) скачивать картинки, чем надолго займет процесс сервера. Представим ситуацию, когда у Вас сотни одновременных клиентов, добрая часть из которых "медленные". Думаю, как бы "тонко" не был бы настроен Apache, все равно часть клиентов "подвиснут" в ожидании очереди на обслуживание.
Выход есть, он не один, но приведу именно тот, кому я отдаю предпочтение.
"Легкий" и быстрый HTTP-сервер Nginx (Engine X)
Его плюсы:
- быстрое обслуживание статических запросов;
- акселерированное проксирование без кэширования;
- простое распределение нагрузки и отказоустойчивость;
- поддержка удаленных FastCGI серверов;
и многое другое...
Его минусы:
- не умеет (да это ему и не надо) работать с PHP-скриптами
и все...
Правильная реализация нашего примера:
ставим Nginx, как фронтенд к серверу Apache, последний, соответственно, становится бэкендом. Проксируем все запросы к PHP на бэкенд, а всю статику отдаем с помощью фронтенда.
Это нам позволяет разгрузить неповоротливый Apache. Теперь его процесс не ждет, когда клиент закончит принимать контент с сервера, а сразу отдает все, что должен передать, на Nginx, а тот в свою очередь, в силу своей "легковесности", может долго ждать медленного клиента и не нагружать память своими модулями. В это самое время, процесс Apache уже готов к приему и обработке новых запросов.
Для справки: у Nginx на 10 000 неактивных HTTP keep-alive соединений расходуется около 2.5M памяти.
Второго зайца мы убили, когда освободили бэкенд от необходимости отдавать статику, теперь это работа фронтенда, который с ней успешнее справляется.
Далее, по пунктам перечисляем преимущества новой схемы:
1. Проксирование запросов на несколько бэкендов.
Nginx прекрасный балансировщик нагрузки, также он позволяет выключать сервера из кластера, не прекращая предоставлять услугу/доступ к проекту.
2. Сжатие отдаваемого контента.
Модуль ngx_http_gzip_module - это фильтр, сжимающий ответ методом gzip, что позволяет уменьшить размер передаваемых данных в 2 и более раз.
3. Сжатие отдаваемых файлов.
Модуль ngx_http_gzip_static_module позволяет отдавать вместо обычного файла предварительно сжатый файл с таким же именем и с суффиксом ".gz".
4. Кэширование.
Модуль ngx_http_headers_module позволяет выдавать строки "Expires" и "Cache-Control" и добавлять произвольные строки в заголовке ответа.
5. Методы обработки соединений.
Nginx позволяет установить подходящие под Вашу систему, оптимальные, методы установки соединений, которые можно задать директивой use.
Перечислены, на мой взгляд, самые важные пункты, на которые следует обратить внимание при настройке фронтенда Nginx.
Ниже представляю конфигурацию Nginx для проксирования запросов нескольким бэкендам и обработки статики.
events {
worker_connections 2000;
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
gzip on; #включаем п.2
gzip_static on; #включаем п.3
gzip_disable "MSIE [1-6]\."; # на старых IE не работает сжатие
gzip_min_length 1100;
gzip_types text/plain;
output_buffers 1 32k;
postpone_output 1460;
keepalive_timeout 35;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# распределение нагрузки на 2 сервера
# unix socket работает быстрее
upstream backend
{
server unix/socket1;
server IP-второго бэкенда:порт;
}
server {
listen внешний IP:порт;
location / {
#передаем управление балансировщику нагрузки, тот потом Apache-серверам
proxy_pass http://backend;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 64k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
charset UTF-8;
}
#тут мы обрабатываем статику, т.е. отдаем ее напрямую и устанавливаем время жизни ей 30 дней
location ~*
^.+\.(jpg|jpeg|gif|css|js|png|tiff|html|htm|flv|gz)$
{
root /var/www;
expires 30d;
}
}
}
Промежуточные результаты:
- скорость отдачи данных увеличилась;
- количество "свободной" ОП увеличилось, следственно наш сервер может обработать большее количество одновременных соединений;
- внедрены базовые функции кэширования для статики;
- благодаря предыдущему пункту, экономится трафик (деньги) пользователей и выросла скорость получения требуемой страницы.
FastCGI
Теперь зададимся вопросом, а зачем нам вообще этот монстр под названием Apache? Неужели только для того, чтоб обрабатывать PHP мы должны его "терпеть" на своих серверах?
Конечно, если бэкенд только обрабатывает PHP-скрипты, без него можно, даже нужно, обойтись.
Итак, схема остается прежней, только вместо бэкенда теперь нам нужно, что-то более легкое и быстрое. Nginx позволяет проксировать запросы на удаленный FastCGI-сервер. Нам нужно запустить PHP в режиме FastCGI.
Для справки: Интерфейс FastCGI - это быстрейший и наиболее безопасный способ обработки запросов внешними программами, такими как Perl, PHP, а также вашими самописными приложениями.
FastCGI ликвидирует множество ограничений CGI-программ. Проблема CGI-программ в том, что они должны быть перезапущены веб-сервером при каждом запросе, что приводит к понижению производительности.
FastCGI убирает это ограничение, сохраняя процесс запущенным и передавая запросы этому постоянно запущенному процессу. Это позволяет не тратить время на создание (fork()) новых процессов.
(По материалам Википедии)
Итак, есть 3 способа запуска PHP в режиме FastCGI.
1. Запустить PHP с использованием его встроенного менеджера FastCGI-запросов;
2. Использовать утилиту spawn-fcgi от другого легковесного Web-сервера lighttpd;
3. Использовать патч php-fpm.
Мы будем использовать последний способ(PHP FastCGI Process Manager), т.к. он наиболее функционален, отказо- и нагрузоустойчив.
Возможности php-fpm:
- Управление процессами. Возможность "плавно" останавливать и перезапускать php воркеры без потери запросов. Возможность плавно обновлять конфигурацию и binary без потери запросов;
- Ограничение ip адресов, с которых могут приходить запросы от web сервера;
- Динамическое количество процессов, в зависимости от нагрузки;
- Запуск воркеров с разными uid/gid/chroot/environment и разными php.ini опциями;
- Логирование stdout & stderr рабочих процессов;
- Аварийный перезапуск всех процессов при случайном разрушении shared memory opcode cache, если используется акселератор;
- Принудительное завершение подвисших процессов, если set_time_limit() не срабатывает.
Установка php-fpm
Нужно убедиться, что в системе установлен пакет libxml2 и libxml2-devel.
Затем скачиваем последний php-fpm(рассматривается для версии php-5.2.6)
$ wget http://php-fpm.anight.org/downloads/head/php-5.2.6-fpm-0.5.8.diff.gz
$ bzip2 -cd php-5.2.6.tar.bz2 | tar xf -
$ gzip -cd php-5.2.6-fpm-0.5.8.diff.gz | patch -d php-5.2.6 -p1
$ cd php-5.2.6 && ./configure --enable-fastcgi --enable-fpm
$ make all install
Далее нужно отредактировать файл php-fpm.conf. На этом останавливаться не буду, т.к. там все просто и понятно.
Теперь наш PHP может запускаться в режиме FastCGI с возможностью управления процессами!
Для запуска FastCGI бэкенда нужно ввести команду из директории, в которую устновился php-fpm. $ ./bin/php-cgi --fpm
Проверим, сколько процессов запустилось $ ps aux|grep php-cgi
обратите внимание на то, сколько процентов памяти заняли эти процессы - ничтожно мало! То, чего мы и добивались, осталось настроить Nginx для работы с данным бэкендом.
В раздел server файла конфигурации nginx.conf добавляете новый location
location ~ .php$ {
#указываете адрес и порт который вы указывали в php-fpm.conf
fastcgi_pass 127.0.0.1:8888;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /path/to/html$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}
Всё, теперь все запросы на странички а-ля somepage.php будут обрабатываться FastCGI бэкендом.
Apache можно выключить!
Итоги статьи:
- мы настроили стек TCP/IP для обработки большого количества одновременных подключений;
- научились настраивать быстрый, легкий и масштабируемый фронтенд Nginx;
- мы оптимизировали отдачу статики, разгрузили бэкенд;
- научились экономить трафик наших дорогих посетителей;
- научились распределять нагрузку на несколько бэкенд-серверов;
- наконец, мы отказались от использования Apache, в пользу php-fpm быстрого, легкого и отказоустойчивого патча к PHP;
- увеличили предыдущим пунктом количество свободной ОП, тем самым, увеличив возможность обработать большее количество запросов одновременно.

В следующей статье:
1. Оптимизация PHP-кода;
2. Кэширование;
3. Framework-и плюсы и минусы;
4. Акселераторы, обзоры и тесты.