Nginx 1.2 как front-end reverse proxy: статика летит, PHP-FPM не захлёбывается
Переносим нагрузку с Apache на Nginx 1.2 в роли reverse proxy: статика отдаётся вдесятеро быстрее, бэкенд не перегружается под всплесками трафика.
Nginx 1.2 (стабильная ветка 2012) набирает долю на рынке веб-серверов; всё больше используется как reverse proxy и балансировщик нагрузки.
Nginx давно не новость - он работает у нас в нескольких конфигурациях уже пару лет. Но до недавнего времени мы использовали его по-простому: отдать статику, проксировать один бэкенд. Когда в апреле вышла 1.2 как стабильная ветка, мы решили наконец довести до ума схему, которую откладывали: вынести Nginx полноценным front-end proxy перед Apache с PHP-FPM. На паре клиентских площадок результат оказался достаточно наглядным, чтобы написать про это.
Откуда задача
У одного из клиентов - небольшой интернет-магазин на самописном PHP - Apache крутился на той же машине и отдавал всё подряд: и картинки с CSS, и PHP-запросы. Схема работала годами, но под нагрузкой в пиковые часы начинались проблемы. Apache держит под каждое соединение процесс (prefork MPM), и когда трафик давал всплески, пул workers заканчивался быстрее, чем медленные PHP-запросы отрабатывали. Страницы начинали тормозить или вовсе не открывались.
Решение прямолинейное: поставить Nginx перед Apache, отдать всю статику через Nginx напрямую, а динамику проксировать на Apache с PHP-FPM только тогда, когда это действительно нужно.
Схема
Клиент -> Nginx (80/443) -> [статика: /css/, /js/, /img/ отдаёт Nginx]
-> [динамика: /index.php и остальное] -> Apache (127.0.0.1:8080) -> PHP-FPM
Apache перевели на локальный порт 8080, закрыли его от внешнего мира через iptables - снаружи только Nginx. Это важно: иначе часть трафика пойдёт мимо прокси, и картина будет нечистой.
Конфиг Nginx для этой схемы несложный:
server {
listen 80;
server_name example.ru;
root /var/www/example;
# Статика - отдаём напрямую, без Apache
location ~* \.(css|js|png|jpg|gif|ico|woff|svg)$ {
expires 30d;
access_log off;
}
# Всё остальное - проксируем на Apache
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
}
Отдельно настроили буферизацию ответов от бэкенда: proxy_buffer_size, proxy_buffers. Это позволяет Nginx принять ответ от Apache целиком и отдавать его клиенту в своём темпе - даже если клиент медленный или на мобильном соединении, Apache-worker освобождается быстро.
Что получилось
Статику Nginx отдаёт принципиально быстрее - это ожидаемо, и здесь без сюрпризов. Картинки, CSS и JS перестали занимать Apache-workers совсем. Под нагрузочным тестом с ab (Apache Benchmark) разница по статике - порядок. Но это не главный эффект.
Главный эффект - поведение под всплеском. Nginx держит тысячи одновременных соединений на event loop без выделения отдельного процесса на каждое. Медленный клиент, который скачивает страницу по байтику, занимает у Nginx копейки памяти, а не целый Apache-процесс. В итоге бэкенд получает только те запросы, которые реально требуют PHP, и получает их равномерно - без того, чтобы сотня клиентов одновременно ломилась в PHP.
Буферизация ответов - ещё один практический бонус. Если PHP генерирует ответ за 200 мс, но клиент забирает его 3 секунды, то без прокси Apache-worker занят все 3 секунды. С Nginx в схеме: Apache отдал ответ за 200 мс, Nginx забрал его в буфер, Apache-worker свободен, а Nginx дальше сам работает с клиентом.
Несколько граблей
X-Forwarded-For обязательно. В Apache после перехода все обращения стали приходить с IP 127.0.0.1. Логи сломались, IP-ограничения в приложении сломались. Без proxy_set_header X-Real-IP и X-Forwarded-For это типичная первая ошибка - приложение надо научить смотреть на заголовок, а не на REMOTE_ADDR. В PHP это либо код приложения, либо переменная в Apache: SetEnvIf X-Forwarded-For "^(.+)$" REMOTE_ADDR=$1.
Apache на 0.0.0.0:8080 - это дыра. Убедились, что после перенастройки порта Apache слушает только на 127.0.0.1. Iptables закрывает 8080 снаружи - это второй рубеж, но слушать на localhost правильнее.
Кэш статики и заголовки. Добавили expires 30d для статических файлов - браузеры перестали дёргать сервер за каждой иконкой. Мелочь, но в совокупности ощутимо.
Балансировщик - на этой площадке один бэкенд, и схема upstream с несколькими серверами осталась за рамками. Nginx 1.2 это поддерживает с минимальной конфигурацией. Для клиентов, у кого два PHP-сервера, это следующий шаг.
Где мы сейчас
На двух площадках схема работает третью неделю, мониторинг не показывает аномалий. Пиковые всплески трафика бэкенд переживает спокойно - Apache-workers не заканчиваются. Клиент, у которого раньше в пятницу вечером сайт начинал тормозить, пока доволен.
Задача поддержки инфраструктуры в таких случаях - не просто «поставить Nginx», а убедиться что приложение корректно работает за прокси, логи пишутся правильно и мониторинг видит реальные IP. Это занимает больше времени, чем сама установка.
Следующий кандидат на такую же схему - площадка с 1С-сайтом, но там своя специфика с сессиями, которую надо проверить отдельно.