Производительность и кэширование
Оптимизация начинается не с кода, а с измерения. Интуиция в этой теме почти всегда врёт: кандидат, который «чувствует», где медленно, правит не тот участок, а потом объясняет, почему стало не сильно лучше. Профайлер отвечает на единственный вопрос, который имеет значение, — куда реально уходит время запроса. И почти всегда оказывается, что уходит оно не в PHP: интерпретатор считает быстро, а секунды набегают в базе, в сети и в блокирующих вызовах к чужим сервисам. Один запрос, повторённый триста раз в цикле; один тяжёлый агрегат без индекса; один curl_exec() к сервису, который сегодня отвечает две секунды, — вот из чего обычно состоят те самые три секунды ответа.
Отсюда и вся тема. Кэш убирает работу, которую можно не делать заново; очередь убирает работу, которую можно сделать не сейчас; масштабирование добавляет мощности, когда убирать уже нечего. Ловушки назовём сразу, потому что на них сыпется большинство. OPcache — это кэш скомпилированных опкодов, а не кэш данных: он не хранит ни результаты ваших запросов, ни отрендеренный вывод, и медленный запрос от его включения быстрее не станет. JIT помогает CPU-нагрузке, а типичное веб-приложение упирается в I/O и базу, так что «включить JIT, чтобы ускорить API» — это не ответ. APCu и static-свойство живут в одном процессе FPM, а не во всех сразу. TTL — это страховка, а не стратегия инвалидации. Задача в очереди сама не выполнится, пока её не заберёт отдельный воркер. А деградация только под нагрузкой — это чаще всего насыщение пула, а не медленный код. Разбор по слоям — ниже.
Карта темы
- Профилирование — Xdebug и Blackfire, чтение графа вызовов по inclusive wall time и почему N+1 ловится счётчиком запросов, а не профайлером.
- Кэш приложения, OPcache и JIT — что на самом деле кэширует OPcache, чем от него отличаются Redis и Memcached, почему APCu не общий и кому реально помогает JIT.
- Инвалидация кэша и cache-aside — паттерн cache-aside, устройство ключа, удаление вместо обновления, теги и версионный префикс, защита от стампеда.
- HTTP-кэширование —
Cache-Controlи свежесть,ETagи304, разницаpublicиprivateи почемуETagэкономит передачу, но не работу. - CDN и статика — что вынос ассетов снимает с origin, зачем версионировать имена файлов и чего от CDN ждать не стоит.
- Очереди и фоновые задачи — почему синхронный вызов держит воркер, как устроены очереди Laravel и Symfony Messenger, идемпотентность и повторы.
- Масштабирование PHP-FPM — пул воркеров как потолок параллелизма, расчёт
pm.max_childrenот RAM, shared-nothing и диагностика насыщения пула.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать OPcache кэшем данных или готового вывода | Он хранит только скомпилированные опкоды — медленный запрос к базе от него быстрее не станет |
Включать JIT, чтобы ускорить веб-API | Веб-нагрузка упирается в I/O и базу, а не в CPU — выигрыш близок к нулю |
Считать APCu или static-свойство общим кэшем | Они живут в одном процессе FPM — на соседнем воркере кэша просто нет |
| Искать N+1 профайлером | 300 запросов по 1 мс не всплывут как один медленный вызов — их нужно считать, а не мерить |
| Строить ключ кэша без фильтров, страницы и локали | Два разных запроса схлопываются в одну запись, и пользователь получает чужой результат |
| Полагаться на TTL как на стратегию инвалидации | Устаревшие данные будут отдаваться весь срок жизни ключа |
| Оставлять холодный ключ без защиты | Всплеск параллельных промахов перестраивает его одновременно и кладёт базу |
Считать Redis зависимостью, а не кэшем | Отказ кэша мгновенно превращается в отказ приложения |
Слать длинный public max-age на персональной странице | Общий прокси сохранит ответ и отдаст его другому пользователю — это утечка данных |
Считать ETag после полного рендера страницы | Сэкономлена передача, но не работа — PHP всё равно построил весь ответ |
| Выкладывать новые ассеты под старым именем | Edge-узел CDN продолжает отдавать устаревшую копию после деплоя |
| Поставить задачу в очередь и не поднять воркера | Задача лежит в очереди и не выполняется никогда — очередь сама ничего не запускает |
| Не перезапускать воркер очереди на деплое | Долгоживущий процесс держит в памяти старый код и работает по нему |
| Считать доставку at-least-once доставкой ровно один раз | Повтор задачи отправит письмо второй раз — обработчик обязан быть идемпотентным |
Считать pm.max_children от числа ядер | Машина уходит в swap и становится медленнее, чем была до «оптимизации» |
Поднимать pm.max_children, когда воркер заблокирован на внешнем вызове | Очередь просто переезжает внутрь пула — нужен таймаут и вынос вызова из запроса |
Значение для собеседований
Производительность — та тема, где интервьюер быстро отличает человека, который держал боевую нагрузку, от того, кто читал статьи. Проверяют не список технологий, а порядок действий и механику: с чего вы начинаете (с измерения), что именно кэшируете, что происходит с кэшем на запись и что случится, когда кэш-узел откажет. Кандидат, который говорит «поставлю Redis», не объяснив ключ, инвалидацию и поведение на промахе, отвечает на другой вопрос.
Что обычно проверяют:
- Что кэширует
OPcacheи чем он принципиально отличается отRedisиMemcached. - Почему
APCuиstatic— это кэш на воркер, а не общий, и что вообще переживает запрос в PHP. - Кому помогает
JITи почему обычному веб-приложению он не помогает. - Как вы профилируете медленный эндпоинт и как отличаете N+1 от одного тяжёлого запроса.
- Устройство ключа кэша и то, как запись инвалидирует затронутые записи.
- Что такое стампед и чем его закрывают.
- Что делают
Cache-ControlиETagи в чём разницаpublicиprivate. - Зачем очередь, кто её обрабатывает и почему обработчик должен быть идемпотентным.
- Как считается
pm.max_childrenи как выглядит насыщение пула.
Типичный неверный ответ: «Включим OPcache — он закэширует данные, и запросы станут быстрее». Это и есть главная ошибка темы: OPcache кэширует опкоды ваших PHP-файлов, то есть убирает из каждого запроса парсинг и компиляцию исходников — и ровно ничего больше. Он не хранит результат вашего SQL-запроса, не хранит отрендеренный HTML и не делает медленный запрос быстрым. Кэш данных — это Redis или Memcached, отдельное хранилище, общее для всех воркеров, куда вы кладёте то, что вычислил ваш код. Смешение этих двух вещей — самый частый провал на собеседовании, и сразу за ним идёт второй: «включим JIT» в ответ на вопрос про медленный API, который на самом деле почти всё время ждёт базу.