PHP-FPM: движок под WordPress

Это четвёртая статья серии «Эксплуатация VPS / DevOps-база». В первой мы поставили сервер на ноги, во второй поняли, как вообще что-то на нём живёт, в третьей подняли nginx и завели HTTPS. Но вы же помните: nginx сам WordPress не исполняет — он передаёт запрос «наверх», через unix-сокет. Мы оставили за этим сокетом голый PHP-FPM по умолчанию, и обещали, что именно там рождаются «белый экран при десятке параллельных гостей» и 502 Bad Gateway. Пришло время залезть внутрь.

PHP-FPM — это и есть тот «движок», на котором крутится весь ваш WordPress: каждый запрос к странице, к админке, к AJAX-плагину — это работа процесса PHP-FPM. По умолчанию он настроен «на среднюю машину», а у вас на носу дешёвый VPS с гигабайтом-двумя памяти. Разница между «настроено по умолчанию» и «посчитано под вашу память» — это между сайтом, который стоит при всплеске трафика, и сайтом, который его переживает. В этой статье мы: разберём, что такое пул процессов и зачем он вообще nginx; научимся считать число процессов по свободной памяти (а не гадать); разберёмся, что живёт в php.ini, а что в www.conf и чем php_admin_value отличается от php_value; включим Opcache (самая большая бесплатная прибавка скорости WordPress) и научимся его «прогревать»; и наконец научимся читать slowlog — лог медленных запросов, который за минуту называет того, кто тормозит страницу, вместо гаданий «кто же виноват: тема, плагин или база».

Термины, которые введём один раз: PHP-FPM (FastCGI Process Manager, «менеджер процессов FastCGI») — программа, которая держит группу процессов PHP и отвечает на FastCGI-запросы от веб-сервера. Мастер-процесс (master) — главный, он сам не исполняет код, а управляет воркерами (читает конфиг, спавнит/убивает, следит). Воркер (worker, «рабочий») — процесс, который реально исполняет PHP-код запроса. Пул (pool) — именованный набор воркеров с общим конфигом (обычно один пул = один сайт). Байткод — скомпилированное представление PHP-файла, которое интерпретатор исполняет быстрее, чем исходный текст. Все домены и пути условные (example.test, /var/www/example.test); подставляйте свои.


Часть I. Что такое пул процессов и зачем он nginx

Чтобы не воспринимать PHP-FPM как «магию, которая где-то там отвечает на сокет», разложим на физическую картину.

1. Как выглядит жизнь одного запроса к WordPress

Возьмём запрос браузера на https://example.test/2026/10/post/ и проведём его по цепочке:

  1. nginx принимает запрос по 443 (см. Часть IV про TLS), видит по location ~ \.php$, что это динамика, и вызывает fastcgi_pass unix:/run/php/php8.2-fpm.sock; — то есть пишет «внутрь сокета».
  2. мастер-процесс PHP-FPM этого сокета не исполняет запрос. Он следит за пулом; очередь воркеров у него в подчинении.
  3. Один свободный воркер берёт соединение, исполняет PHP-код (index.php → WordPress → тема Kadence → запрос к базе), и возвращает nginx’у готовый HTML по тому же FastCGI-протоколу.
  4. nginx отдаёт HTML браузеру, а воркер освобождается и ждёт следующего запроса.

Ключевое: воркеров конечное число, и они заняты не весь день, а только пока исполняют запрос. Если все воркеры заняты (много гостей / тормозящий плагин), новый запрос стоит в очереди; если очередь переполнена — nginx получит отказ на сокет и вернёт 502 Bad Gateway. Именно поэтому «белый экран / 502 при наплыве» — это почти всегда про число и скорость воркеров, а не про nginx.

2. Почему PHP-FPM, а не другой способ подключить PHP

PHP можно исполнять по-разному. Для самописного VPS под WordPress реалистично сравнивать два:

  • mod_php (внутри Apache). PHP вшит в веб-сервер; каждый Apache-поток тащит интерпретатор с собой. Это тяжёло и «вязаное» — не разорёшь, не перезапустишь независимо. (nginx так не умеет, у него только FastCGI-путь.)
  • PHP-FPM (отдельная программа). PHP-исполнение вынесено в свой демон, общающийся с веб-сервером по сокету. nginx остаётся лёгким «фасадом», а PHP-процессы живут своей жизнью: их можно ограничивать по памяти, перегруппировывать по сайтам, перезапускать отдельно, не роняя фронтенд. Это то, что и ставили в Части III.

Плюс PHP-FPM даёт то, чего у mod_php нет: пулы. Можно дать каждому сайту свой набор процессов, своего пользователя и свои лимиты, живя при этом на одном установленном PHP (см. Часть V про изоляцию нескольких сайтов). Для того кто держит на одной машине несколько доменов (у вас не один же сайт), это и есть главное удобство.

3. Где живут файлы и как это всё зовётся

# какой PHP и какой FPM вообще стоит
php -v
ls /etc/php/                    # папки версиями: 8.1 8.2 8.3 …
# где конфиг пула и где php.ini для fpm-окружения
ls /etc/php/8.2/fpm/            # файлы этой версии
ls /etc/php/8.2/fpm/pool.d/     # пулы: www.conf и т.п. (один файл = один пул)
cat /etc/php/8.2/fpm/php-fpm.conf   # главный, он включает pool.d/*.conf
# как называется сам юнит (по нему делаем restart/reload)
systemctl status php8.2-fpm --no-pager | head -5

Два файла, которые мы будем править:

ФайлОбласть действияЧто задаём
/etc/php/8.2/fpm/php.iniвесь PHP этого окружения (все пулы и CLI-версия с другой копией php.ini)memory_limit, opcache.*, базовые upload_*/post_max_size, max_execution_time
/etc/php/8.2/fpm/pool.d/www.confодин пул ([www])режим pm, число детей, listen= (сокет), пользователь/группа, точечные php_admin_value

⛔ Фатал, перенесённый из конвенций Ветки B (как и в Части IV про systemd): если пишете свой цикл-воркер на PHP (у вас похожий ai-news-autopilot), никаких сетевых вызовов в первую строку до готовности процесса — только лениво, внутри рабочего цикла. PHP-FPM сам по себе ленив к старту, но кастомный «долгий» процесс должен уметь не умирать в первые секунды: иначе словите стартовый exit-code и рестарт-шторм в логах пула.


Часть II. Пул и его число: pm, pm.max_children и как считать память

Сердце настройки — секция [www] в www.conf. Разберём построчно и, главное, как выбрать числа для вашего VPS, а не «взять из статьи».

1. Режим pm — как пул набирает воркеров

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

  • pm = static — пул держит ровно pm.max_children процессов всегда. Ни больше, ни меньше. Минимум сюрпризов и минимум задержек «проснуться». Лучший выбор, когда памяти мало и вы точно знаете бюджет (на 512 МБ–1 ГБ static честнее dynamic: dynamic сначала плодит детей по запросу, и в пик может перескочить через вашу реальную память).
  • pm = dynamic (дефолт) — число детей плавает между pm.min_spare_servers и pm.max_spare_servers, но не больше pm.max_children. Пул сам гасит простаивающих и будит нужных. Оправдан на машине с запасом RAM и неравномерным трафиком.
  • pm = ondemand — процессы создаются только под запрос и умирают после простоя pm.process_idle_timeout. Экономит память на сайтах, к которым почти никто не ходит (лендинг, внутренний админ-домен, второй сайт «для проформы»). Минус: первый запрос после тишины «холодный».

2. Директивы pm.* под динамический режим

; /etc/php/8.2/fpm/pool.d/www.conf  (секция [www])
pm = dynamic
pm.max_children = 10            ; потолок одновременно живых воркеров
pm.start_servers = 3            ; сколько создать при старте пула
pm.min_spare_servers = 2        ; сколько всегда держать свободными (ниже — породит)
pm.max_spare_servers = 5        ; сколько свободных можно копить (выше — погасит)
pm.max_requests = 500           ; после скольких запросов воркер мягко пересоздаётся

Что делает каждая (важно не только число, но и смысл):

  • pm.max_children — главная цифра статьи. Максимум процессов, которые исполняют код одновременно. Все они держат память одновременно. Если у вас 10 детей и каждый ест ~60 МБ — это ~600 МБ только PHP-воркеров. Превысили физическую память — OOM-killer кого-то из них (или базу, или SSH!) прирежет.
  • pm.start_servers — сколько воркеров поднять сразу при старте пула (чтобы не было «холодного» первого запроса после рестарта).
  • pm.min_spare_servers / pm.max_spare_servers — нижняя и верхняя границы числа свободных (не занятых запросом) процессов. Пул старается, чтобы свободных было между этими числами: кончилось ниже min — родит детей; стало больше max — погасит лишних. (При static они не нужны; при dynamic это и есть регулятор.)
  • pm.max_requests — после скольки обработанных запросов воркер аккуратно пересоздаётся (master убивает старый, поднимает новый). Это против утечек памяти в плагинах: воркер, подлизавший 500 запросов, не утащит накопленный мусор в бесконечность. Ставят 500–1000; не 0 (без рестарта — при утечке пул разбухнет).

3. Как считать pm.max_children по памяти (формула, а не «у меня 40»)

Классическая ошибка — ставить max_children «на глаз» или копировать чужие цифры с VPS вдвое жирнее вашего. Считают так.

Шаг 1. Узнать, сколько памяти реально можно отдать PHP-FPM.

# свободная память на машине
free -h
# и сколько сейчас ест ВАС средний воркер:
ps -o rss= -C php-fpm8.2 --no-headers | awk '{s+=$1; n++} END {if(n) printf "воркеров: %d, средний RSS: %.1f МБ\n", n, s/n/1024}'

free -h покажет Available — сколько реально свободно с учётом кэшей. Из этого вычитаем запас на ОС, на базу (MariaDB живёт рядом, см. следующую статью №5), и swap как подстраховку (из Части V про фундамент). Не отдавайте PHP всю память — база и система задыхаются первыми, а OOM выбирает жертву по «вредности».

Шаг 2. Измерить память одного воркера под реальной нагрузкой. Не «в простое», а когда пул обслуживает живой WordPress-запрос. Значение RSS (resident set size — сколько процесс занимает физической памяти) из ps выше — это и есть ваш ориентир. Для типичного WordPress-воркера это обычно 40–80 МБ (без Opcache), с Opcache один воркер занимает меньше, потому что байткод разделяется.

Шаг 3. Делим.

pm.max_children ≈ (доступно_МБ − запас_МБ) / RSS_одного_воркера_МБ

Пример (числа условные, подставьте свои): доступ 1200 МБ, оставляем 400 МБ запасу/базе/ОС, один воркер ест 60 МБ → (1200−400)/60 ≈ 13. Берём с осторожностью вниз: pm.max_children = 12.

⚠️ Про Opcache и «память одного воркера». Opcache держит общий пул байткода в разделяемой памяти (одна копия на все воркеры, см. Часть IV) — это не на воркера, а на весь пул. Не складывайте memory_consumption Opcache в формулу «на воркер»: он съедает память из того же бюджета «доступно», поэтому формула становится pm.max_children ≈ (доступно − запас − opcache_memory_consumption − буфер_базы) / RSS_воркера. Проще: сначала включите Opcache и отберите ему память, потом считайте детей.

4. Проверка после правки

После изменения pm.* — перечитать конфиг пула без разрыва запросов (master перезагрузится и аккуратно сменит воркеров):

# синтаксис пула/конфига
sudo php-fpm8.2 -t
# мягко перезагрузить пул (не роняя соединения; systemd-терминология из Части III)
sudo systemctl reload php8.2-fpm
# посмотреть, сколько детей реально поднялось
ps -C php-fpm8.2 --no-headers | wc -l

Часть III. www.conf против php.ini: что и где задавать

Оба файла правят поведение PHP, но разными методами. Путаница порождает «я поставил лимит, а он не подействовал». Разберём слои.

1. Уровни настроек и приоритет

У PHP-настроек есть «скоуп» (где действует): PHP_INI_ALL (можно где угодно, хоть в рантайме), PHP_INI_SYSTEM (только в системном конфиге/пуле, не в рантайме), PHP_INI_PERDIR (в конфиге директории) и т.д. Для нас практически это значит:

  • php.ini — базовый слой. То, что задано здесь, действует для всех пулов этого php.ini. Это место для общих вещей: включение расширений, memory_limit, все opcache.*, дефолтные upload_max_filesize/post_max_size/max_execution_time, часовой пояс.
  • пул [www] — слой поверх, точечный и на конкретного пользователя/сайт. Здесь задают сокет, владельца процесса, число детей, и умеют переопределять php.ini-настройки через php_admin_value/php_value/php_flag — см. ниже.

Приоритет на практике: пул > php.ini для значений, которые пул объявил через php_admin_* или php_* директивы; сами php.ini-файлы между cli и fpm — разные копии (у вас в /etc/php/8.2/cli/php.ini и /etc/php/8.2/fpm/php.ini разное окружение!).

2. php_admin_value vs php_value vs php_flag — и почему это путают

В пуле можно задавать PHP-параметры напрямую. Три формы, разница принципиальная:

; /etc/php/8.2/fpm/pool.d/www.conf
php_admin_value[upload_max_filesize] = 32M   ; НЕ переопределяется ниже (ни кодом, ни ini-файлом в рантайме)
php_value[post_max_size]             = 34M   ; переопределяется ini_set()/.htaccess (мягкая)
php_flag[display_errors]             = off    ; логический on/off (как php_value, но для булевых)
  • php_admin_value[ключ] — значение вбивается наглухо: PHP не сможет его сменить из ini_set() в плагине или из .htaccess/.user.ini в рантайме. Так ставят то, что админ должен контролировать (безопасность, лимиты, disable_functions). Если ваш плагин «сам себе ставит лимит» и вы хотите, чтобы ваше слово было финальным — php_admin_value.
  • php_value[ключ] — мягкое: задаёт дефолт, но плагин может переопределить через ini_set() или пользовательский .user.ini. Ставят то, где рантайм-гибкость допустима.
  • php_flag[ключ] = on|off — то же, что php_value, но для булевых настроек (display_errors, engine); пишите on/off, а не 1/0.

3. Согласование лимитов загрузки: nginx (фронт), php.ini (ядро), пул (уточнение)

Из Части VI про nginx осталась ловушка: «лимитов два, а не один». Здесь закрываем окончательно. Чтобы загрузить файл размером X, он должен пройти три сита, и любое может его порезать:

  1. nginx — client_max_body_size (фронт, Часть VI). Режет тело запроса, отдаёт 413 Request Entity Too Large.
  2. php.ini — upload_max_filesize (макс одного загружаемого файла) и post_max_size (макс всего POST-тела; должен быть ≥ upload_max_filesize). Режет уже внутри PHP — это не «413», а молча пустой $_FILES / «файл не загружен» / белый экран.
  3. пул [www] — те же upload_max_filesize/post_max_size через php_admin_value, если хотите разное для разных сайтов на одном PHP.

Корректная связка (числа условные, ставьте под себя, согласованно):

; /etc/php/8.2/fpm/php.ini  — базовый дефолт
upload_max_filesize = 32M
post_max_size       = 34M
memory_limit        = 256M       ; на воркера, под ваш пик запроса (см. ниже про память)

; /etc/php/8.2/fpm/pool.d/www.conf — если этому пулу нужно крупнее
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size]       = 66M
php_admin_value[memory_limit]        = 384M

И в nginx-server: client_max_body_size 70m; (чуть больше POST, чтобы nginx не резал раньше PHP). Правило неравенств: client_max_body_size (nginx) > post_max_size (PHP) > upload_max_filesize (PHP).

Про memory_limit отдельно: это потолок памяти на один запрос в PHP, не на процесс. WordPress с тяжёлыми плагинами/редактором/экспортом может упереться в 128M (дефолт по умолчанию у части сборок) и упасть с «Allowed memory size exhausted» (это не 502, а белый экран / fatal в error.log). Поднимают до 256M–512M, но помните: memory_limit × pm.max_children ≈ реальный пик памяти пула; не ставьте 512M при max_children=12 — это уже 6 ГБ на машину, которой и так тесно.

4. www.conf как файл пула, а не только про PHP

Помимо php_*, в секции [www] живут и чисто-FPM настройки, которые нам пригодятся в Части V:

[www]
user  = www-data                 ; от кого работают воркеры этого пула
group = www-data
listen = /run/php/php8.2-fpm.sock ; сокет этого пула (файл, права)
listen.owner = www-data          ; владелец файла сокета (его читает nginx)
listen.group = www-data
listen.mode  = 0660              ; права на сокет: читают лишь owner/group (безопасно)

listen.mode 0660 + listen.owner/group — это защита: до сокета допускаются только nginx (в группе www-data) и root. Не ставьте 0666 (все подряд смогут слать запросы вашему PHP).


Часть IV. Opcache: байткод в памяти и «прогрев» кэша

Это раздел с самым большим отношением «прирост скорости / затраченные усилия». Если на вашем PHP-FPM Opcache выключен или работает с дефолтами — включение и правильные параметры заметно снижают нагрузку на CPU и ускоряют отклик страницы, потому что WordPress больше не перекомпилирует одни и те же файлы на каждый запрос.

1. Что делает Opcache простыми словами

PHP-код — текст. Перед исполнением интерпретатор компилирует каждый .php в байткод. Без кэша это происходит на каждый запрос: WordPress тянет ядро, тему, десятки плагинов — их файлы компилируются заново каждый раз. Opcache (OPcode cache) хранит этот байткод в общем сегменте разделяемой памяти: первый запрос скомпилировал — дальше все воркеры берут готовый байткод. Компиляция — это чистый CPU, и Opcache именно его экономит.

Есть ещё var cache (кэш пользовательских переменных, apcu) — он кэширует данные, а не код; это отдельная тема, ниже лишь упомянем, что на WordPress его тоже полезно иметь (не смешивайте: Opcache — про байткод, APCu — про данные/сессии).

2. Включаем и задаём параметры

Всё в /etc/php/8.2/fpm/php.ini (внимание: секция [opcache] в php.ini обычно пишется как opcache.имя):

; /etc/php/8.2/fpm/php.ini
opcache.enable=1                       ; сам переключатель (для FPM)
; opcache.enable_cli=0                 ; для CLI не включаем, если не нужно (CLI-процесс одноразовый)

; сколько памяти выделить под байткод, МБ (ОДИН общий сегмент на все воркеры)
opcache.memory_consumption=192

; отдельный маленький буфер под строковые константы файлов (дефолт 16, для WP часто ставят 32–64)
opcache.interned_strings_buffer=32

; максимум кэшируемых файлов; если файлов больше — старые вытесняются раньше времени
opcache.max_accelerated_files=20000

; проверять изменённые файлы каждые N секунд вместо каждой проверки (0 = только руками после деплоя)
opcache.revalidate_freq=60

; в продакшене: НЕ проверять, не менялся ли файл (самый большой выигрыш CPU),
; сброс делаем ЯВНО после обновления. ⚠️ см. грабли ниже
opcache.validate_timestamps=1          ; новичку держим 1; 0 — только с дисциплиной сброса

; безопасно для sharedhosting с кучей путей
opcache.fast_shutdown=1

Подбор memory_consumption: WordPress с плагиной (ядро+тема+плагины) компилируется в байткод размером в десятки-сотни МБ. 64–128 МБ хватает на скромный сайт, 192–256 МБ — на средний. Как понять, что не хватает, — смотрим ниже счётчиками.

3. Грабли validate_timestamps и «почему сайт не обновился»

opcache.validate_timestamps=1 (безопасный дефолт): Opcache периодически смотрит, не трогали ли файл. Период задаёт opcache.revalidate_freq (секунды). При 1 и revalidate_freq=60 вы будете подхватывать изменения файлов до минуты позже — это нормально для продакшена, но не «мгновенно».

opcache.validate_timestamps=0 (агрессивный режим): Opcache вообще не смотрит на файлы — байткод считается вечно валидным. Это экономит ещё CPU (нет stat() по каждому файлу), но: обновили плагин/тему — а воркеры крутят СТАРЫЙ байткод, пока вы не сбросите кэш. Отсюда классическая ситуация «залил исправление, а сайт по-прежнему битый». При 0 обязан порядок после всякого изменения кода:

# сбросить Opcache без перезагрузки пула (нужен CLI доступ того же SAPI)
# вариант 1 — рестарт пула (грубо, но гарантированно):
sudo systemctl reload php8.2-fpm
# вариант 2 — точечно по API (если есть скрипт сброса):
sudo php -r 'opcache_reset();'   # работает для CLI-SAPI; для FPM-SAPI нужен скрипт-хук

Практика для одного человека на self-hosted: держите validate_timestamps=1 с revalidate_freq=60. Получили «изменения подхватываются с задержкой до минуты» и не словите «залил и не работает». В 0 переходят только когда замерите, что stat-вызовы реально жгут CPU на холодном старте, и приучите себя/CI сбрасывать кэш после деплоя (см. CI/CD своими словами).

4. Прогрев (warm-up) и как смотреть счётчики

Проблема cold start. После рестарта пула/перезагрузки Opcache пуст: первые запросы дорогие (компилируют файлы заново). На оживлённом сайте это «просадка отклика на первую сотню хитов». Прогрев (warm-up) — заранее «прогнать» основные URL/файлы, чтобы они скомпилировались до прихода реальных гостей.

# примитивный прогрев: обойти главную и пару тяжёлых страниц, игнорируя тело
curl -s -o /dev/null https://example.test/
curl -s -o /dev/null https://example.test/wp-login.php
curl -s -o /dev/null https://example.test/feed/
# если есть wp-cli (WP-CLI), есть и аккуратные обходы/грелки; встраивается в cron или
# в systemd-таймер (механика — см. Части II/III про systemd) сразу после reload пула

Оцифровать «хорошо ли живёт Opcache» можно только через счётчики. Проще всего — скрипт, выводящий opcache_get_status() (затем удалить/закрыть; статус-скрипт не должен быть публичным):

<?php
// /var/www/example.test/opcache-status.php — ВРЕМЕННЫЙ диагностический файл, закрыть
echo json_encode(opcache_get_status(false), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// Ключевые поля: opcache_enabled, memory_usage.used_memory, statistics.num_cached_scripts,
// statistics.missed_scripts (промахи — много = мало памяти/частые изменения),
// statistics.blacklist_miss_ratio.

Что смотреть в нём:

  • used_memory близко к memory_consumption + num_cached_scripts растёт без падения — памяти хватает.
  • missed_scripts много и растёт — байткод вытесняется: поднимите memory_consumption/max_accelerated_files.
  • hash_updates_in_cycle / num_hits и num_misses — hits/misses показывает, как часто байткод перекомпилируется.

5. Про APCu и сессии одной строкой

Opcache кэширует код. APCu (apcu.enable=1) кэширует данные: транзакционные объекты, и главное — объект-кэш WordPress (wp-content/object-cache.php, WP_OBJECT_CACHE). На WordPress с APCu-бэкендом админ-запросы (которых не кэширует страница) заметно легче; сессии можно тоже вынести в APCu (session.save_handler). Это тема Кеширование из серии про архитектуру; здесь зафиксировали, что «Opcache есть» и «APCu тоже стоит включить» — но не смешивать: это два разных кэша.


Часть V. Изоляция нескольких сайтов на одном PHP-FPM пулами

У вас не один сайт, и хочется, чтобы один «затормозивший» не тянил за собой остальные. Именно это решают пулы — и это вторая по ценности часть статьи после pm.max_children.

1. Идея: один PHP, много пулов, разные сайты

php-fpm.conf включает pool.d/*.conf, и .conf-файл с секцией [имя] — это отдельный пул. Дефолтный — [www] в www.conf. Вы добавляете: example.test.conf с секцией [example], shop.test.conf с [shop] и т.д. Каждый пул:

  • свой сокет (listen = /run/php/php8.2-fpm-example.sock),
  • свой воркер-пользователь (user = site_example),
  • свои pm.*-числа и php_admin_value (лимиты/память).

В nginx у соответствующего server этого домена ставите fastcgi_pass unix:/run/php/php8.2-fpm-example.sock; (см. Часть III) — и весь трафик домена пойдёт в его пул.

2. Что это даёт по факту

  • Файрвол от соседей по нагрузке. Один сайт словил DDoS/тяжёлый cron — кончились его воркеры → у него 502, а второй сайт живёт в своём пуле со своими детьми. Без пулов (всё в один [www]) «жадный» сайт забивает всех.
  • Разные права и безопасность. user = site_example и group = site_example + open_basedir в пуле → процессы этого сайта физически не читают файлы чужого сайта. Это серверная половина изоляции (вторую половину — пользователь WordPress и права файлов — см. Часть V про права и authorized_keys).
  • Разные лимиты на запрос. У «медиа-сайта» крупный upload_* и memory_limit, у лендинга — мелкий: php_admin_value в пуле позволяет не раздувать лимиты ради всех.

3. Шаблон пула и «кому нужен лишний пул»

HTML
; /etc/php/8.2/fpm/pool.d/example.test.conf
[example]
user  = site_example
group = site_example
listen = /run/php/php8.2-fpm-example.sock
listen.owner = www-data
listen.group = www-data
listen.mode  = 0660

pm = ondemand                ; сайт с малым трафиком — не держим детей зря (см. Части II)
pm.max_children = 5
pm.process_idle_timeout = 10s
pm.max_requests = 300

php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size]       = 66M
php_admin_value[memory_limit]        = 256M
php_admin_value[open_basedir]        = /var/www/example.test:/tmp:/var/lib/php/sessions
php_admin_value[session.save_path]   = /var/www/example.test/sessions ; своя папка сессий

; медленный лог и пороги — см. Часть VI
slowlog = /var/log/php-fpm/example-slow.log
request_slowlog_timeout = 5s

Плюсы/минусы пулов честно: плюсы — изоляция нагрузки, раздельные права и лимиты, точечный slowlog на сайт. минусы — суммарное число детей по всем пулам не должно превышать память машины (суммируйте pm.max_children × RSS по каждому пулу в один бюджет); на совсем маленьком VPS для второго сайта проще ondemand, а не static-пул; на одном сайте пулы не нужны, достаточно одного [www]. Не раздувайте количество: изоляция нужна там, где есть конкуренция за ресурсы.


Часть VI. slowlog и error log: как узнать, кто тормозит, без гаданий

«Страница открылась за 4 секунды. Почему?» WordPress не скажет. PHP-FPM скажет — если включить. Разберём два разных лога, которые новички путают.

1. Две разные беды — два разных лога

ЛогФайл (обычно)Что ловит
PHP error log/var/log/php8.2-fpm.log (или как в php.ini: error_log)фатальные ошибки, warning, «memory exhausted», «Allowed memory size», фаталы плагинов. Отвечает на «почему упало».
slowlog пулаslowlog в секции [pool] (например /var/log/php-fpm/example-slow.log)стек-трейс того, что выполняется дольше request_slowlog_timeout. Отвечает на «что именно тормозит».

Про первый, error log, с точки зрения systemd-терминов — PHP-FPM пишет и туда, и в syslog/journal; читать можно и так:

# ошибки PHP-FPM через journal (юнит-лог; см. Части V про диагностику systemd)
sudo journalctl -u php8.2-fpm -p err --since today --no-pager
# или в файле
sudo tail -n 50 /var/log/php8.2-fpm.log

2. Включаем slowlog и «ловим» тормозящий запрос

slowlog не включён по умолчанию (в стандартных пулах его часто нет). Включается двумя директивами в пуле:

; /etc/php/8.2/fpm/pool.d/www.conf
slowlog = /var/log/php-fpm/$pool-slow.log   ; $pool = имя пула; папка должна существовать и быть прав пула
request_slowlog_timeout = 5s                ; дольше 5 сек воркер исполняет запрос → пишем трейс

; опционально: ловить не только «долго», но и «застряло» (по умолчанию выкл)
; request_terminate_timeout = 60s           ; убить запрос, идущий дольше 60 сек (защита от зависших)

request_slowlog_timeout — ключевая директива. Заставляет FPM: если воркер на запросе дольше порога — сделать дамп трейса выполнения в slowlog, не прерывая запрос (он продолжится; мы просто зафиксируем «где он висел»). Это и есть ответ на «какой плагин/функция тормозит».

Как выглядит запись в slowlog:

[2026-10-17 20:01:12]  [pool example] pid 8123
script_filename = /var/www/example.test/index.php
[0x...] wp_get_attachment_image() /var/www/example.test/wp-includes/media.php:712
[0x...] ... kadence_post_thumbnail() /var/www/example.test/wp-content/themes/kadence/inc/.../post-thumbnail.php:33
# ↑ вот эта последняя функция/файл сверху — это и есть «кто тормозит»,
#   в отличие от error log, который скажет только «упало».

Вы видите цепочку вызовов (backtrace) от PHP-FPM-ядра вниз до конкретного файла WordPress/темы/плагина. Идёте «снизу вверх» — самая верхняя ваша строка (в wp-content/... или wp-includes/...) обычно и есть виновник: медленная функция плагина, WP_Query без ограничения, генерация миниатюры, внешний HTTP-запрос (transients/REST к стороннему API).

3. Как читать slowlog осмысленно

# показать последние 50 строк и группировать по верхним фреймам
sudo tail -n 50 /var/log/php-fpm/example-slow.log
# быстро увидеть, кто самый частый «тормоз» (верхний не-ядерный фрейм):
sudo grep -hoP '.*\.php:\d+' /var/log/php-fpm/example-slow.log | sort | uniq -c | sort -rn | head

Правило чтения: если фреймы повторяются из запроса в запрос в одном и том же файле плагина — это плагин, ищем у него кэш/оптимизацию или отключаем. Если тормозит wp-includes/... на тяжёлых WP_Query/get_posts — вероятна база: оптимизация запросов/индексов (см. Оптимизация баз данных), это тема следующей статьи про MariaDB.

4. Пороги и «не спамить»

  • request_slowlog_timeout = 5s — разумный старт: фиксирует явно-медленные запросы, не засоряя лог «нормальными по 1-2 сек». На плагинных «нормальных 1-3 сек» ставьте выше.
  • request_terminate_timeout — уже убивает запрос дольше порога (по умолчанию не задан = ждём сколько PHP max_execution_time). Нужен, если есть зависающие намертво запросы (внешние API, которые молчат), но он обрывает и «полезные длинные» операции (импорт, генерация больших миниатюр) — ставьте осознанно и выше реальных рабочих-операций.
  • Не включайте slowlog в request_slowlog_timeout = 0 — это выключает лог; медленные останутся загадкой.

5. Куда смотреть дальше: мост к диагностике

slowlog отвечает «что в PHP тормозит». Но если тормозит база, медленный запрос будет виден как mysqli-фрейм в том же slowlog. Дальнейший шаг — посмотреть SHOW PROCESSLIST и медленный лог сервера (статья №5 про MariaDB) и связать: «PHP-воркер висел в get_posts() → база держала долгий SELECT → нет индекса». Это метод от верхнего к нижнему (сайт → nginx → php-fpm → db → ОС), которым построена финальная статья «Диагностика: когда сервер „не дышит“». Здесь вы держите второй этаж этой лестницы.


Инструменты под задачу

Не уверены, сколько воркеров выдержит ваш VPS? Введите реальную память машины, swap и примерный размер одного PHP-процесса — калькулятор посчитает безопасный pm.max_children (и start/min_spare/max_spare для dynamic), а ИИ даст экспертное заключение именно по вашим числам, без гаданий.

Калькулятор бюджета пула PHP-FPM
Гость
Токены: 0 / 50 000
Расчёт бесплатен и идёт прямо в браузере. Токены тратятся только на кнопку «Экспертное заключение ИИ».
Заполните поля (или возьмите пресет) и нажмите «Рассчитать бюджет» — pm.max_children посчитается мгновенно. Затем можно запросить экспертное заключение ИИ.

Чек-лист граблей PHP-FPM

  • `pm.max_children` «на глаз» больше, чем делится память на воркера → OOM-killer съедает детей (и, что страшнее, базу/SSH). Считайте по `free` и реальному RSS.
  • `static` при высокой и редкой нагрузке держит детей «впрок» и жрёт память зря; `ondemand`/`dynamic` — по задаче.
  • Забыли `pm.max_requests` → при утечке в плагине пул разбухает; дети не пересоздаются.
  • Правки в `/etc/php/8.2/cli/php.ini` вместо `…/fpm/php.ini` → «настроил, не подействовало»: CLI и FPM имеют разные php.ini.
  • `php_admin_value` и `php_value` перепутали: хотите запретить плагину менять лимит — нужен `admin`, а не мягкий `php_value`.
  • `memory_limit` (на запрос) поднят до 512M при `max_children=12` → фактический пик пула ~6 ГБ → обвал.
  • Ограничили лимиты только в PHP, не в nginx → снова `413`; или наоборот. Все три сита должны быть согласованы (`client_max_body_size` > `post_max_size` > `upload_max_filesize`).
  • `opcache.validate_timestamps=0` без сброса после обновления → «залил — а старый код живёт». Либо держите `=1`, либо приучите себя сбрасывать кэш.
  • `memory_consumption` Opcache не посчитан в бюджете: Opcache-сегмент **плюсом** к детям; суммируйте при расчёте `max_children`.
  • `slowlog` на путь, которого нет или нет прав у пула → лог не пишется и вы «вслепую» чините тормоза. Создайте папку и проверьте владельца (user пула).
  • `listen.mode 0666` на сокете пула → чужие процессы пишут запросы в ваш PHP; ставьте `0660` + правильный owner/group.
  • Пулы плодят суммарных детей больше, чем память: суммируйте `pm.max_children` по всем пулам.

Материал написан простым языком; любой англоязычный термин (PHP-FPM, master, воркер, пул, FastCGI, RSS, pm, Opcache, байткод, warm-up, slowlog, backtrace, ondemand) объясняется при первом употреблении. Конфиги — для Ubuntu/Debian с PHP-FPM из apt; конкретные версии и пути (8.2 против 8.3, php8.2-fpm.sock против php8.3-fpm.sock, имя юнита php8.2-fpm) сознательно условны — проверяйте фактические через php -v, systemctl list-units | grep fpm и содержимое pool.d/. Домены и пути условные (example.test, /var/www/example.test); подставляйте свои. После правки пула — php-fpm8.2 -t и systemctl reload php8.2-fpm; после правки php.ini — рестарт пула (reload перечитывает pool.d, php.ini обычно при restart). Тонкие механизмы (pm.status_path + живой статус-эндпоинт FPM, emergency_restart_interval/restart_request/rlimit_*, catch_workers_output, clear_env, TCP-listen для удалённой БД, listen.allowed_clients) упомянуты лишь в контексте необходимости — это уровень man php-fpm, когда базовый WordPress-стек уже живёт и стабилен.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.