Эволюция: CGI, FastCGI, PHP-FPM
CGI (Common Gateway Interface)
CGI -- первый способ запуска PHP на веб-сервере. Для каждого HTTP-запроса веб-сервер создавал новый процесс PHP-интерпретатора, который обрабатывал скрипт и завершался. Это означало полную инициализацию PHP на каждый запрос: загрузка расширений, парсинг конфигурации, подключение к базе данных.
HTTP Request → Nginx/Apache → fork() → PHP process → Response → process dies
HTTP Request → Nginx/Apache → fork() → PHP process → Response → process dies
HTTP Request → Nginx/Apache → fork() → PHP process → Response → process dies
Проблемы CGI очевидны: fork() системный вызов стоит дорого, инициализация PHP занимает время, соединения с БД создаются заново. При 100 запросах в секунду сервер порождает и убивает 100 процессов -- это колоссальная нагрузка на ОС.
FastCGI
FastCGI решил главную проблему CGI -- процессы стали постоянными. Вместо fork/die на каждый запрос, набор воркеров запускается заранее и обрабатывает запросы последовательно. Соединение между веб-сервером и FastCGI-процессами поддерживается через Unix-сокет или TCP.
┌─ Worker 1 (persistent)
HTTP Request → Nginx ─┼─ Worker 2 (persistent)
└─ Worker 3 (persistent)
Однако стандартная реализация FastCGI для PHP (php-cgi) не умела управлять процессами -- не было graceful restart, мониторинга, adaptive spawning. Нужен был менеджер процессов.
PHP-FPM (FastCGI Process Manager)
PHP-FPM -- это продвинутая реализация FastCGI, созданная специально для PHP. Изначально существовал как патч к PHP (автор -- Andrei Nigmatulin), а с PHP 5.3.3 включён в ядро.
Что добавил PHP-FPM поверх FastCGI:
- Менеджер процессов с тремя режимами (static, dynamic, ondemand)
- Graceful restart без потери запросов (
kill -USR2) - Slow log для отладки медленных скриптов
- Emergency restart при массовых падениях воркеров
- Раздельные пулы -- разные приложения с изоляцией
- Страница статуса для мониторинга в реальном времени
- Контроль таймаутов на уровне процесс-менеджера
Сегодня PHP-FPM -- стандарт де-факто для production-развёртывания PHP-приложений.
Режимы менеджера процессов (pm)
Директива pm определяет стратегию управления воркерами. Выбор режима критически влияет на потребление памяти и отзывчивость приложения.
static -- фиксированное количество воркеров
; /etc/php/8.4/fpm/pool.d/www.conf
[www]
pm = static
; Fixed number of workers -- always running, always consuming memory
pm.max_children = 50
; Restart worker after N requests to prevent memory leaks
pm.max_requests = 1000
При pm = static PHP-FPM запускает ровно pm.max_children воркеров при старте и поддерживает это число постоянно. Процессы не создаются и не уничтожаются в зависимости от нагрузки.
Преимущества:
- Предсказуемое потребление памяти (всегда одинаковое)
- Нет задержек на создание процессов при пике нагрузки
- Нет CPU-оверхеда на fork/kill
Недостатки:
- Память занята, даже если нагрузки нет (ночью, в выходные)
- Нужен точный расчёт -- если мало воркеров, запросы будут ждать в очереди
Когда использовать: выделенные серверы со стабильно высокой нагрузкой, где трафик предсказуем и RAM выделена только под PHP.
dynamic -- адаптивное масштабирование
[www]
pm = dynamic
; Upper limit -- never exceed this number of workers
pm.max_children = 50
; Workers to start initially
pm.start_servers = 10
; Minimum idle workers ready to accept new requests
pm.min_spare_servers = 5
; Maximum idle workers (excess killed to free memory)
pm.max_spare_servers = 20
; Kill idle workers above min_spare after this timeout
pm.process_idle_timeout = 10s
; Restart worker after N requests (memory leak protection)
pm.max_requests = 1000
В режиме dynamic PHP-FPM активно управляет числом процессов: если свободных воркеров меньше min_spare_servers -- создаются новые, если больше max_spare_servers -- лишние убиваются. Это баланс между потреблением памяти и скоростью отклика.
Как это работает на временной шкале:
Startup: 10 workers (pm.start_servers)
Low traffic: 5 workers (pm.min_spare_servers)
Growing: 15 workers (scaling up)
Peak: 50 workers (pm.max_children -- ceiling)
After peak: 20 workers (pm.max_spare_servers -- excess killed)
Night: 5 workers (pm.min_spare_servers)
Когда использовать: большинство production-серверов. Это самый распространённый и рекомендуемый режим для веб-приложений с переменной нагрузкой.
ondemand -- создание по запросу
[www]
pm = ondemand
; Maximum workers if all are busy
pm.max_children = 50
; Kill idle worker after this timeout
pm.process_idle_timeout = 10s
; Restart worker after N requests
pm.max_requests = 1000
Режим ondemand -- самый экономный по памяти. При старте PHP-FPM не создаёт ни одного воркера. Процесс порождается только когда приходит запрос, а после периода простоя (process_idle_timeout) -- уничтожается.
Преимущества:
- Минимальное потребление памяти в состоянии покоя (0 воркеров)
- Идеален для серверов с множеством сайтов (shared hosting)
Недостатки:
- Первый запрос после простоя получает задержку на fork (~50-100ms)
- При резком всплеске трафика все запросы ждут создания воркеров
Когда использовать: малопосещаемые сайты, staging-окружения, серверы с десятками пулов.
Ключевые параметры пула с формулами расчёта
pm.max_children -- главный параметр
Это максимальное число одновременно работающих PHP-процессов. Самый критичный параметр: слишком мало -- запросы встают в очередь; слишком много -- сервер уходит в swap и всё замирает.
Формула расчёта:
pm.max_children = (Total_RAM - OS_overhead - DB_memory - Cache_memory - Buffer) / Avg_PHP_process_memory
Как измерить средний расход памяти одного воркера:
# Method 1: Check RSS of FPM workers under real load
ps -eo pid,rss,command | grep 'php-fpm: pool' | awk '{sum += $2; n++} END {print "Average: " sum/n/1024 " MB", "Count: " n}'
# Method 2: Check via FPM status page (see monitoring section)
curl -s http://127.0.0.1/fpm-status?full | grep 'last request memory'
# Method 3: Check peak memory in PHP code
# memory_get_peak_usage(true) -- real allocated memory
Пример расчёта для сервера 8 GB:
Total RAM: 8192 MB
OS reserved: -1024 MB
PostgreSQL: -2048 MB
Redis: -512 MB
Nginx: -128 MB
Safety buffer: -512 MB
──────────────────────────────────
Available for PHP-FPM: 3968 MB
Average process memory: 80 MB (measured under load!)
pm.max_children = 3968 / 80 = 49 → set to 49
Критическое правило: Всегда ИЗМЕРЯЙТЕ реальное потребление памяти под нагрузкой. Не угадывайте. Laravel-приложение может потреблять 40 MB на простых запросах и 200 MB на запросах с отчётами.
pm.start_servers -- начальное число воркеров
Рекомендуемая формула для dynamic режима:
pm.start_servers = pm.min_spare_servers + (pm.max_spare_servers - pm.min_spare_servers) / 2
Пример: min_spare=5, max_spare=20, тогда start_servers = 5 + (20 - 5) / 2 = 12.
Эта формула обеспечивает, что при старте PHP-FPM количество процессов находится посередине диапазона и не требует немедленного масштабирования ни вверх, ни вниз.
pm.min_spare_servers -- минимум свободных воркеров
Определяет, сколько воркеров всегда должны быть свободны и готовы принять новый запрос. Если свободных меньше -- PHP-FPM немедленно создаёт новые.
; Low traffic site: 2-5
pm.min_spare_servers = 5
; High traffic API: 10-20
pm.min_spare_servers = 10
Слишком низкое значение -- при всплеске нагрузки запросы попадут в listen queue, пока воркеры создаются. Слишком высокое -- лишняя память расходуется впустую.
pm.max_spare_servers -- максимум свободных воркеров
Если свободных воркеров больше этого числа, PHP-FPM убивает лишние. Это экономит память после пиков нагрузки.
; Typical range: 2-4x min_spare_servers
pm.max_spare_servers = 20
pm.max_requests -- защита от утечек памяти
; Restart worker after 1000 requests (prevents memory leaks)
pm.max_requests = 1000
; 0 = never restart (DANGEROUS in production!)
; pm.max_requests = 0
Даже в хорошо написанном PHP-коде могут быть микроутечки: неочищенные массивы, циклические ссылки, баги в расширениях. Директива pm.max_requests гарантирует, что каждый воркер периодически перезапускается с чистой памятью.
Оптимальные значения:
500-1000-- стандартное production-значение200-500-- если есть подозрения на утечки5000-10000-- если приложение стабильно и перезапуски дороги (preloading)
Совет: Для рандомизации перезапусков можно задавать разные значения в пулах. Иначе все воркеры могут перезапуститься одновременно, вызвав кратковременное падение производительности.
Таймауты и логирование
request_terminate_timeout -- убить зависший скрипт
; Kill PHP process if request takes longer than 60 seconds
; Independent of PHP's max_execution_time!
request_terminate_timeout = 60s
; 0 = disabled (NOT recommended -- hung processes consume workers)
Разница с max_execution_time: директива PHP считает только CPU-время, а request_terminate_timeout считает реальное время (wall clock). Если скрипт ждёт ответа от внешнего API 5 минут -- max_execution_time не сработает, но request_terminate_timeout убьёт процесс.
request_slowlog_timeout -- обнаружение медленных запросов
; Log stack trace if request takes longer than 5 seconds
request_slowlog_timeout = 5s
; Path to slow log file
slowlog = /var/log/php/fpm-slow.log
Slow log -- бесценный инструмент для диагностики. Когда запрос превышает порог, PHP-FPM записывает полный stack trace выполняющегося кода. Вы увидите, какая именно функция или запрос к БД вызывает задержку.
Пример вывода slow log:
[27-Feb-2026 14:32:11] [pool www] pid 1234
script_filename = /var/www/app/public/index.php
[0x00007f1234567890] PDOStatement->execute() /var/www/app/src/Repository/OrderRepository.php:89
[0x00007f1234567891] App\Repository\OrderRepository->findWithFilters() /var/www/app/src/Service/OrderService.php:45
[0x00007f1234567892] App\Service\OrderService->getFilteredOrders() /var/www/app/src/Controller/OrderController.php:31
Из этого стека сразу видно: проблема в OrderRepository::findWithFilters() -- медленный SQL-запрос.
access.log -- журнал доступа FPM
; FPM access log (separate from Nginx access log!)
access.log = /var/log/php/fpm-access.log
; Custom format:
; %R - remote IP, %u - user, %t - time, %m - method
; %r - request URI, %s - status, %d - duration (ms)
; %M - peak memory (KB), %C - CPU usage (%)
access.format = "%R - %u %t \"%m %r\" %s %d ms %{mega}M MB %C%%"
Пример строки лога:
127.0.0.1 - - 27/Feb/2026:14:32:11 +0000 "GET /api/orders" 200 245 ms 42.5 MB 12%
В Docker: направляйте
slowlogиaccess.logв/dev/stderrи/dev/stdoutсоответственно.
Конфигурации для реальных сценариев
Сценарий 1: VPS 2 GB (WordPress/простой сайт)
; /etc/php/8.4/fpm/pool.d/wordpress.conf
[wordpress]
user = www-data
group = www-data
listen = /run/php/php-fpm.sock
; 2 GB total: OS ~512MB, MySQL ~512MB, Nginx ~64MB
; Available: ~900 MB, avg process ~60 MB
; max_children = 900 / 60 = 15
pm = dynamic
pm.max_children = 15
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
pm.process_idle_timeout = 10s
; Kill hung scripts after 30 seconds
request_terminate_timeout = 30s
; Log slow requests (>3 seconds)
request_slowlog_timeout = 3s
slowlog = /var/log/php/slow.log
; PHP overrides for this pool
php_admin_value[memory_limit] = 128M
php_admin_value[max_execution_time] = 30
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php/error.log
Сценарий 2: Dedicated 8 GB (Laravel API)
; /etc/php/8.4/fpm/pool.d/laravel-api.conf
[laravel-api]
user = www-data
group = www-data
listen = /run/php/php-fpm.sock
listen.backlog = 1024
; 8 GB total: OS ~1GB, PostgreSQL ~2GB, Redis ~512MB, buffer ~512MB
; Available: ~4 GB, avg process ~80 MB
; max_children = 4096 / 80 = 50
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
pm.process_idle_timeout = 10s
request_terminate_timeout = 300s
request_slowlog_timeout = 5s
slowlog = /var/log/php/slow.log
; Enable FPM status for monitoring
pm.status_path = /fpm-status
pm.status_listen = 127.0.0.1:9001
; PHP settings for API
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 60
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php/error.log
php_admin_value[open_basedir] = /var/www/app:/tmp
Сценарий 3: High-load 32 GB (микросервисы, несколько пулов)
; === Pool 1: API Gateway (high throughput, fast responses) ===
[api-gateway]
user = www-data
group = www-data
listen = /run/php/php-fpm-api.sock
listen.backlog = 4096
; Dedicated 10 GB for this pool
; avg process ~50 MB (lightweight API proxy)
; max_children = 10240 / 50 = 200
pm = static
pm.max_children = 200
pm.max_requests = 5000
request_terminate_timeout = 10s
request_slowlog_timeout = 1s
slowlog = /var/log/php/api-slow.log
php_admin_value[memory_limit] = 64M
php_admin_value[max_execution_time] = 10
; === Pool 2: Background Workers (heavy processing) ===
[workers]
user = www-data
group = www-data
listen = /run/php/php-fpm-workers.sock
; Dedicated 8 GB for this pool
; avg process ~200 MB (report generation, data processing)
; max_children = 8192 / 200 = 40
pm = dynamic
pm.max_children = 40
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 15
pm.max_requests = 200
request_terminate_timeout = 600s
request_slowlog_timeout = 30s
slowlog = /var/log/php/workers-slow.log
php_admin_value[memory_limit] = 512M
php_admin_value[max_execution_time] = 300
Ключевой принцип: разделяйте пулы по характеру нагрузки. Быстрые API-запросы не должны конкурировать с тяжёлыми отчётами за одни и те же воркеры.
Мониторинг и диагностика
Страница статуса PHP-FPM
; Enable in pool config
pm.status_path = /fpm-status
; Optionally listen on separate port (no Nginx needed)
pm.status_listen = 127.0.0.1:9001
# Nginx config to expose FPM status (restrict to localhost!)
location = /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Запрос статуса и интерпретация:
# Short status
curl -s http://127.0.0.1:9001/fpm-status
# Full status with per-process details
curl -s http://127.0.0.1:9001/fpm-status?full
# JSON format (for monitoring systems)
curl -s http://127.0.0.1:9001/fpm-status?json
Ключевые метрики:
pool: www
process manager: dynamic
start time: 27/Feb/2026 10:00:00
start since: 14400 ← seconds since FPM start
accepted conn: 125000 ← total requests handled
listen queue: 0 ← CRITICAL: requests waiting for worker
max listen queue: 12 ← historical max (if >0, was overloaded)
listen queue len: 128 ← OS socket backlog size
idle processes: 8 ← workers waiting for requests
active processes: 12 ← workers currently handling requests
total processes: 20 ← idle + active
max active processes: 48 ← historical peak of active workers
max children reached: 2 ← times max_children hit (should be 0!)
slow requests: 34 ← requests exceeding slowlog threshold
Красные флаги:
| Метрика | Проблема | Решение |
|---|---|---|
listen queue > 0 |
Все воркеры заняты, запросы ждут | Увеличить max_children или оптимизировать код |
max children reached > 0 |
Достигнут потолок воркеров | Увеличить max_children (если есть RAM) |
slow requests растёт |
Медленные запросы | Анализировать slow log |
idle = 0 постоянно |
Нет свободных воркеров | Увеличить max_children |
Валидация конфигурации
# Check config syntax before restart
php-fpm -t
# Output: NOTICE: configuration file /etc/php/8.4/fpm/php-fpm.conf test is successful
# Show effective configuration
php-fpm -tt
# Reload FPM without dropping connections (graceful)
kill -USR2 $(cat /run/php/php-fpm.pid)
# or
systemctl reload php8.4-fpm
Мониторинг памяти воркеров
# Real-time memory usage of FPM workers
watch -n 2 'ps -eo pid,rss,vsz,%mem,command | grep "php-fpm: pool" | sort -k2 -rn'
# Summary: total and average RSS
ps -eo rss,command | grep 'php-fpm: pool' | awk '{sum+=$1; n++} END {printf "Workers: %d, Total: %d MB, Average: %.1f MB\n", n, sum/1024, sum/n/1024}'
Безопасность PHP-FPM
Права доступа к сокету
; Unix socket permissions
listen = /run/php/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Only Nginx (running as www-data) can connect to the socket
; TCP alternative (for Docker/remote setups)
; listen = 127.0.0.1:9000
; listen.allowed_clients = 127.0.0.1
chroot -- изоляция процесса
; Restrict FPM workers to a specific directory tree
; Workers cannot access ANYTHING outside this path
chroot = /var/www/app
; After chroot, all paths are relative to chroot directory
; So /var/www/app/public/index.php becomes /public/index.php
chdir = /public
Внимание: chroot требует, чтобы все зависимости (библиотеки, расширения, временные файлы) были доступны внутри chroot-директории. Это сложно в настройке, но даёт сильную изоляцию.
security.limit_extensions -- ограничение расширений
; ONLY allow .php files to be executed via FastCGI
; Prevents executing .jpg, .txt, etc. as PHP (path traversal attacks)
security.limit_extensions = .php
; NEVER set to empty string in production!
; security.limit_extensions = ← DANGEROUS: any file can be executed as PHP
Раздельные пулы для приложений
; Pool 1: Main application (trusted code)
[app-main]
user = app-main
group = app-main
listen = /run/php/app-main.sock
php_admin_value[open_basedir] = /var/www/app-main:/tmp
php_admin_value[disable_functions] = exec,shell_exec,system,passthru
; Pool 2: Admin panel (higher privileges)
[app-admin]
user = app-admin
group = app-admin
listen = /run/php/app-admin.sock
php_admin_value[open_basedir] = /var/www/app-admin:/tmp
php_admin_value[memory_limit] = 512M
; Pool 3: Third-party integration (untrusted, sandboxed)
[app-external]
user = app-sandbox
group = app-sandbox
listen = /run/php/app-external.sock
php_admin_value[open_basedir] = /var/www/app-external:/tmp
php_admin_value[disable_functions] = exec,shell_exec,system,passthru,proc_open,popen,dl
php_admin_value[memory_limit] = 64M
request_terminate_timeout = 10s
Каждый пул работает под своим пользователем ОС, с собственным open_basedir и ограничениями функций. Компрометация одного пула не даёт доступ к файлам другого.
Глобальные настройки безопасности FPM
; /etc/php/8.4/fpm/php-fpm.conf
[global]
; Emergency restart: if N children die within M seconds, restart FPM
; Protects against segfaults in PHP extensions
emergency_restart_threshold = 10
emergency_restart_interval = 60s
; Graceful stop timeout: how long to wait for children to finish
process_control_timeout = 10s
; Run as non-root (recommended)
daemonize = yes
Диагностика типичных проблем
Проблема: "502 Bad Gateway"
Nginx возвращает 502, когда не может связаться с PHP-FPM.
# Check if FPM is running
systemctl status php8.4-fpm
# Check socket exists and has correct permissions
ls -la /run/php/php-fpm.sock
# Check FPM error log
tail -50 /var/log/php/fpm-error.log
# Common causes:
# 1. FPM crashed (check emergency_restart)
# 2. Socket permissions mismatch (listen.owner != Nginx user)
# 3. All workers busy + listen queue full (increase max_children)
Проблема: "504 Gateway Timeout"
Nginx ждёт ответа от PHP-FPM, но не получает его в отведённое время.
# Check Nginx timeout settings
# fastcgi_read_timeout should be >= request_terminate_timeout
# Check FPM slow log for the culprit
tail -100 /var/log/php/slow.log
# Check if specific endpoints are slow
# access.log with %d (duration) helps identify patterns
Проблема: постоянный рост памяти воркеров
# Monitor RSS over time
while true; do
echo "$(date): $(ps -eo rss,command | grep 'php-fpm: pool' | awk '{sum+=$1; n++} END {printf "Avg: %.0f MB, Max: ", sum/n/1024}')$(ps -eo rss,command | grep 'php-fpm: pool' | awk '{print $1/1024}' | sort -rn | head -1) MB"
sleep 60
done
# Solution: reduce pm.max_requests to force more frequent restarts
# pm.max_requests = 500 (or even 200 for leaky apps)