В начале 2014 года у нас было три VPS-сервера на Ubuntu 12.04 с Apache 2.2 и mod_php. Каждый обслуживал несколько сайтов. При пиковой нагрузке - более 200 одновременных соединений - серверы уходили в своп, появлялись 502-ошибки, и всё встававало.
Проблема была не в мощности железа. Проблема была в архитектуре Apache.
Почему Apache тормозит под нагрузкой
Apache в режиме prefork (стандартный при mod_php) создаёт отдельный процесс для каждого HTTP-соединения. Каждый такой процесс загружает интерпретатор PHP в память.
На сервере с 2GB RAM:
Один процесс Apache + mod_php: ~30-40MB
200 одновременных соединений × 35MB = 7GB нужно
Доступно: 2GB
Результат: своп, OOM killer, падение сервера
Nginx устроен принципиально иначе: один мастер-процесс запускает несколько воркеров (обычно по числу ядер CPU). Каждый воркер обрабатывает тысячи соединений через неблокирующий I/O. PHP-код при этом выполняется в отдельном пуле процессов PHP-FPM.
Установка и тест без остановки Apache
Сначала устанавливаем Nginx, не трогая работающий Apache:
# Apache остаётся на порту 80, Nginx ставим на 8080 для тестирования
apt-get install nginx php5-fpm
# Меняем порт Nginx на 8080 временно
nano /etc/nginx/sites-available/default
# listen 8080;
Тестируем на 8080, убеждаемся что всё работает, потом переключаем.
Конфигурация PHP-FPM
; /etc/php5/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
; Unix-сокет быстрее TCP на одном сервере
listen = /var/run/php5-fpm.sock
; Динамический пул процессов
pm = dynamic
pm.max_children = 25 ; Максимум PHP-воркеров
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500 ; Перезапускать воркер после 500 запросов (борьба с утечками памяти)
; Таймауты
request_terminate_timeout = 30s
request_slowlog_timeout = 5s
slowlog = /var/log/php5-fpm-slow.log
Правило для pm.max_children: берём доступную RAM для PHP и делим на средний размер PHP-процесса. Для нашего сервера: 1.5GB / 40MB ≈ 37, взяли 25 с запасом.
Конфигурация Nginx
# /etc/nginx/sites-available/mysite.com
server {
listen 80;
server_name mysite.com www.mysite.com;
root /var/www/mysite/public;
index index.php index.html;
# Сжатие
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 256;
gzip_comp_level 6;
# Статика - отдаём напрямую, без PHP
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# PHP-файлы → PHP-FPM
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 30;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
}
# Красивые URL для CMS и фреймворков
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Скрываем скрытые файлы (.git, .env и т.д.)
location ~ /\. {
deny all;
}
}
Переключение
# Останавливаем Apache
service apache2 stop
update-rc.d apache2 disable
# Возвращаем Nginx на порт 80
# Убираем временный listen 8080, ставим listen 80
# Запускаем
service nginx start
service php5-fpm start
# Следим за ошибками первые 10 минут
tail -f /var/log/nginx/error.log
Что сломалось при переезде
PHP-сессии. Apache и PHP-FPM запускаются под разными пользователями. Файлы сессий, созданные Apache, были недоступны PHP-FPM:
chown -R www-data:www-data /var/lib/php5/sessions
$_SERVER['REMOTE_ADDR'] возвращал 127.0.0.1 вместо IP клиента. Nginx проксирует запросы и передаёт реальный IP в заголовке X-Forwarded-For. Добавили в fastcgi_params:
fastcgi_param REMOTE_ADDR $remote_addr;
.htaccess - Nginx его не читает вообще. Все правила из .htaccess (редиректы, rewrite) пришлось вручную переписать в location-блоки Nginx. На это ушло около 3 часов на сайт.
Результаты
| Метрика | Apache + mod_php | Nginx + PHP-FPM |
|---|---|---|
| RAM в простое | 490MB | 90MB |
| RAM при 200 соединениях | 2.1GB (своп!) | 380MB |
| Статика, запросов/сек | 750 | 9200 |
| PHP-страницы, запросов/сек | 270 | 310 |
| Время ответа (среднее) | 190ms | 85ms |
Самый неожиданный результат - статика. Nginx отдаёт картинки и CSS без участия PHP в десятки раз быстрее. Нагрузка на сервер при одинаковом трафике упала на 60%.
Один VPS - несколько сайтов
Отдельный пул PHP-FPM на каждый сайт даёт изоляцию:
; /etc/php5/fpm/pool.d/site1.conf
[site1]
user = site1_user
listen = /var/run/php5-fpm-site1.sock
pm.max_children = 10
; /etc/php5/fpm/pool.d/site2.conf
[site2]
user = site2_user
listen = /var/run/php5-fpm-site2.sock
pm.max_children = 8
Если один сайт съест все свои воркеры - остальные продолжают работать. Под Apache с mod_php пул процессов был общим.
Принцип разделения веб-сервера и runtime приложения, который мы освоили в 2014-м, до сих пор актуален - только теперь это Docker-контейнеры вместо процессов.