Жизненный цикл запроса и модель shared-nothing
Вопрос, который проверяет, понимаете ли вы, почему PHP устроен не как Node.js и не как Java.
Shared-nothing — модель выполнения, при которой каждый запрос обрабатывается в чистом окружении: переменные, объекты и подключения не переживают завершение скрипта.
Вопрос 1: «Что происходит с момента прихода запроса до отдачи ответа?»
Что проверяет интервьюер
Знаете ли вы, кто именно исполняет ваш код в продакшене. Ответ «Apache запускает PHP» звучит как знание 2010 года; сегодня в 9 случаях из 10 это связка nginx + PHP-FPM. Разложите ответ по шагам:
- nginx принимает HTTP-запрос и по правилу
location ~ \.php$отдаёт его по протоколу FastCGI менеджеру процессов PHP-FPM. - FPM выбирает свободный воркер — отдельный процесс из заранее созданного пула.
- Воркер собирает суперглобальные массивы (
$_GET,$_POST,$_SERVER,$_COOKIE), находит входной файл (index.php) и запускает его. - PHP компилирует исходники в опкоды (или берёт их готовыми из OPcache) и исполняет виртуальной машиной Zend Engine.
- Скрипт формирует ответ, который через FastCGI возвращается в nginx, а оттуда — клиенту.
- Наступает shutdown: выполняются функции, зарегистрированные через
register_shutdown_function, вызываются деструкторы, освобождается вся память запроса. Воркер очищается и берёт следующий запрос.
<?php
register_shutdown_function(function () {
echo "3. shutdown: соединения закрыты\n";
});
$counter = 0;
echo "1. обработка запроса\n";
$counter++;
echo "2. ответ отправлен, счётчик = $counter\n";
Вывод:
1. обработка запроса 2. ответ отправлен, счётчик = 1 3. shutdown: соединения закрыты
Вопрос 2: «Почему статическая переменная не сохраняется между запросами?»
Потому что вся память запроса освобождается на фазе shutdown. Даже если следующий запрос попадёт в тот же самый процесс-воркер, он начнёт с чистого листа: классы будут загружены заново (опкоды — из кэша, но состояние — нет).
<?php
class Counter {
public static int $count = 0;
}
Counter::$count++;
Counter::$count++;
echo "внутри одного запроса: ", Counter::$count, "\n";
echo "после завершения скрипта это состояние исчезнет\n";
Вывод:
внутри одного запроса: 2 после завершения скрипта это состояние исчезнет
Отсюда практические следствия, которые и хочет услышать интервьюер:
- Общее состояние нужно выносить наружу — в Redis, Memcached или базу. Кэш «в статическом свойстве» живёт ровно один запрос.
- Утечки памяти почти не страшны. Процесс всё равно очистится; поэтому PHP прощает то, за что в долгоживущем приложении пришлось бы расплачиваться.
- Нет состояния гонки между запросами внутри PHP-кода: параллельные запросы — это разные процессы с разной памятью. Гонки возникают только на общих ресурсах — в базе, файлах, Redis.
- Соединения с базой не переиспользуются по умолчанию — на каждый запрос новое (либо persistent-соединение или внешний пулер вроде PgBouncer).
Вопрос 3: «Как настраивается пул FPM и на что влияет?»
; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic ; статический / динамический пул воркеров
pm.max_children = 20 ; потолок одновременных запросов
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500 ; перезапуск воркера после N запросов (лечит утечки)
request_terminate_timeout = 30s
Главное число здесь — pm.max_children: это максимум одновременно обрабатываемых запросов. Считают его от памяти: доступная память делится на средний расход одного воркера. Поставите слишком много — сервер уйдёт в swap; слишком мало — запросы встанут в очередь FPM и клиенты получат таймаут. На собеседовании ценят, если вы упомянете, что медленный внешний API легко «съедает» весь пул: воркер занят ожиданием, а не вычислениями.
Стоит знать и про альтернативы классической модели: долгоживущие рантаймы (RoadRunner, Swoole, FrankenPHP) поднимают приложение один раз и держат его в памяти между запросами. Это быстрее, но требования меняются радикально — утечки, глобальное состояние и незакрытые ресурсы начинают накапливаться, как в Java.
Типичные ошибки кандидатов
- Считают, что PHP — многопоточный, и переживают за состояние гонки между запросами внутри кода.
- Уверены, что
static-кэш переживает запрос, и строят на этом «оптимизации». - Путают
max_childrenс числом ядер — это про память и характер нагрузки, а не про CPU. - Не знают, что происходит после
echo: забывают про фазу shutdown, деструкторы иfastcgi_finish_request(), который позволяет отдать ответ и продолжить работу. - Говорят «PHP медленный, потому что интерпретируемый», не упоминая компиляцию в опкоды и OPcache.
Как ответить кратко
«nginx передаёт запрос в PHP-FPM, свободный воркер собирает суперглобальные массивы, PHP компилирует код в опкоды (обычно берёт их из OPcache) и исполняет; после ответа идёт shutdown — деструкторы и полная очистка памяти. Это модель shared-nothing: между запросами не сохраняется ничего, поэтому общее состояние живёт в Redis или базе, а параллельные запросы — это отдельные процессы, а не потоки».