Среда выполнения и внутреннее устройство PHP
Как работает PHP — модель shared-nothing, демоны, php-fpm с nginx, OPcache и его настройка в проде, генераторы и итераторы, возможности PHP 8, воркер-рантаймы (FrankenPHP, RoadRunner, Octane) и внутреннее устройство движка (JIT, refcount и сборщик циклов, copy-on-write у zval).
17 вопросов
MiddleТеорияОчень частоКак php-fpm работает и взаимодействует с nginx?
Как php-fpm работает и взаимодействует с nginx?
php-fpm — это FastCGI-менеджер процессов: он держит пул постоянных рабочих процессов PHP. nginx сам отдаёт статику, а PHP-запросы пересылает по протоколу FastCGI (Unix-сокет или TCP) через fastcgi_pass. Свободный воркер обрабатывает запрос и возвращает ответ; nginx сам PHP не выполняет. Размер пула (pm.max_children) ограничивает конкурентность.
Типичные ошибки
- ✗Думать, что nginx сам выполняет PHP, а не делегирует php-fpm по FastCGI
- ✗Считать, что php-fpm форкает новый процесс на запрос вместо переиспользования воркеров пула
- ✗Путать протокол FastCGI с проксированием обычного HTTP на PHP-бэкенд
Уточняющие вопросы
- →Как
pm.max_childrenсоотносится с доступной памятью при выборе безопасного размера пула? - →В чём разница между менеджерами процессов
static,dynamicиondemand?
SeniorТеорияОчень частоПроведите запрос от nginx через php-fpm до ответа Laravel и обратно.
Проведите запрос от nginx через php-fpm до ответа Laravel и обратно.
nginx сам отдаёт статику, остальное шлёт по FastCGI мастеру php-fpm, тот передаёт запрос свободному воркеру; если все заняты, запрос ждёт в listen-очереди. Воркер выполняет index.php: OPcache отдаёт готовые опкоды, фреймворк бутстрапится, middleware доходит до контроллера, ответ уходит, и воркер сносит всё.
Типичные ошибки
- ✗Считать OPcache кэшем приложения или ответов, а не кэшем скомпилированных опкодов
- ✗Думать, что бутстрап фреймворка платится один раз при старте пула, а не на каждый запрос
- ✗Забывать про listen-очередь, где запросы ждут, когда все воркеры заняты
Уточняющие вопросы
- →Что именно воркер php-fpm сохраняет между двумя запросами в модели shared-nothing?
- →Почему OPcache убирает парсинг и компиляцию, но не стоимость бутстрапа фреймворка?
JuniorТеорияЧастоЧто такое генераторы и как они связаны с итераторами?
Что такое генераторы и как они связаны с итераторами?
Генератор — это функция, использующая yield, чтобы выдавать значения лениво, по одному, не строя всю коллекцию в памяти. Её вызов возвращает объект Generator, реализующий интерфейс Iterator — поэтому по нему можно пройти через foreach. Каждый yield приостанавливает функцию и продолжает на следующей итерации.
Типичные ошибки
- ✗Думать, что генератор строит всю коллекцию заранее, а не выдаёт значения лениво
- ✗Не знать, что
GeneratorреализуетIterator, поэтому работает прямо вforeach - ✗Считать, что генератор можно перемотать или пройти повторно как массив (он однонаправленный)
Уточняющие вопросы
- →Что меняет
yield $key => $valueпо сравнению с простымyield $value? - →Когда вы бы реализовали интерфейс
Iteratorвручную вместо генератора?
JuniorТеорияЧастоЧто OPcache хранит между запросами и от какой работы он избавляет PHP?
Что OPcache хранит между запросами и от какой работы он избавляет PHP?
Без него PHP парсит и компилирует каждый исходный файл в опкоды на каждый запрос. OPcache держит эти скомпилированные опкоды в разделяемой памяти, поэтому следующий запрос пропускает компиляцию и сразу исполняет их. Он кэширует код, а не данные: не результаты запросов.
Типичные ошибки
- ✗Думать, что OPcache кэширует данные или результаты запросов, а не скомпилированные опкоды
- ✗Считать, что он хранит готовый HTML и отдаёт страницы, не запуская PHP
- ✗Полагать, что иначе PHP не перекомпилирует каждый исходный файл на каждый запрос
Уточняющие вопросы
- →Чем JIT-компилятор идёт дальше того, что кэширует сам OPcache?
- →Что происходит с закэшированными опкодами при выкладке новых исходников?
MiddleТеорияЧастоЧем PHP 8 отличается от PHP 7 и что делает strict_types?
Чем PHP 8 отличается от PHP 7 и что делает strict_types?
PHP 8 добавляет JIT-компилятор, union-типы (int|string), именованные аргументы, атрибуты, выражение match, продвижение свойств в конструкторе и nullsafe-оператор ?->. declare(strict_types=1) переключает файл с приводящей на строгую типизацию скаляров — передача string туда, где объявлен int, кидает TypeError вместо тихого приведения.
Типичные ошибки
- ✗Думать, что
strict_types=1глобален — он действует только в объявившем его файле - ✗Считать, что строгая типизация запрещает приведение везде, включая
intиfloat - ✗Путать union-типы (PHP 8.0) с сокращением для nullable
?typeиз PHP 7.1
Уточняющие вопросы
- →Почему
strict_types=1влияет на поведение вызывающего файла, а не вызываемой функции? - →Чем выражение
matchотличается отswitchпо сравнению и проваливанию (fall-through)?
JuniorТеорияИногдаКак написать долгоживущий демон (фоновый процесс) на PHP?
Как написать долгоживущий демон (фоновый процесс) на PHP?
Запустите CLI-скрипт с бесконечным циклом, который делает работу и засыпает, поддерживаемый супервизором (systemd или supervisord), а не демонизируйте вручную. Внутри цикла вызывайте gc_collect_cycles() и следите за памятью, так как долгоживущий процесс PHP накапливает состояние. Обрабатывайте сигналы (pcntl_signal) для корректного завершения, а перезапуск доверьте супервизору.
Типичные ошибки
- ✗Игнорировать рост памяти в долгоживущем процессе, что ведёт к утечке и OOM
- ✗Демонизировать вручную вместо доверия жизненного цикла systemd/supervisord
- ✗Забыть обработку сигналов, из-за чего процесс не завершается и не перезагружается корректно
Уточняющие вопросы
- →Почему долгоживущий PHP-воркер течёт по памяти сильнее, чем процесс php-fpm на запрос?
- →Как
pcntl_signalпозволяет воркеру доделать текущую задачу перед остановкой?
MiddleПроизводительностьИногдаЧто происходит в памяти при передаче большого массива в функцию по значению?
Что происходит в памяти при передаче большого массива в функцию по значению?
При вызове не копируется ничего. Семантически PHP передаёт по значению, но движок разделяет тот же массив и увеличивает refcount — это copy-on-write. Дублирование происходит, только когда одна из сторон пишет при refcount > 1. Добавление элемента внутри функции копирует весь массив.
Типичные ошибки
- ✗Думать, что семантика по значению означает физическую копию в момент вызова
- ✗Хвататься за
&для параметров только на чтение, полагая, что это экономит копию - ✗Забывать, что одна запись внутри функции отделяет и дублирует весь массив
Уточняющие вопросы
- →Когда передача через
&оправдана и чего она стоит вызывающему коду? - →Как ведёт себя отделение для вложенного массива, который сам разделён по refcount?
MiddleТеорияИногдаЧто персистентный рантайм Laravel Octane держит в памяти между запросами и что это ломает?
Что персистентный рантайм Laravel Octane держит в памяти между запросами и что это ломает?
Octane поднимает приложение Laravel один раз и держит его экземпляр и контейнер живыми внутри воркера Swoole, FrankenPHP или RoadRunner. Поэтому ломается всё, что ждало свежий процесс: ваши синглтоны, статические свойства, изменённый конфиг, протёкший пользователь.
Типичные ошибки
- ✗Кэшировать данные уровня запроса в синглтоне, который Octane держит живым
- ✗Считать, что Octane сбрасывает ваши статические свойства и изменённый в рантайме конфиг
- ✗Ждать, что контейнер приложения пересобирается с нуля на каждый запрос
Уточняющие вопросы
- →Какая настройка Octane сбрасывает привязки и почему это не покрывает ваши статические свойства?
- →Как обнаружить значение уровня запроса, протекающее в более поздний запрос?
MiddleПроизводительностьИногдаКакие настройки OPcache важны в проде и чего стоит opcache.validate_timestamps=0?
Какие настройки OPcache важны в проде и чего стоит opcache.validate_timestamps=0?
opcache.memory_consumption должен вмещать все скомпилированные скрипты, иначе кэш буксует, а opcache.max_accelerated_files обязан превышать реальное число файлов. opcache.validate_timestamps=0 убирает проверку файлов на каждом запросе, но выкладка требует сброса.
Типичные ошибки
- ✗Оставлять
opcache.max_accelerated_filesпо умолчанию, далеко ниже реального числа файлов - ✗Ставить
validate_timestamps=0без сброса OPcache или перезагрузки FPM при выкладке - ✗Ждать от JIT ускорения типичных I/O-bound веб-запросов
Уточняющие вопросы
- →Как читать
opcache_get_status(), чтобы отличить переполненный кэш от здоровой ротации? - →Как безопаснее всего инвалидировать кэш при выкладке без простоя?
MiddleТеорияИногдаКак PHP освобождает память — что упускает refcount и что делает сборщик циклов?
Как PHP освобождает память — что упускает refcount и что делает сборщик циклов?
Каждое значение несёт refcount; при нуле оно освобождается сразу, и так уходит почти всё. Чего refcount не освободит никогда — это цикл ссылок: каждый объект держит счётчик от другого. PHP копит возможные корни циклов и запускает отдельный сборщик циклов.
Типичные ошибки
- ✗Считать, что одного refcount хватает на всё, включая циклы ссылок
- ✗Полагать, что сборщик циклов работает по таймеру, а не при заполнении буфера корней
- ✗Игнорировать циклы в долгоживущем воркере, ведь запрос FPM и так всё освобождает
Уточняющие вопросы
- →Почему цикл куда важнее в долгоживущем воркере, чем в запросе под FPM?
- →Как разорвать цикл родитель-потомок, чтобы одного refcount хватило для освобождения?
MiddleТеорияИногдаКак рантаймы режима воркеров RoadRunner и FrankenPHP меняют модель запросов PHP?
Как рантаймы режима воркеров RoadRunner и FrankenPHP меняют модель запросов PHP?
Приложение поднимается один раз, а дальше воркер обслуживает много запросов в цикле, поэтому загрузка фреймворка, конфиг, контейнер и соединения с базой переиспользуются. Плата: процесс переживает запрос, поэтому статические свойства и синглтоны утекают в следующий.
Типичные ошибки
- ✗Считать, что состояние запроса по-прежнему уничтожается между запросами автоматически
- ✗Оставлять значение уровня запроса в статическом свойстве или синглтоне
- ✗Думать, что выигрыш даёт более быстрый HTTP-слой, а не переиспользованный бутстрап
Уточняющие вопросы
- →Какие приёмы библиотек и фреймворков текут состоянием, когда процесс переживает запрос?
- →Как поддерживать соединение с базой живым на тысячах запросов внутри одного воркера?
MiddleТеорияИногдаЧем yield from отличается от цикла по внутреннему генератору с повторным yield?
Чем yield from отличается от цикла по внутреннему генератору с повторным yield?
yield from делегирует: он отдаёт значения внутреннего итерируемого, сохраняя его ключи, пробрасывает наружу значение return внутреннего генератора (читается через getReturn()) и передаёт в него send(). Ручной foreach с повторным yield не делает ничего из этого.
Типичные ошибки
- ✗Считать, что ручной
foreachс повторнымyieldсохраняет ключи и значениеreturn - ✗Отдавать делегирующий генератор в
iterator_to_array(), не отключив сохранение ключей - ✗Не знать, что
yield fromпробрасываетsend()во внутренний генератор
Уточняющие вопросы
- →Почему
iterator_to_array()может терять значения после делегирования нескольким генераторам? - →Как ведёт себя
getReturn(), если генератор ещё не завершился?
SeniorДебаггингИногдаДолгоживущий CLI-скрипт импорта падает по memory_limit. Определите причину.
Долгоживущий CLI-скрипт импорта падает по memory_limit. Определите причину.
Это удержание, а не утечка. fetchAll() материализует все 5 млн строк ещё до входа в цикл, а $processed[] держит живую ссылку на каждую сущность, поэтому refcount не падает до нуля и освобождать нечего. Нужно стримить: fetch() в цикле или генератор, и не копить.
Типичные ошибки
- ✗Вызывать
gc_collect_cycles()— он не освободит то, на что скрипт всё ещё держит ссылку - ✗Ставить диагноз «утечка», когда скрипт просто удерживает выборку и каждую сущность
- ✗Поднимать
memory_limitвместо стриминга, что лишь откладывает ту же фатальную ошибку
Уточняющие вопросы
- →Почему
fetchAll()даёт пик памяти ещё до первой итерации цикла? - →Как небуферизованный запрос в расширении для БД
PDOменяет то, что PHP держит в памяти?
SeniorДизайнИногдаКоманда хочет запустить новый HTTP-сервис на персистентном воркер-рантайме — FrankenPHP, RoadRunner или Laravel Octane — вместо классического php-fpm, аргументируя тем, что так гораздо быстрее. Ограничения: сервис тянет несколько сторонних пакетов Composer, которые никто не аудировал, персистентный рантайм в проде никто в команде не поднимал, а на php-fpm сегодня p95 равен 90 мс, из которых примерно 15 мс — бутстрап фреймворка. Примите решение и обоснуйте его. Скажите, что воркер-рантайм реально даёт при таких числах, какой класс отказов он приносит, которого у FPM нет, что вы обязаны проверить до перехода и что должно измениться, чтобы ответ поменялся на противоположный.
Команда хочет запустить новый HTTP-сервис на персистентном воркер-рантайме — FrankenPHP, RoadRunner или Laravel Octane — вместо классического php-fpm, аргументируя тем, что так гораздо быстрее. Ограничения: сервис тянет несколько сторонних пакетов Composer, которые никто не аудировал, персистентный рантайм в проде никто в команде не поднимал, а на php-fpm сегодня p95 равен 90 мс, из которых примерно 15 мс — бутстрап фреймворка. Примите решение и обоснуйте его. Скажите, что воркер-рантайм реально даёт при таких числах, какой класс отказов он приносит, которого у FPM нет, что вы обязаны проверить до перехода и что должно измениться, чтобы ответ поменялся на противоположный.
Остаёмся на FPM. Shared-nothing снос состояния — свойство безопасности, а не только расход: утечка или fatal в одном запросе не заденет следующий. Воркер-режим стартует один раз, но превращает статику, синглтоны и глобалы в межзапросные инциденты. Бутстрап — 15 мс из 90, потолок ~17%.
Типичные ошибки
- ✗Читать посреквестный снос состояния в FPM как чистый расход, а не как гарантию изоляции
- ✗Считать, что код менять не нужно, хотя статика и синглтоны теперь переживают запрос
- ✗Принимать 15 мс бутстрапа внутри 90 мс p95 за главную составляющую задержки
Уточняющие вопросы
- →Какое состояние на уровне библиотек вы бы аудировали первым перед переездом в воркер-режим?
- →Как должен выглядеть бюджет задержки, чтобы компромисс качнулся в пользу воркер-режима?
SeniorТеорияИногдаЧем Opcache отличается от JIT и как устроен сборщик мусора PHP?
Чем Opcache отличается от JIT и как устроен сборщик мусора PHP?
Opcache кэширует скомпилированные опкоды в разделяемой памяти, чтобы PHP не парсил и не компилировал исходник заново на каждый запрос. JIT (PHP 8) идёт дальше, компилируя горячие опкоды в нативный машинный код в рантайме — он помогает CPU-bound коду больше, чем типичным I/O-bound веб-запросам. Сборщик мусора — это подсчёт ссылок у каждого ZVAL плюс сборщик циклов, периодически находящий и освобождающий недостижимые циклы ссылок.
Типичные ошибки
- ✗Смешивать Opcache (кэш опкодов) с JIT (компиляция в нативный машинный код)
- ✗Ждать от JIT ускорения типичного I/O-bound веб-кода так же, как CPU-bound работы
- ✗Думать, что подсчёта ссылок хватает для всего, упуская роль сборщика циклов
Уточняющие вопросы
- →Что такое ZVAL и как его тег типа и refcount поддерживают copy-on-write?
- →Почему один подсчёт ссылок не может освободить самоссылающийся граф объектов?
SeniorДизайнИногдаВы начинаете greenfield-сервис на текущей стабильной линии PHP 8.5, но привычки команды родом из PHP 7.4: нет declare(strict_types=1), конструкторы написаны руками, константы класса используются как enum-подобные магические строки, объекты-значения изменяемы. Вы отвечаете за гайдлайн по коду. Решите, какие современные возможности языка станут обязательными, какие останутся опциональными, а какие вы осознанно придержите, обосновав каждую группу тем, какие ошибки она убирает против того, что добавляет к нагрузке на ревью. Разберите как минимум readonly, enum, match, продвижение свойств в конструкторе, property hooks, асимметричную видимость и новейший синтаксис 8.5. Наконец, скажите, на что сервису опираться в вопросе конкурентности и вправе ли какая-либо часть дизайна рассчитывать, что эта поддержка изменится.
Вы начинаете greenfield-сервис на текущей стабильной линии PHP 8.5, но привычки команды родом из PHP 7.4: нет declare(strict_types=1), конструкторы написаны руками, константы класса используются как enum-подобные магические строки, объекты-значения изменяемы. Вы отвечаете за гайдлайн по коду. Решите, какие современные возможности языка станут обязательными, какие останутся опциональными, а какие вы осознанно придержите, обосновав каждую группу тем, какие ошибки она убирает против того, что добавляет к нагрузке на ревью. Разберите как минимум readonly, enum, match, продвижение свойств в конструкторе, property hooks, асимметричную видимость и новейший синтаксис 8.5. Наконец, скажите, на что сервису опираться в вопросе конкурентности и вправе ли какая-либо часть дизайна рассчитывать, что эта поддержка изменится.
Обязательно то, что убирает классы ошибок: strict_types, продвижение свойств, enum, readonly, match. Property hooks и асимметричную видимость (PHP 8.4) берём там, где они убирают бойлерплейт. Новый синтаксис 8.5 придерживаем. Конкурентность — на том, что есть: RFC True Async — черновик без голосования.
Типичные ошибки
- ✗Планировать под приход async/await в ядро PHP — RFC True Async это черновик без голосования
- ✗Приписывать property hooks и асимметричную видимость версии 8.5, хотя обе вышли в PHP 8.4
- ✗Делать новейший синтаксис обязательным раньше, чем его свободно читают ревьюеры и анализаторы
Уточняющие вопросы
- →Какие из этих правил вы бы проверяли механически в CI, а не чек-листом на ревью?
- →Что должно измениться, прежде чем вы допустите пайп-оператор
|>в продакшен-код?