MAATRIX / Блог / Сколько сайтов на одном PHP-FPM: считаем пул воркеров до первых 502

Сколько сайтов на одном PHP-FPM: считаем пул воркеров до первых 502

MAATRIX

Сайт открывается нормально, а через пару минут под нагрузкой — то у одного клиента, то у другого — вылезает 502 Bad Gateway, хотя nginx жив и процессор не загружен. Дело почти всегда не в коде и не в базе, а в том, что PHP-FPM физически не может обработать больше запросов одновременно, чем у него есть свободных воркеров. Дальше — как посчитать этот предел заранее, а не искать его методом проб на боевом сервере.

Почему PHP-FPM вообще упирается в потолок

nginx сам PHP не выполняет — он передаёт запрос по FastCGI в PHP-FPM, а тот раздаёт его одному из воркеров пула. Каждый воркер — это отдельный процесс, который в любой момент времени обрабатывает ровно один запрос. Пока воркер занят, он недоступен для следующего запроса. Если все воркеры пула заняты, а новый запрос приходит — он не «теряется», он встаёт в очередь на уровне сокета (backlog), и уже там начинается обратный отсчёт.

Это ключевое отличие от привычной картины «сервер медленный, но справляется»: PHP-FPM не деградирует плавно. Пока есть свободный воркер — запрос обрабатывается с обычной скоростью. Как только воркеры кончились — новые запросы просто ждут в очереди, и если ожидание превышает таймаут (на стороне nginx это fastcgi_read_timeout, либо соединение с сокетом вообще не устанавливается из-за переполненного backlog) — nginx отдаёт 502. Внешне это выглядит как «сайт то работает, то нет», хотя на самом деле сервер работает ровно по конфигурации: воркеров не хватило, и это ожидаемое, настраиваемое поведение, а не сбой.

Отсюда практический вывод: число сайтов, которые вы можете держать на одном PHP-FPM, — это не вопрос «сколько выдержит сервер», а вопрос «сколько воркеров вы можете себе позволить по памяти и сколько из них реально нужно под пиковую параллельную нагрузку всех сайтов вместе».

pm.max_children — жёсткий потолок параллельных запросов

Параметр pm.max_children в конфиге пула (/etc/php/8.x/fpm/pool.d/*.conf или /etc/php-fpm.d/*.conf в зависимости от дистрибутива) — это абсолютный максимум одновременно работающих воркеров данного пула. Не «рекомендация», а жёсткая граница: FPM физически не запустит воркер номер max_children + 1, сколько бы запросов ни стояло в очереди.

Важно понимать, что это ограничение параллелизма, а не пропускной способности. Если у вас pm.max_children = 10 и каждый запрос обрабатывается 50 мс, пул легко пропускает несколько сотен запросов в секунду — воркеры быстро освобождаются. Но если один из запросов подвис на 10 секунд (внешний API, медленный SQL-запрос без индекса, зависший curl), он держит воркер все эти 10 секунд, и в моменте у вас на один воркер меньше для всех остальных сайтов пула. Именно поэтому 502 из-за нехватки воркеров почти всегда бьёт «пачками»: один медленный запрос запускает цепную реакцию — очередь растёт, следующие запросы тоже упираются в таймаут, картина усугубляется сама собой.

Проверить, что причина именно в этом, можно по логу PHP-FPM — при достижении лимита туда пишется характерная строка:

[pool www] server reached pm.max_children setting (10), consider raising it

Если вы видите такие записи в момент всплесков 502 — это не гипотеза, а подтверждённый факт: пул исчерпан, и дальше вопрос только в том, поднимать ли max_children, чем занят зависший воркер и хватит ли памяти на увеличение пула.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Память на воркер × число воркеров — второй потолок

pm.max_children можно поставить сколь угодно большим — ядро это не остановит. Остановит память. Каждый воркер PHP-FPM — это полноценный процесс с загруженным интерпретатором, всеми подключёнными расширениями и опкэшем (если не shared) — по памяти он не бесплатный, и при достаточно большом числе воркеров именно RAM, а не CPU, обычно становится первым потолком.

Формула простая:

pm.max_children ≤ (Доступная RAM − память под ОС, БД, nginx, кэш и другие сервисы) / средняя память на воркер PHP-FPM

Среднюю память на воркер нельзя брать «из интернета» — она зависит от вашего фреймворка, подключённых расширений и от того, что именно исполняет код (лёгкий процедурный сайт и тяжёлый Laravel/WordPress с десятком плагинов дают совершенно разные цифры). Замерить свою можно так:

ps --no-headers -o rss -C php-fpm8.3 | awk '{ sum+=$1; n++ } END { print sum/n/1024 " MB average per worker" }'

Команда берёт RSS (реально занятую физическую память, а не виртуальную) всех процессов php-fpm и делит на их количество. Снимайте замер под реальной нагрузкой, а не сразу после рестарта — память воркера растёт по мере того, как в него подгружаются используемые классы и autoloader.

Дальше — пример методики, а не готовая цифра: если у вас 4 ГБ RAM, под ОС и сопутствующие сервисы вы закладываете 1 ГБ, а замер показал ~50 МБ на воркер (именно ваш замер, не наш ориентир), то теоретический потолок — около (4096 − 1024) / 50 ≈ 61 воркера. Здесь же стоит вычесть небольшой запас на пик, потому что память под воркер не константа — она колеблется в зависимости от того, что именно он сейчас обрабатывает.

Отдельно держите в уме pm.max_requests — число запросов, после которого воркер перезапускается. Это защита от постепенных утечек памяти в самом коде: PHP-скрипты иногда текут (незакрытые соединения, растущие статические массивы), и периодический рестарт воркера — дешёвая гигиена, без которой формула через пару недель аптайма перестаёт сходиться с реальностью.

static, dynamic, ondemand: какой режим пула нужен для нескольких сайтов

Директива pm определяет, как FPM управляет числом живых воркеров, и для хостинга нескольких сайтов на одном сервере это едва ли не важнее самого значения max_children.

РежимКак работаетКогда уместен
staticРовно pm.max_children воркеров запущены всегда, независимо от нагрузкиОдин нагруженный сайт с предсказуемым стабильным трафиком — предсказуемое потребление памяти, нет задержки на спавн воркера
dynamicЧисло воркеров плавает между pm.min_spare_servers и pm.max_spare_servers, стартовое количество — pm.start_serversНесколько сайтов с разным профилем трафика — баланс между расходом памяти в простое и готовностью к всплеску
ondemandВоркеры создаются по факту запроса и убиваются после pm.process_idle_timeout простояМного низкотрафичных сайтов (визитки, админки, staging-копии) — минимальный расход памяти в простое, но первый запрос после паузы идёт с задержкой на форк процесса

Для типичного сценария «десяток-два сайтов на одном VPS» рабочая комбинация — dynamic для 2-3 сайтов с реальным трафиком и ondemand для остальных: мелкие проекты не держат воркеры простаивающими, а память освобождается для тех сайтов, которым она действительно нужна. Чистый static на весь сервер в таком раскладе почти всегда означает переплату по памяти — вы держите воркеры живыми под сайты, где трафик бывает раз в час.

Обратная сторона dynamic и особенно ondemand — форк нового процесса не мгновенный. Под резким всплеском (рассылка разом привела сотню посетителей на сайт, где pm.min_spare_servers было 1-2) FPM спавнит новые воркеры со скоростью, ограниченной pm.start_servers, и часть запросов в эти секунды встаёт в очередь, даже если памяти формально хватает. Для сайтов с предсказуемыми пиками имеет смысл держать pm.min_spare_servers не на минимуме, а с запасом под типичный всплеск.

Общий пул на все сайты или отдельный пул на каждый

Это решение сильнее влияет на надёжность хостинга нескольких сайтов, чем любая отдельная цифра в конфиге. Есть два подхода.

Один общий пул на несколько сайтов (все vhost'ы nginx проксируют на один и тот же сокет FPM) экономит память — не нужно держать отдельные min_spare_servers под каждый сайт — но убивает изоляцию. Если на одном из сайтов зависает внешний API или запускается тяжёлый импорт, он вычерпывает общий пул воркеров, и 502 получают посетители всех остальных сайтов, хотя их код и база ни при чём. Диагностировать такую ситуацию тяжелее: лог FPM покажет «pm.max_children reached», но не покажет сразу, какой сайт эти воркеры удерживает — приходится смотреть slowlog или pm.status в разрезе конкретных PID.

Отдельный пул на каждый сайт — свой listen-сокет, свой системный пользователь (user/group) и свой pm.max_children — стоит дороже по памяти (у каждого пула свой минимум простаивающих воркеров), зато один зависший сайт не роняет соседей: у него кончаются свои воркеры, а не общие. Отдельные пулы также дают раздельный лог ошибок и разный php_admin_value[memory_limit] для тяжёлого и лёгкого проекта, не завышая лимит глобально.

Практический ориентир: если сайты на сервере принадлежат разным клиентам или разным по важности проектам — разделяйте пулы, экономия памяти того не стоит. Если это несколько внутренних лендингов одной команды с похожим кодом и вы сознательно жертвуете изоляцией ради простоты — общий пул допустим, но тогда обязательно ставьте request_terminate_timeout в конфиге пула, чтобы один зависший скрипт не держал воркер бесконечно.

Как увидеть насыщение пула до того, как это увидят пользователи

Ждать 502 в логах nginx — реактивный подход. FPM умеет отдавать текущее состояние пула через встроенную страницу статуса, и её стоит подключить заранее, а не после первого инцидента.

Включается в конфиге пула:

pm.status_path = /fpm-status

И проксируется отдельным location в nginx (закройте его от внешнего доступа — allow/deny по IP или Basic Auth, наружу такая страница светить не должна):

location = /fpm-status {
    fastcgi_pass unix:/run/php/php8.3-fpm-sitename.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
    allow 127.0.0.1;
    deny all;
}

В расширенном режиме (?full) вывод показывает по каждому активному процессу PID, время выполнения текущего запроса и объём запрошенной памяти — по этим цифрам легко находится тот самый воркер, который держит запрос дольше нормы. В обычном режиме важны три поля:

  • active processes — сколько воркеров занято прямо сейчас; если это число регулярно упирается в max_children — пул на грани, и 502 — вопрос времени, а не вероятности;
  • listen queue — сколько запросов стоит в очереди на сокет; в норме должно быть 0, любое устойчивое ненулевое значение — прямой признак насыщения;
  • max children reached — счётчик того, сколько раз пул упирался в потолок с момента последнего рестарта; растущее число здесь — то же самое предупреждение, что и строка в логе, но в форме, удобной для мониторинга (Zabbix, Prometheus через php-fpm_exporter, или просто периодический скрипт с алертом).

Отдельно включите slowlog — он пишет полный backtrace запросов, которые выполняются дольше заданного порога, и обычно именно он указывает на первопричину (не хватает воркеров, потому что часть из них подвисла на конкретном участке кода):

slowlog = /var/log/php-fpm/sitename-slow.log
request_slowlog_timeout = 5s

Частые ошибки конфигурации

  • max_children посчитан «на глаз», без учёта реальной памяти на воркер. Обычно берут круглое число вроде 20 или 50 и не пересчитывают его при добавлении нового сайта на сервер — в какой-то момент сумма пулов начинает превышать физическую RAM, и сервер уходит в своп при пиковой нагрузке, что медленнее и хуже, чем честный 502.
  • max_children выставлен слишком высоко «про запас». Обратная ошибка: если сумма worker'ов по всем пулам способна съесть всю RAM сервера, при одновременном пике на нескольких сайтах вместо контролируемого 502 вы получаете OOM killer, который убивает произвольные процессы — не обязательно PHP-FPM, а с равной вероятностью базу данных.
  • Общий пул под сайты с разной критичностью без request_terminate_timeout. Один зависший внешний запрос на второстепенном сайте держит воркер бесконечно и постепенно вычерпывает пул, которым пользуются и более важные проекты.
  • listen.backlog оставлен на дефолте при высоком трафике. Значение по умолчанию (обычно 511) — это очередь на уровне сокета до того, как запрос дойдёт до воркера. При коротких, но частых всплесках она переполняется раньше, чем срабатывает логика pm.max_children, и nginx получает отказ в соединении, а не таймаут — тоже 502, но с другой причиной в логах.
  • Таймауты nginx и FPM не согласованы. Если fastcgi_read_timeout в nginx меньше, чем реальное время выполнения тяжёлых, но легитимных запросов (экспорт отчёта, генерация PDF), nginx рвёт соединение и отдаёт 502 на полностью рабочий, просто не самый быстрый скрипт — это чинится не увеличением пула, а согласованием таймаутов.
  • pm.max_requests не выставлен вовсе. На небольших нагрузках это долго не заметно, но на сервере с несколькими сайтами и разным качеством кода один из пулов рано или поздно начинает медленно течь по памяти, и через недели аптайма формула расчёта воркеров по памяти перестаёт совпадать с реальностью.

Если вы только настраиваете сервер под несколько сайтов с нуля, порядок действий и общая структура файлов подробно разобраны в статье про настройку VPS под хостинг нескольких сайтов; там же — расчёт того, сколько ресурсов вообще нужно серверу под заданное число проектов. Если 502 уже появляется, а причина не всегда в PHP-FPM, стоит сначала исключить более общие сценарии — им посвящён разбор 502 Bad Gateway в nginx. А если по соседству упирается в потолок сам nginx, а не PHP-FPM — это отдельная история, разобранная в статье про предел одновременных соединений nginx.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько сайтов реально можно держать на одном PHP-FPM?

Универсального числа нет — это всегда производная от доступной памяти, среднего расхода на воркер конкретных проектов и того, насколько трафик разных сайтов пересекается по времени пиков. Правильный вопрос не «сколько сайтов», а «сколько параллельных запросов нужно обрабатывать одновременно с запасом» — и уже под это считать пул.

Можно ли просто сильно увеличить pm.max_children и забыть про проблему?

Нет — если увеличить лимит воркеров без учёта памяти, сервер вместо контролируемых 502 начнёт уходить в своп или получать OOM killer при пиковой нагрузке, что диагностировать и чинить сложнее, чем изначальную нехватку воркеров.

502 всегда означает нехватку pm.max_children?

Нет. Та же ошибка возникает при переполнении listen.backlog, при рассинхронизации таймаутов nginx и FPM, при падении самого php-fpm процесса или проблемах с правами на unix-сокет. Лог FPM со строкой «reached pm.max_children» — единственное надёжное подтверждение именно этой причины.

Нужно ли использовать static pm, если сервер выделен под один высоконагруженный сайт?

Это разумный вариант — static убирает задержку на форк новых воркеров под растущей нагрузкой и даёт предсказуемое потребление памяти, но требует точного расчёта числа воркеров заранее, поскольку резерва под неожиданный всплеск в этом режиме нет.

Как понять, что именно memory, а не CPU, стал узким местом раньше?

Если pm.status показывает max children reached и растущий listen queue при том, что top/htop не показывает загрузку CPU у php-fpm-процессов близко к 100% — это признак того, что предел по памяти (низкий max_children из-за нехватки RAM) наступил раньше, чем предел по вычислительным ресурсам.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →