Оптимизация LEMP-стека:настройка Nginx, PHP-FPM, MySQL для высоких нагрузок
Введение
Настройка LEMP-стека «из коробки» сделаны для удобства разработки, а не для работы на реальном сайте (в продакшене). Nginx после установки уже работает, стратегия управления совокупностью процессов PHP-FPM по умолчанию консервативна, а MySQL выделяет под буферный пул лишь скромный объём памяти. При низкой нагрузке это незаметно. Но как только растёт число одновременных запросов, узкие места начинают провялятся по очереди. Сначала запросы ждут PHP-FPM. Потом тормозит MySQL. В результате сайт начинает отдавать ошибки 502,504.
Сложность рационализации не в том, чтобы знать, «какие параметры крутить», а в понимании компромиссов за каждым из них. Слишком большой pm.max_children приведёт к OOM, слишком маленький — к очередям запросов. Завышенный innodb_buffer_pool_size заставит систему уйти в swap, слишком низкий — вызовет рост дискового ввода-вывода. В статье разберём, как работает регулировка по слоям. Дадим готовые начальные настройки и способы проверки. Мы не будем давать список цифр только для зубрёжки.
Основная часть
Системный слой: сначала фундамент для трёх верхних слоёв
Прежде чем трогать Nginx, PHP или MySQL, потолком становятся ограничения операционной системы. Лимит файловых дескрипторов — это самая частая возникающая трудность. Под высокой нагрузкой Nginx и PHP-FPM нуждаются в одновременном открытии множества соединений, файлов и дефолтные 1024 приводят к ошибке «Too many open files» уже через несколько минут после начала нагрузочного теста. В рабочей среде ulimit -n следует поднять до 65535 и закрепить это в /etc/security/limits.conf.
Параметры ядра тоже требуют настройки. Net.core.somaxconn управляет максимальной длиной очереди подсоединения. Значение по умолчанию при всплеске подключений приводит к отказам при установке соединений. Net.ipv4.tcp_tw_reuse разрешает повторное использование портов в состоянии TIME_WAIT, что заметно снижает риск их исчерпания. Пока в памяти ещё есть запас, vm.swappiness стоит снизить примерно до 10. Чтобы система не вытесняла на диск горячие данные PHP-FPM или MySQL.
Ещё один фактор, который часто забывают, - это носитель данных. IOPS и задержка NVMe значительно лучше, чем у SATA SSD и HDD, и для MySQL с интенсивным случайным чтением и записью тип диска может влиять на производительность сильнее, чем корректировка настроек.
Nginx: обработка соединений и раздача статики
Главное преимущество Nginx — умение мгновенно реагировать на события, и цель оптимизации в том, чтобы это преимущество раскрылось полностью.
Worker_processes ставим в auto, чтобы Nginx сам определил число ядер CPU. Worker_connections задаёт, сколько соединений может одновременно обрабатывать каждый worker; дефолтные 512 при высокой конкурентности явно недостаточны, разумная отправная точка — 10240, в паре с multi_accept on, чтобы worker принимал все новые соединения за раз. Use epoll на Linux обязателен: это наиболее эффективный механизм мультиплексирования ввода-вывода.
Улучшения на уровне HTTP сводится к сокращению лишней передачи данных и накладных расходов на соединения. Sendfile on позволяет отправлять статические файлы напрямую из пространства ядра, минуя копирование в пользовательское пространство. Tcp_nopush и tcp_nodelay работают в паре: первый сокращает число сетевых пакетов при отправке больших файлов, второй отключает алгоритм Нейгла там, где нужна низкая задержка. Gzip-сжатие даёт заметный эффект для текстовых ресурсов (CSS, JS, JSON), а gzip_comp_level 5 балансирует степень сжатия и нагрузку на CPU. Если значения выше, начинается убывающая отдача.
Сохранение статических ресурсов — самая доступная оптимизация. Для картинок, шрифтов и таблиц стилей стоит выставить expires 30d и отключить access_log для этих путей. Это снизит запись на диск, и нагрузку на серверную часть.
PHP-FPM: менеджер процессов — самый важный регулятор
В тюнинге PHP-FPM есть только один ключевой вопрос: каким сделать pm.max_children? Это число определяет, сколько PHP-запросов обрабатывается одновременно. При неверном значении вы либо тратите память впустую, либо получаете очереди требований и ошибку 502.
Логика расчёта прямолинейна: pm.max_children ≤ доступная память / средний объём памяти одного PHP-процесса. Объём памяти процесса зависит от фреймворка и расширений. Тяжёлые приложения вроде Laravel или Magento могут занимать 80–150 МБ, лёгкие скрипты — 30–50 МБ. Рекомендуется оставлять 20–30% памяти системе и остальным сервисам. Среднее значение RSS лучше оценивать через ps aux | grep php-fpm, а не угадывать.
Выбор стратегии управления процессами зависит от характера нагрузки. При стабильном трафике и предсказуемом пике подходит static — фиксированное число процессов, без накладных расходов на пересоздание. При переменной нагрузке — dynamic, где pm.start_servers, pm.min_spare_servers и pm.max_spare_servers задают коридор свободных процессов, а pm.max_requests периодически перезапускает воркеры, чтобы избежать потери памяти. Ondemand уместен при редких запросах. В этом случае процессы создаются по мере необходимости и быстро завершаются, экономя память в простое.
Отдельно стоит упомянуть pm.max_requests. Утечки памяти в PHP-приложениях — это реальность, и периодический перезапуск воркеров (например, каждые 500–1000 запросов) будет приемлемой защитой, которая предотвратит постепенную деградацию.
MySQL: буферный пул, соединения и медленные запросы
В LEMP-стеке MySQL чаще всего становится слабым звеном в последнюю очередь. Но когда это происходит, проявляется заметнее всего. Основной параметр — innodb_buffer_pool_size. Он определяет, сколько памяти отведено под кэш данных и индексов InnoDB. На выделенном сервере под базу данных разумно отдавать 60 –70% доступной памяти. На сервере, где находятся ещё Nginx и PHP-FPM, выделяем 30 – 40%. В противном случае система уйдёт в swap и деградация будет хуже, чем от нехватки кэша.
Innodb_log_file_size влияет на производительность записи. Слишком маленький лог-файл вызывает частые контрольные точки и рост дискового ввода-вывода. Значение в 256–512 МБ — это распространённая отправная точка для нагруженных систем. Менять его нужно осторожно, с учётом общего объёма данных.
Управление соединениями — вторая частая проблема. Max_connections по умолчанию равен 151, при большом числе PHP-воркеров, каждый из которых держит соединение с базой, лимит исчерпывается, а приложение получает «Too many connections». Значение нужно рассчитывать от числа PHP-воркеров и группы подключений, а не брать наугад. Постоянные соединения (pconnect) ускоряют работу, не нужно каждый раз налаживать связь, но обращаться с ними надо аккуратно. При неверной настройке они приводят к исчерпанию лимита.
Третий пункт — это работа с медленными запросами. Slow_query_log с long_query_time, выставленным в 1–2 секунды, даёт картину того, что реально тормозит. Без этого данные для настройки не имеют реальной основы. Индексы, переписанные запросы и исправленные схемы часто дают больший прирост, чем любая накрутка параметров.
Практические выводы
Начинайте с системного слоя. Лимиты дескрипторов, свойство ядра и тип диска будут ограничением, выше которого уже не подняться.
Считайте pm.max_children от реального потребления памяти, а не от желаемого числа одновременных запросов. Формула проста, но требует измерения RSS, а не домыслов.
Не отдавайте весь сервер под буферный пул MySQL, если на нём же работают Nginx и PHP-FPM. Swap убивает производительность сильнее, чем нехватка кэша.
Включите slow query log до того, как начнутся проблемы. Без данных о медленных запросах настройка базы превращается в угадывание.
Проверяйте каждое изменение под нагрузкой. Параметр, который показал себя хорошо на тестовом стенде, может навредить при реальном трафике. Изменения вносите по одному и фиксируйте результат.
Заключение
Совершенствование LEMP-стека — это не поиск волшебных значений в конфигурациях, а последовательное устранение слабых мест на каждом слое. Системные лимиты задают фундамент. Nginx раскрывает событийную архитектуру через грамотную работу с соединениями и статикой. PHP-FPM требует точного расчёта всех процессов от доступной памяти. MySQL держится на буферном кэше, разумном управлении соединениями и работе с медленными запросами по фактическим данным. Каждый пласт влияет на соседние, изменение одного параметра без проверки остальных легко переносит уязвимости на другой уровень. Оптимизация — это цикл: измерить, изменить, проверить, повторить. Стек, настроенный по этому принципу, выдерживает нагрузки, которые компоновка по умолчанию не переживёт и близко.














