<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
DEVOPS · MIDDLE ЧАСТО ПИТАЮТЬ

Як nginx передає запит у PHP-FPM і які налаштування пулу найважливіші?

nginx сам PHP не виконує: він упаковує запит у FastCGI-записи й віддає їх у сокет пулу PHP-FPM, де вільний воркер виконує скрипт із `SCRIPT_FILENAME`. Маршрутизацію задає `try_files`, пропускну здатність - `pm` і `pm.max_children`, який вважають від реально вільної памʼяті, а видимість проблем дають `slowlog` і `request_terminate_timeout`.

Під навантаженням сайт спочатку гальмує, а потім віддає 502. У логах nginx `connect() to unix:/run/php/php8.4-fpm.sock failed (11: Resource temporarily unavailable)`. Що відбувається?
Скільки б ви поставили `pm.max_children` на сервері з 8 ГБ, де ще живе MySQL, і як це число обґрунтуєте?
Чому FPM іноді віддає `File not found`, хоча файл у теці точно є?
Чим `request_terminate_timeout` відрізняється від `max_execution_time` і навіщо тримати обидва?
nginx PHP-FPM FastCGI продакшн тюнінг

PHP виконує не nginx, а PHP-FPM; nginx тут лише FastCGI-клієнт. Коли запит підпадає під location ~ \.php$, nginx серіалізує метод, шлях, заголовки, тіло й набір CGI-змінних у бінарні записи протоколу FastCGI і пише їх у сокет пулу. Master-процес PHP-FPM слухає цей сокет, вільний воркер забирає запит, виконує файл і повертає stdout, який nginx розбирає на заголовки та тіло відповіді. Ключова змінна тут SCRIPT_FILENAME: FPM відкриває рівно той шлях, який у ній прийшов, тому помилка в root дає Access denied або Primary script unknown від FPM, а не 404 від nginx. Класична пастка: підключити fastcgi_params замість fastcgi.conf. У першому файлі SCRIPT_FILENAME немає взагалі, у другому вже прописано $document_root$fastcgi_script_name.

За маршрутизацію відповідає try_files. Для Laravel root дивиться в public/, і рядок try_files $uri $uri/ /index.php?$query_string читається так: є такий файл або тека, віддай з диску, і статика ніколи не турбує PHP; немає, заводь усе у фронт-контролер. Збережений ?$query_string потрібен, бо внутрішній редирект без нього втрачає GET-параметри. Другий try_files, уже всередині location ~ \.php$, виглядає зайвим, але саме він закриває path info: regexp \.php$ матчить і /uploads/avatar.jpg/x.php, а try_files $fastcgi_script_name =404 не пускає у FPM шлях, якого немає на диску. Дзеркальний випадок, коли у FPM намагаються протягнути .jpg як скрипт, прикриває security.limit_extensions (за замовчуванням .php).

Пропускну здатність задає група pm. Режим dynamic тримає від pm.start_servers процесів і дихає між pm.min_spare_servers і pm.max_spare_servers, не перевищуючи pm.max_children; розумний дефолт для сервера зі змінним трафіком. static форкає max_children одразу й ніколи їх не гасить: у контейнері з фіксованим лімітом памʼяті це навіть краще, бо немає витрат на fork під піком і поведінка передбачувана. ondemand не тримає жодного воркера, поки не прийде запит, і гасить простійні через pm.process_idle_timeout; він для машини з двома десятками малотрафікових сайтів, де памʼять важливіша за першу мілісекунду відповіді. Окремо стоїть pm.max_requests: воркер самознищується після N запитів, що прикриває тічки в сторонніх розширеннях. OPcache при цьому не страждає, бо його SHM-сегмент належить master-процесу, і новий воркер дістає вже прогрітий кеш.

pm.max_children виводять з памʼяті, а не з інтуїції. Спочатку виміряйте реальне споживання: ps -o rss,cmd -C php-fpm8.4 --sort=-rss покаже RSS, але він подвійно рахує спільні сторінки, тому чесніше число дає smem -P php-fpm у колонці PSS. Далі віднімайте від памʼяті машини все, що там уже живе (ОС, nginx, MySQL, Redis), лишайте 10-15% запасу й діліть на споживання воркера: 8 ГБ мінус 1.5 ГБ на MySQL, мінус 1 ГБ на решту, залишок 4.8 ГБ, і при воркері на 100 МБ виходить десь 45 процесів. У контейнері рахуйте від ліміту cgroup: памʼять хоста тут ні до чого, і воркери, які в неї не влізли, знімає OOM killer. Другий обмежувач, про який забувають, це CPU: для CPU-bound коду сотня процесів на чотирьох ядрах лише розтягне латентність усім, тоді як застосунок, що переважно чекає на БД і зовнішні API, справді виграє від більшої кількості воркерів. Коли ліміт вичерпано, запити не падають одразу: вони стоять у listen.backlog (за замовчуванням 511), сайт гальмує, а 502 з Resource temporarily unavailable приходить уже після переповнення черги.

Видимість тримається на двох таймаутах. request_slowlog_timeout = 5s разом із slowlog нікого не вбиває: FPM просто знімає бектрейс запиту, який перетнув межу, і пише його у файл з іменем файлу, функцією й номером рядка на кожному кадрі (глибина керується request_slowlog_trace_depth). Коли APM немає, це найдешевший спосіб дізнатися, що саме гальмує в продакшені. request_terminate_timeout натомість знімає воркера сигналом і лишає в логу execution timed out ... terminating. Він не дублює max_execution_time: PHP-ний таймер на Unix не рахує час, проведений у системних викликах, тобто зависла відповідь від зовнішнього API чи запит, що чекає на блокування в БД, для max_execution_time триває нуль секунд. За жорстке зняття платять тим, що код не добігає до finally й register_shutdown_function, а незакомічена транзакція відкатується вже по розриву зʼєднання; тому значення ставлять із запасом над реальним p99, а не «щоб швидше». І нехай fastcgi_read_timeout у nginx буде на кілька секунд більшим: інакше nginx відвалиться першим, у логах FPM не буде жодного сліду, а воркер продовжить працювати на нікого.

# /etc/nginx/sites-enabled/app.conf - усе, крім .php, віддає nginx
root /var/www/app/current/public;
index index.php;

location / {
    # файл або тека є - віддати з диску; решта йде у фронт-контролер
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    # без цього рядка /uploads/avatar.jpg/x.php дійде до FPM
    try_files $fastcgi_script_name =404;

    include fastcgi.conf;          # саме fastcgi.conf: у ньому вже є SCRIPT_FILENAME
    fastcgi_pass unix:/run/php/php8.4-fpm-app.sock;
    fastcgi_read_timeout 35s;      # більше за request_terminate_timeout
    fastcgi_hide_header X-Powered-By;
}

# /etc/php/8.4/fpm/pool.d/app.conf - хто виконує PHP
[app]
user = app
listen = /run/php/php8.4-fpm-app.sock
listen.owner = www-data            # інакше nginx: Permission denied
listen.mode = 0660

pm = dynamic
pm.max_children = 32               # 3200 МБ вільних / 100 МБ реального RSS
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 800              # перезапуск воркера: страховка від тічки
pm.status_path = /fpm-status       # listen queue, max children reached

slowlog = /var/log/php-fpm/app-slow.log
request_slowlog_timeout = 5s       # бектрейс живого запиту, воркер живий
request_slowlog_trace_depth = 30
request_terminate_timeout = 30s    # а тут SIGTERM воркеру + запис у лог
Що nginx працює як FastCGI-клієнт: серіалізує метод, шлях, заголовки й тіло в записи протоколу, а ключовий параметр `SCRIPT_FILENAME` вказує FPM точний файл на диску; неправильний `root` дає `Primary script unknown`, а не 404 від нього.
Що `try_files $uri $uri/ /index.php?$query_string` віддає статику nginx і лише решту заводить у фронт-контролер, а в `location ~ \.php$` потрібен `try_files $fastcgi_script_name =404`, щоб у FPM не потрапляли шляхи неіснуючих файлів.
Різницю pm-режимів: `dynamic` тримає вилку `min_spare`/`max_spare` і дихає під трафіком, `static` одразу форкає `max_children` (контейнер із відомим лімітом), `ondemand` піднімає воркери під запит і гасить їх через `pm.process_idle_timeout` (багато тихих сайтів на одній машині).
Формулу `max_children = (вільна памʼять) / (реальний RSS воркера)`, із зауваженням, що `memory_limit` тут не множник, а аварійна стеля, і що в контейнері вважають від ліміту cgroup, а не від памʼяті хоста.
Що `request_slowlog_timeout` пише бектрейс живого запиту й нічого не вбиває, а `request_terminate_timeout` знімає воркера сигналом і ловить те, що `max_execution_time` пропускає - блокуючі системні виклики й запити в БД.
Піднімати `pm.max_children` до сотні «щоб не було 502» на машині з 4 ГБ: воркери йдуть у swap, і замість помилки під навантаженням отримують повну зупинку або OOM killer, який зносить MySQL.
Вважати `max_children` як `RAM / memory_limit`: `memory_limit = 256M` не означає, що кожен воркер зʼїдає 256 МБ; типовий Laravel-воркер тримає 50-120 МБ RSS.
Писати `location ~ \.php$ { fastcgi_pass ...; }` без `try_files`, лишивши `cgi.fix_pathinfo=1`: запит на `/uploads/avatar.jpg/x.php` дійде до FPM, який за path info підставить завантажений файл.
Підключати `fastcgi_params` замість `fastcgi.conf` і не додавати `SCRIPT_FILENAME` руками: у `fastcgi_params` цього параметра немає, і кожен запит на PHP падає з `Primary script unknown`.
Ставити `fastcgi_read_timeout` меншим за `request_terminate_timeout`: nginx відвалюється першим, у логах FPM нічого немає, а воркер ще хвилину домолочує вже нікому не потрібний запит.
Забути `listen.owner = www-data` і `listen.mode = 0660` після переїзду на окремий пул - сокет створюється від root, nginx отримує `Permission denied`.
ПОРАДА

Скажіть, що 502 під навантаженням майже ніколи не про nginx: він лише не дочекався сокета. Далі назвіть три місця, куди дивитесь по черзі - `max children reached` у `pm.status_path`, `listen queue` там само і рядки про `execution timed out` у логі FPM. Людина, яка розділяє «пул переповнений» і «PHP упав», звучить як та, що тримала продакшен, а не читала howto.

оновлено 30 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`max_children` - це стеля одночасно живих процесів, кожен з яких тримає свій RSS постійно, а не лише під запитом. Тому число виводять із вільної памʼяті, поділеної на виміряне споживання воркера; `memory_limit` тут аварійна стеля на один запит, а не середнє.