pm.max_children в PHP-FPM: расчёт вместо угадывания

Опубликовано 19 мин. чтения

Сообщение server reached pm.max_children не означает, что значение нужно удвоить. Измерьте, рассчитайте, выберите режим работы и после этого докажите, что значение действительно подходит.

Рано или поздно в журнале PHP-FPM появляется эта строка, и совет она выдаёт сразу же:

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

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

Эта инструкция показывает путь от угадывания к расчёту: измерить потребление памяти одним рабочим процессом, определить бюджет, выбрать режим работы и после этого доказать, что значение верное.

Все данные относятся к Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS и Ubuntu 22.04 LTS. Команды рассчитаны на работу под root, обычному пользователю нужно добавлять впереди sudo. Везде подставляйте тот номер версии PHP, который стоит в вашей системе.

СистемаPHPСлужбаФайл пулаЖурнал FPM
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/pool.d/www.conf/var/log/php8.4-fpm.log
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/pool.d/www.conf/var/log/php8.2-fpm.log
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/pool.d/www.conf/var/log/php8.3-fpm.log
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/pool.d/www.conf/var/log/php8.1-fpm.log

Какие версии установлены, видно по содержимому каталога. На серверах, прошедших обновление дистрибутива, их часто оказывается две:

ls /etc/php/
ls /etc/php/*/fpm/pool.d/

Что на самом деле ограничивает pm.max_children

Это значение ограничивает не посетителей и не соединения, а количество PHP-запросов, которые выполняются в один и тот же момент. Запрос длиной 80 миллисекунд занимает рабочий процесс на 80 миллисекунд и затем освобождает его. Отсюда следует почти всё остальное:

  • Большому трафику нужно мало процессов, пока скрипты работают быстро. 40 запросов в секунду по 80 миллисекунд каждый дают чуть больше трёх одновременных запросов, а не 40.
  • Одно медленное место рушит весь расчёт. Обращение к внешнему API, для которого не задан собственный тайм-аут, держит процесс занятым пять секунд, хотя тот ничего не делает. Пять таких обращений в секунду постоянно связывают 25 процессов.
  • Ожидающие соединения процесс не занимают. Keep-alive-соединение с nginx стоит одного файлового дескриптора, но не рабочего процесса.

Когда все процессы заняты, новые запросы ждут в очереди приёма на сокете. Её размер задаётся в файле пула параметром listen.backlog, по умолчанию 511, и дополнительно ограничивается ядром через net.core.somaxconn, который на всех четырёх системах равен 4096:

sysctl net.core.somaxconn

Пока очереди хватает, посетитель замечает только возросшее время загрузки. Когда заполняется и она, ядро отклоняет соединение, nginx пишет в журнал 11: Resource temporarily unavailable, а до посетителя доходит 502. Чтобы отделить эту причину от остальных: как исправить ошибку nginx 502 Bad Gateway.

Деталь, которая порождает много путаницы: предупреждение появляется один раз за фазу насыщения, а не на каждый пострадавший запрос. FPM ставит внутри себя отметку и снимает её только тогда, когда снова освобождается процесс. Одна-единственная строка может стоять за целым часом пик. Тот, кто считает строки, недооценивает масштаб проблемы. Честная метрика — это счётчик max children reached на странице статуса, которую мы вызовем ниже.

Путь назад, ещё до того как вы что-то поменяете

Здесь может пойти не так две вещи, и обе бьют по работающему сервису.

Первое: FPM больше не запускается. Команда systemctl reload посылает главному процессу сигнал USR2, после чего тот перезапускает сам себя с новой конфигурацией. Если она недействительна, инициализация обрывается и главный процесс завершается. Работавший экземпляр превращается в мёртвый. Поэтому без исключений: сначала проверка, потом загрузка:

php-fpm8.2 -t

При успехе вывод заканчивается словами test is successful.

Второе: значение слишком велико. Тогда сервер ломается не сразу, а при ближайшем пике нагрузки, причём настолько основательно, что и вход по SSH зависает или вовсе не устанавливается. На этот случай вам нужен второй путь к серверу.

У KVM-root-серверов и выделенных серверов KernelHost это VNC-консоль в личном кабинете. Она работает на уровне виртуализации, а у выделенных серверов — на уровне самого подключения, и остаётся доступной даже тогда, когда служба SSH из-за нехватки памяти уже не отвечает. Зайдите через неё один раз заранее. Аварийный путь, который впервые пробуют в момент аварии, никакой не путь.

Кроме того, сделайте копию файла пула с отметкой времени, чтобы вторая попытка не затёрла первую копию:

mkdir -p /root/backups
cp -a /etc/php/8.2/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)
ls -l /root/backups/

Тогда путь назад укладывается в три строки:

cp -a /root/backups/www.conf.2026-09-03-1015 /etc/php/8.2/fpm/pool.d/www.conf
php-fpm8.2 -t
systemctl reload php8.2-fpm

Страховка на случай, если вы ошибётесь в расчётах

Лимит памяти на systemd-юните добивается того, что ошибка в оценке ударит по рабочему процессу PHP, а не по базе данных: ядро тогда завершает процесс внутри control-group самого FPM, вместо того чтобы искать самый крупный процесс по всей системе.

mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/memory.conf <<'EOF'
[Service]
MemoryHigh=1500M
MemoryMax=2G
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm

Проверить, что настройка применилась:

systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax

MemoryHigh притормаживает и освобождает память, MemoryMax — это жёсткая граница. Все четыре системы работают с унифицированной иерархией control-groups, поэтому значения действуют напрямую. Это именно страховка, а не замена расчёту: при достижении границы конкретный посетитель по-прежнему увидит ошибку, зато не ляжет весь сервер. Как аккуратно раскладывать такие дополняющие файлы: создание systemd-сервиса.

И третье правило, для которого не нужна ни одна команда: по одному изменению за раз. Тот, кто трогает режим работы, pm.max_children и значения spare одновременно, потом не знает, что именно сработало.

Инвентаризация: какой пул действует

У каждого пула собственное значение pm.max_children, а для памяти важна сумма по всем пулам:

grep -n "^pm" /etc/php/*/fpm/pool.d/*.conf

В состоянии сразу после установки на всех четырёх системах стоит одно и то же:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

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

grep -n "^process.max" /etc/php/*/fpm/php-fpm.conf

Если вывод остаётся пустым, значит строка закомментирована и действует значение по умолчанию 0, то есть глобального ограничения нет. На серверах с большим числом пулов именно эта строка не даёт сумме разорвать память, как только несколько пулов одновременно попадают под нагрузку.

В конечном счёте решает не файл, а то, что из него сделал FPM. Ключ -tt выводит конфигурацию полностью развёрнутой, вместе со всеми значениями по умолчанию, которых нет ни в одном файле:

php-fpm8.2 -tt 2>&1 | grep -E "^\[|pm |pm\.|listen ="

Позже этот же вывод послужит доказательством того, что изменение применилось.

Измеряем потребление памяти одним рабочим процессом

Вот здесь большинство инструкций теряет точность. Три величины регулярно путают между собой:

  • memory_limit, по умолчанию 128M в режиме FPM: это верхний предел на один запрос, а не расход. Скрипту, которому нужно 12 МБ, и при 512M понадобится всего 12 МБ.
  • RSS: всё, что в данный момент находится в оперативной памяти, включая общий Opcache и разделяемые библиотеки. Тот, кто складывает RSS по 20 процессам, засчитывает один и тот же Opcache двадцать раз.
  • PSS: совместно используемые страницы делятся на число тех, кто ими пользуется. Единственная из трёх величин, которую есть смысл суммировать.

Для расчёта вам нужен PSS. Ядро отдаёт его уже в готовом сводном виде в /proc/<pid>/smaps_rollup, чтение доступно под root:

pgrep -f "php-fpm: pool www" | while read -r p; do
  awk '/^Pss:/ {print $2}' "/proc/$p/smaps_rollup"
done | awk '{s+=$1; n++} END {
  if (!n) { print "рабочие процессы не найдены"; exit }
  printf "Процессы: %d   Сумма: %.0f MiB   Среднее: %.1f MiB\n", n, s/1024, s/1024/n
}'

Для сравнения тот же пул, но через RSS:

ps -eo pid,rss,args --sort=-rss | grep '[p]hp-fpm' | head

Сумма RSS в зависимости от размера Opcache оказывается заметно выше. Тот, кто считает по ней, получает слишком маленькое pm.max_children и покупает себе время ожидания, которого могло бы и не быть.

Годится ли измерение хоть на что-то, решают два условия. Измеряйте под настоящей нагрузкой, а не сразу после перезапуска: только что порождённый процесс дёшев, потому что поначалу он лишь пользуется страницами памяти главного процесса совместно с ним, и дорожает только с первыми запросами. И не берите одно только среднее значение: если среднее составляет 60 МиБ, а самый крупный процесс 190 МиБ, считать по 60 нельзя.

Второй источник: журнал доступа FPM

По желанию FPM записывает для каждого запроса пиковое потребление и время выполнения. Обе строки лежат в файле пула закомментированными:

access.log = /var/log/php8.2-fpm.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

Написание %{mili}d именно такое и есть правильное, оно без изменений взято из штатного файла и даёт время выполнения в миллисекундах, а %{kilo}M — пиковое потребление в килобайтах. После этого проверить, загрузить и посмотреть, появляется ли файл:

php-fpm8.2 -t
systemctl reload php8.2-fpm
ls -l /var/log/php8.2-fpm.access.log

Дайте журналу поработать полные сутки, чтобы в него попали часы пик. После этого разберите пиковое потребление по квантилям:

awk '{print $(NF-1)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Запросов: %d   Медиана: %d kB   p95: %d kB   Максимум: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

Тот же разбор для времени выполнения, которое сейчас понадобится для второго расчёта, берёт предыдущий столбец:

awk '{print $(NF-2)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Медиана: %.0f ms   p95: %.0f ms   Максимум: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'

Два ограничения. Во-первых, позиции полей завязаны на показанную выше строку формата, потому что она заканчивается временем выполнения, памятью и долей CPU. Тот, кто меняет access.format, должен пересчитать и их. Во-вторых, %{kilo}M — это собственный учёт памяти внутри PHP, а значит нижняя граница: код программы, расширения и та память, которую библиотека C после запроса возвращает не сразу, в него не входят. Поэтому для формулы берётся PSS. Журнал доступа же нужен, чтобы найти скрипты-выбросы, из-за которых процесс надолго остаётся большим.

Потом снова выключите этот журнал или настройте для него ротацию. Штатное правило охватывает только журнал ошибок, а заполненный накопитель порождает симптомы, которые с PHP-FPM уже никак не связаны: что делать, когда диск в Linux заполнен.

Расчёт

Величин две: верхний предел, вытекающий из оперативной памяти, и потребность, вытекающая из нагрузки. Правильное значение — меньшая из двух.

Верхний предел из оперативной памяти

pm.max_children = бюджет для PHP  /  PSS на один рабочий процесс

Бюджет — это не вся оперативная память:

free -m

Столбец available уже учитывает кэш, который можно освободить, но не содержит той памяти, которую прямо сейчас удерживают ваши рабочие процессы. Значит, бюджет — это available плюс измеренная сумма PSS, минус резерв. В качестве резерва хорошо себя показали примерно 20% всей памяти, но не меньше 512 МБ. Он перехватывает то, что не попадает ни в одно измерение: растущий пул буферов базы данных, резервное копирование, обновление пакетов, импорт данных, запущенный в самый неподходящий момент.

Всего RAMПостоянно занятоРезервБюджет для PHPPSS на процессpm.max_children
4 ГБ1,5 ГБ0,8 ГБ1,7 ГБ60 МБ29
8 ГБ3,0 ГБ1,6 ГБ3,4 ГБ80 МБ43
16 ГБ6,0 ГБ3,2 ГБ6,8 ГБ110 МБ63

Всегда округляйте вниз. Один процесс сверху ничего не даёт, а один лишний процесс может стоить всего.

Потребность из нагрузки

нужное число процессов = запросов в секунду  ×  среднее время выполнения в секундах

Число запросов в секунду даёт страница статуса FPM: accepted conn, делённое на start since, даёт среднее с момента последнего запуска. Время выполнения берётся из разбора выше, причём значение p95, а не медиана, иначе вы рассчитаете сервер на спокойный вечер, а не на час пик.

Пример: 40 запросов в секунду на пике, время выполнения p95 равно 300 миллисекундам, получается 12 одновременных запросов, с надбавкой на короткие всплески примерно от 20 до 25. Если верхний предел равен 43, впишите значение потребности, а оставшуюся память оставьте там, где от неё больше пользы, то есть в кэше базы данных.

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

dynamic, ondemand или static

Режим работы определяет, когда процессы появляются и исчезают. Верхний предел pm.max_children действует во всех трёх случаях.

Режим работыПроцессов при стартеПоведениеПамятьПодходит для
dynamicpm.start_serversдержит наготове от min_spare до max_spare бездействующих процессов, при необходимости порождает новые вплоть до max_childrenколеблется вместе с нагрузкойобычного случая: один или несколько пулов, переменная нагрузка
ondemandни одногозапускает процесс только при поступлении запроса и завершает его по истечении pm.process_idle_timeoutв простое минимальнамножества пулов на одном сервере, сайтов с малым трафиком
staticpm.max_childrenровно столько процессов, постоянно, без порождения и завершения во время работыпостоянно на максимумеодного пула на отведённом под него оборудовании, равномерной нагрузки

dynamic стоит по умолчанию и для обычного случая подходит. Он стоит некоторого процессорного времени на порождение процессов и требует, чтобы все четыре значения были согласованы между собой.

ondemand ощутимо экономит память в простое, когда на одной машине лежит десяток пулов для редко посещаемых сайтов. Плата за это — первый запрос после паузы, которому приходится ждать порождения процесса. pm.process_idle_timeout задаёт, сколько живёт бездействующий процесс (по умолчанию десять секунд), и работает исключительно в этом режиме. Учтите ещё одно: предупреждение о насыщении называет здесь max_children без префикса pm.. Тот, кто ищет привычную формулировку, не найдёт ничего.

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

Независимо от режима работы стоит взглянуть на pm.max_requests, по умолчанию 0, то есть без ограничения. Значение вроде 500 заменяет каждый рабочий процесс после 500 запросов. Против медленно растущего потребления памяти из-за неаккуратного расширения это средство действенное и дешёвое, ведь Opcache остаётся общим и заново не собирается. Только не ставьте значение 20, иначе FPM проведёт всё своё время за порождением процессов.

start_servers и значения spare

Эти три значения работают только при pm = dynamic и определяют, насколько быстро FPM реагирует на пик нагрузки:

  • pm.min_spare_servers: столько бездействующих процессов FPM держит наготове как минимум, это буфер на всплески. Слишком низкое значение означает, что каждый пик сначала ждёт порождения процессов.
  • pm.max_spare_servers: столько бездействующих процессов FPM терпит максимум. Без этой границы FPM сохранял бы после пика все процессы, а вместе с ними и занятую ими память.
  • pm.start_servers: столько процессов существует сразу после запуска.

При запуске FPM проверяет четыре условия и отказывается работать, если нарушено хотя бы одно: оба значения spare должны быть больше нуля, ни одно из них не должно превышать pm.max_children, max_spare не может быть меньше min_spare, а start_servers должен лежать между ними. Если pm.start_servers отсутствует совсем, FPM считает сам и пишет в журнал:

NOTICE: [pool www] pm.start_servers is not set. It's been set to 3.

Формула для этого такая: min_spare + (max_spare - min_spare) / 2, то есть середина между двумя значениями spare. В качестве отправной точки на практике работает такой набор:

pm.max_children       = 40
pm.start_servers      = 10
pm.min_spare_servers  = 6
pm.max_spare_servers  = 16

Легко упустить из виду: бездействующие процессы тоже занимают память. Бюджет должен выдерживать pm.max_children, тогда как повседневное потребление примерно равно max_spare плюс активные процессы. Высокое значение max_spare держит память занятой и в три часа ночи.

Как с этим связан swap

Заманчиво включить в расчёт и память подкачки. Не делайте этого. Рабочий процесс, данные которого лежат на накопителе, отвечает на порядки медленнее и потому дольше остаётся занятым. Число одновременных запросов растёт, FPM порождает новые процессы, а те вытесняют ещё больше памяти. Именно эта обратная связь объясняет, почему перегруженный сверх меры сервер не деградирует постепенно, а заваливается за несколько минут.

Что swap всё же даёт, так это буфер на случай ошибки. Без него перебор заканчивается резко: ядро завершает процесс. С ним вы сначала получаете медленный сервер, а вместе с ним и окно времени для вмешательства. Правило такое: swap да, но pm.max_children считать исключительно от настоящей оперативной памяти, никогда от суммы оперативной памяти и swap.

Идёт ли уже вытеснение в подкачку, видно в двух местах. free -m показывает занятость, но ничего не говорит об активности, ведь однажды вытесненная и больше никогда не понадобившаяся страница безобидна. Показательны столбцы si и so, то есть подкачка в память и из памяти за секунду:

free -m
vmstat 1 5

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

cat /proc/pressure/memory

Если файла нет, значит функция в ядре отключена и вам остаётся vmstat. Как создать swap, прописать его на постоянной основе и подобрать vm.swappiness, описано в статье настройка swap и защита от нехватки памяти.

Значение задрано: вместо очереди приходит OOM-killer

Допустим, вы вписываете 200, чтобы предупреждение наконец пропало. В простое ничего не происходит, страница грузится, всё выглядит решённым. При следующем наплыве FPM действительно порождает до 200 процессов, каждый с первыми запросами дорастает до своего настоящего размера, свободная память падает, ядро сначала выбрасывает файловый кэш (из-за чего база данных замедляется и её запросы выполняются дольше), потом уходит в подкачку, а потом вступает OOM-killer.

Жертву он выбирает по потреблению памяти, а самый крупный отдельный процесс на веб-сервере — это не рабочий процесс PHP с его 80 МБ, а база данных с её пулом буферов:

dmesg -T | grep -iE "out of memory|oom-kill"
journalctl -k --since "24 hours ago" | grep -i "out of memory"

Две строки, которые здесь важны, выглядят так:

php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Out of memory: Killed process 1234 (mariadbd) total-vm:2891234kB, anon-rss:1783456kB,
file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:4096kB oom_score_adj:0

Инициатор и жертва здесь два разных процесса, и именно это делает поиск причины неприятным: в почтовом ящике лежит уведомление об упавшей базе данных, и никто не вспоминает о позавчерашней настройке PHP. В зависимости от базы данных в скобках стоит mariadbd или mysqld.

Если же удар всё-таки приходится по рабочему процессу, FPM сообщает об этом сам, а при заданном лимите памяти это дополнительно попадает в журнал systemd:

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start
php8.2-fpm.service: A process of this unit has been killed by the OOM killer.

Разница между этими двумя картинами отказа и есть настоящая мысль этой статьи. Заниженное pm.max_children порождает очередь: измеримую, задокументированную в журнале, сводимую к одному значению, и сервер при этом остаётся отзывчивым. Завышенное порождает завершённый процесс там, где вы этого не выбирали, у службы, которая при некоторых обстоятельствах сама не поднимется. Время ожидания — это рабочее состояние, а событие OOM — это инцидент.

Частые ошибки и способы их устранения

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: в какой-то момент все рабочие процессы были заняты. Строка появляется один раз за фазу насыщения, так что одна-единственная строка может означать час полной загрузки. Повышайте значение только после того, как измерите PSS и определите бюджет.

WARNING: [pool www] server reached max_children setting (5), consider raising it: та же ситуация при pm = ondemand, в формулировке без префикса pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, следом ERROR: failed to post process the configuration и ERROR: FPM initialization failed: самый частый случай при снижении pm.max_children. Тот, кто уходит с 50 на 8 и оставляет pm.max_spare_servers = 20, получает службу, которая больше не запускается. Все четыре значения нужно менять вместе.

ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: pm.start_servers вышел за пределы диапазона. Исправьте значение или удалите строку, тогда FPM посчитает сам.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): это memory_limit, и к pm.max_children оно отношения не имеет. Отдельный скрипт запросил больше, чем PHP разрешает на один запрос. Более высокое pm.max_children здесь ничего не изменит, а более низкое memory_limit, наоборот, не уменьшит потребление памяти рабочим процессом, оно лишь раньше оборвёт скрипты. Для планирования всё же важно: memory_limit — это верхняя граница того, что процесс вправе запросить.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream в журнале ошибок nginx: все процессы заняты, и вдобавок заполнена очередь приёма. Более высокое listen.backlog лишь отодвигает проблему, причина же кроется в числе процессов или во времени выполнения скриптов.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: процесс был жёстко завершён извне, как правило OOM-killer. Здесь значение слишком велико, а не слишком мало. Проверка от обратного делается по журналу ядра.

«Я повысил значение, и ничего не меняется.» Почти всегда правился не тот файл: вторая версия PHP в /etc/php/, второй пул или копия в pool.d/, которая вообще не читается. Решающее значение имеет то, к какому сокету обращается nginx и какой пул на нём слушает:

grep -Rn "fastcgi_pass" /etc/nginx/
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

«После reload сайт пропал.» Reload перезапускает FPM с новой конфигурацией, и если она недействительна, главный процесс завершается. Верните копию из /root/backups и приучите себя выполнять php-fpm8.2 -t перед каждым reload.

Как убедиться, что значение верное

«Предупреждение пропало» доказательством не является. Со значением 500 оно тоже пропадёт, вплоть до первого события OOM. Надёжных проверок пять.

Первое: страница статуса FPM. Впишите pm.status_path = /status в файл пула, проверьте конфигурацию и загрузите её, а затем опрашивайте сокет напрямую, полностью в обход nginx. Так страница остаётся недоступной из сети:

apt-get install -y libfcgi-bin
SCRIPT_NAME=/status SCRIPT_FILENAME=/status REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock

Ответ несут четыре строки вывода:

ПолеЧто означаетЦелевое значение
max children reachedсколько раз с момента запуска был достигнут верхний предел0
max listen queueсамая длинная замеченная очередь на сокете0
max active processesнаибольшее число одновременно активных процессовзаметно ниже pm.max_children
slow requestsзапросы дольше request_slowlog_timeoutжелательно 0

Показательными эти цифры становятся только после полной недели со всеми часами пик. Если max active processes потом стоит на 12, тогда как pm.max_children равно 43, у вас есть запас, и резерв можно применить в другом месте.

Второе: память под нагрузкой, не ночью, а на пике. available должно заметно превышать ноль, а столбцы si и so должны стоять на нуле:

free -m
vmstat 1 5

Третье: ни одного события OOM. Эта проверка самая важная, потому что она исключает худшую из картин отказа. Пустой вывод и есть желаемый результат:

journalctl -k --since "7 days ago" | grep -i "out of memory"

Четвёртое: сумма по всем пулам. На сервере с несколькими сайтами важно не отдельное значение, а сумма, умноженная на PSS одного процесса. Она должна укладываться в бюджет даже тогда, когда все пулы одновременно находятся под нагрузкой:

php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Пятое: настройка переживает перезагрузку. Пункт, который пропускают чаще всего. Значение в файле, который вообще не читается, обнаружится только при следующей перезагрузке, а она редко случается тогда, когда вы как раз смотрите на сервер:

systemctl is-enabled php8.2-fpm
systemctl restart php8.2-fpm
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

После этого один раз перезагрузите сервер контролируемо, пока вы ещё за ним наблюдаете. У KVM-root-серверов и выделенных серверов KernelHost эта перезагрузка запускается в личном кабинете, а за загрузкой системы можно следить через VNC-консоль, даже если веб-служба ещё не отвечает. Серверы стоят в дата-центре maincubes во Франкфурте-на-Майне.

Краткий чек-лист

  1. Копия файла пула лежит в /root/backups, доступ через VNC-консоль один раз опробован.
  2. Измерить PSS на один рабочий процесс под настоящей нагрузкой, а не сразу после перезапуска, и не считать по RSS.
  3. Определить бюджет: available плюс текущая сумма PSS, минус осознанно заданный резерв.
  4. Рассчитать верхний предел, сопоставить его с потребностью (запросов в секунду, умноженные на время выполнения p95), взять меньшее значение и округлить вниз.
  5. Выбрать режим работы и привести значения spare в соответствие вместе с pm.max_children.
  6. php-fpm8.2 -t, затем systemctl reload, затем php-fpm8.2 -tt как доказательство того, что значение загружено.
  7. Через неделю проверить max children reached, max listen queue и журнал ядра.

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

Как правильно рассчитать pm.max_children?
По двум величинам, и побеждает меньшая. Верхний предел — это бюджет для PHP, делённый на приходящуюся на один рабочий процесс память (PSS). Бюджет складывается из столбца available в выводе free -m плюс та память, которую прямо сейчас удерживают работающие процессы, минус резерв примерно в 20% всей памяти. Вторая величина — это потребность: число запросов в секунду на пике, умноженное на время выполнения p95 в секундах. Если верхний предел равен 43, а потребность 22, впишите 22 и оставьте остаток памяти базе данных. Округляйте всегда вниз.
Почему завышенное значение опаснее заниженного?
Заниженное значение порождает очередь. Запросы ждут в очереди приёма на сокете, страница отдаётся медленнее, сервер остаётся отзывчивым, а журнал точно говорит вам, что происходит. Завышенное значение под нагрузкой приводит к нехватке памяти, и тогда ядро само выбирает жертву, причём по объёму потребляемой памяти. Самый крупный процесс на веб-сервере — это обычно база данных с её пулом буферов, а не рабочий процесс PHP. То есть вы меняете измеримое время ожидания на необъявленный отказ совсем другой службы.
Почему считать нужно по PSS, а не по RSS?
RSS содержит и совместно используемые области памяти, прежде всего Opcache и разделяемые библиотеки. Тот, кто складывает RSS по 20 рабочим процессам, засчитывает один и тот же Opcache двадцать раз и получает сильно завышенное потребление памяти, а значит и без нужды маленькое pm.max_children. PSS делит совместно используемые страницы на число тех, кто ими пользуется, и поэтому суммируется корректно. Значение доступно под root в /proc/PID/smaps_rollup в строке Pss.
Когда выбирать dynamic, когда ondemand, а когда static?
dynamic стоит по умолчанию и подходит для обычного случая: один или несколько пулов с переменной нагрузкой. ondemand оправдан, когда на одной машине лежит много пулов для редко посещаемых сайтов, ведь в простое процессов там нет вовсе. Плата за это — первый запрос после паузы, которому приходится ждать порождения процесса. static подходит, когда PHP является главным потребителем ресурсов машины, а нагрузка равномерна: память занимается сразу и навсегда, зато под нагрузкой сюрпризов больше не будет. Если же PHP делит сервер с базой данных, static отнимает у неё возможность временно держать больше кэша.
Можно ли включать swap в расчёт?
Нет. Рабочий процесс, данные которого лежат на накопителе, отвечает на порядки медленнее и потому дольше остаётся занятым. Число одновременных запросов растёт, FPM порождает новые процессы, а те вытесняют ещё больше памяти. Именно поэтому перегруженный сверх меры сервер заваливается за несколько минут, а не деградирует постепенно. Swap тем не менее полезен, но как буфер на случай ошибки: он даёт вам окно времени для вмешательства, прежде чем ядро завершит процесс. Расчёт же ведётся исключительно от настоящей оперативной памяти.
Я повысил pm.max_children, и ничего не меняется. В чём дело?
Почти всегда правился не тот файл. На серверах с несколькими версиями PHP в /etc/php/ или с несколькими пулами считается только тот пул, к сокету которого действительно обращается nginx. Сравните fastcgi_pass из конфигурации nginx со строками listen в файлах пулов. Что FPM загрузил на самом деле, показывает php-fpm8.2 -tt, в выводе которого стоит полностью развёрнутая конфигурация со всеми значениями по умолчанию.
После снижения pm.max_children PHP-FPM больше не запускается. Что случилось?
Скорее всего, значения spare остались прежними, более высокими. FPM отказывается стартовать с сообщением о том, что pm.min_spare_servers и pm.max_spare_servers не могут быть больше pm.max_children, а следом идёт FPM initialization failed. Приведите в соответствие все четыре значения сразу: оба значения spare должны быть больше нуля и не превышать pm.max_children, max_spare не может быть меньше min_spare, а pm.start_servers должен лежать между ними.
Связан ли memory_limit с pm.max_children?
Только косвенно. memory_limit — это верхний предел, который PHP навязывает каждому отдельному запросу, по умолчанию 128M в режиме FPM. Сообщение об исчерпанной памяти (Allowed memory size exhausted) относится к одному скрипту, и более высокое pm.max_children его не исправит. И наоборот, более низкое memory_limit не уменьшит потребление памяти рабочим процессом, оно лишь раньше оборвёт скрипты. Связь проявляется при планировании: тот, кто ставит memory_limit равным 1024M, должен в худшем случае рассчитывать на такой объём и на каждый процесс.

PHP-FPM pm.max_children Оперативная память Debian Ubuntu nginx OOM killer Оптимизация сервера