Среда выполнения PHP
Классический PHP устроен так, как не устроен почти никакой другой серверный язык: каждый запрос начинается с чистого листа. Процесс поднимает приложение, читает конфиг, собирает контейнер, обрабатывает запрос, отдаёт ответ — и уничтожает всё: переменные, объекты, статические свойства, глобальные. В следующий запрос не переезжает ничего. Это и называется shared-nothing, и это одновременно главная страховка PHP и его главный налог. Страховка — потому что утечка памяти живёт миллисекунды и не копится, а упавший запрос не может испортить соседний. Налог — потому что за полный бутстрап приложения вы платите на каждом запросе.
Именно из этого налога выросли два механизма, вокруг которых крутится вся тема. Первый — OPcache: он держит в разделяемой памяти уже скомпилированные опкоды, чтобы исходник не парсился заново в каждом запросе. Тут же и главная ловушка темы: OPcache — кэш кода, а не данных; путать его с Redis или Memcached — самый распространённый неверный ответ, и на нём отсеивают. Второй механизм — воркер-рантаймы (FrankenPHP, RoadRunner, Laravel Octane): они отказываются от shared-nothing и держат загруженное приложение в памяти между запросами. Бутстрап исчезает, соединения с базой переиспользуются — но вместе с ними в следующий запрос переезжают статики, синглтоны и всё, что вы забыли обнулить. Обмен «скорость против утечки состояния» и есть позвоночник этой темы. Поверх лежит работа с памятью — copy-on-write, refcount и сборщик циклов — и ленивые генераторы, которые позволяют пройти миллион строк, не собирая их в массив. Разбор по слоям — ниже.
Карта темы
- Модель процессов — shared-nothing на каждый запрос, что переживает запрос, а что нет, и чем от этого отличается долгоживущий CLI-демон.
- php-fpm и nginx — FastCGI-пул воркеров, откуда берётся предел конкурентности и как считать
pm.max_children. - OPcache и JIT — кэш опкодов, а не данных; продовые настройки,
opcache.validate_timestampsи почему JIT почти не помогает обычному веб-запросу. - Воркер-режим — FrankenPHP, RoadRunner и Octane держат приложение в памяти; что это даёт и какой класс багов приносит.
- Zval и copy-on-write — почему передать большой массив по значению бесплатно ровно до первой записи в него.
- Сборка мусора — refcount освобождает почти всё, циклы — не освобождает; что делает сборщик циклов и чем утечка отличается от удержания.
- Итераторы — интерфейс
Iterator, протоколforeachиIteratorAggregate. - Генераторы —
yield, ленивая выдача по одному значению,yield fromи делегирование. - Современный PHP — что принесли PHP 8.0–8.5, что делает
strict_typesи на чём в PHP на самом деле строят конкурентность.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать OPcache кэшем данных, «как Redis» | Он кэширует опкоды; результатов запросов и HTML в нём нет и не будет |
| Ждать от JIT ускорения обычного веб-запроса | Типичный запрос упирается в базу и сеть; JIT ускоряет CPU-bound код, а не ожидание I/O |
| Думать, что nginx сам исполняет PHP | Он отдаёт статику и делегирует PHP воркеру php-fpm по FastCGI |
| Думать, что php-fpm форкает процесс на каждый запрос | Воркеры пула переиспользуются; предел конкурентности — pm.max_children |
Поднять pm.max_children «чтобы держать нагрузку» | Воркеры вытеснят память в своп, и станет хуже, чем при очереди в backlog |
Оставить opcache.validate_timestamps=0 и просто выложить код | Воркеры продолжают исполнять старые опкоды — нужен сброс кэша или reload FPM |
| Перенести приложение в воркер-режим, не вычистив состояние | Синглтоны и статики переезжают в следующий запрос — чужие данные у чужого пользователя |
| Ждать от воркер-режима ускорения, когда бутстрап — 15 мс из 90 | Потолок выигрыша ≈17%; остальное — база и сеть, их воркер-режим не трогает |
| Считать, что передача большого массива по значению копирует его | Копии нет — только refcount; копирование случится на первой записи при refcount > 1 |
| Ждать, что refcount освободит всё | Цикл ссылок refcount не обнулит — его освобождает только сборщик циклов |
Лечить рост памяти в CLI вызовом gc_collect_cycles() | Если на объекты есть живые ссылки, это не мусор, а удержание — сборщик тут бессилен |
| Считать, что генератор строит коллекцию заранее | Смысл yield ровно в обратном — значения выдаются по одному, память константна |
| Пытаться пройти генератор дважды | Генератор однонаправленный: повторный обход бросит исключение |
Считать strict_types=1 глобальным | Он действует только в объявившем его файле и по режиму вызывающего |
Значение для собеседований
Среда выполнения — раздел, где проверяют, есть ли у кандидата модель того, что реально происходит с запросом, а не пересказ документации. Хороший ответ на «как работает PHP под нагрузкой» звучит так: «свободный воркер из пула php-fpm принимает FastCGI-запрос, опкоды уже лежат в OPcache, приложение поднимается с нуля, отдаёт ответ и всё уничтожает; конкурентность ограничена pm.max_children». Такой кандидат сразу отделяется от того, кто говорит «nginx запускает скрипт».
Что обычно проверяют:
- Что означает shared-nothing и что именно переживает границу запроса.
- Роли nginx и php-fpm, откуда берётся предел одновременных запросов.
- Что хранит OPcache и чем он не является; что делает
opcache.validate_timestamps=0и какой ценой. - Чем JIT отличается от OPcache и почему на обычном веб-запросе он почти ничего не даёт.
- Что воркер-рантайм переиспользует между запросами и какой класс багов это приносит.
- Что происходит в памяти при передаче большого массива по значению.
- Что refcount не может освободить и что с этим делает сборщик циклов.
- Чем генератор отличается от итератора и что добавляет
yield from.
Типичный неверный ответ: «OPcache — это кэш, он хранит результаты, чтобы не ходить в базу». Это самое частое и самое показательное заблуждение темы: OPcache не знает ничего ни о ваших данных, ни о ваших запросах — он хранит скомпилированный код, чтобы движок не парсил .php-файлы заново на каждом запросе. Кэшированием данных занимаются Redis и Memcached, и это совершенно другой слой. Второй классический провал — «переедем на Octane, и всё станет в разы быстрее»: если бутстрап занимает 15 мс из 90, то потолок выигрыша — около 17%, а платой станет целый новый класс инцидентов с утечкой состояния между запросами.