Skip to content

Фаза 0 — Производительность и кеширование

Статус: спека фазы 0, сквозная. Требования: публичный сайт держит нагрузку при минимальных запросах к БД; кеширование эффективно, инвалидируется точно и отлаживается инструментами, а не гаданием; система утилизирует многоядерные CPU. Стыкуется с тремя осями (кеш настроек, события инвалидации), WidgetRegistry (кеш областей), контрактом темы (кеш резолва шаблонов) и центром обновлений (прогрев после апгрейда).

Главный инвариант: бюджет запросов к БД

СценарийБюджет БДЧем обеспечивается
Публичная страница из page-cache0 запросовтеговый HTML-кеш отдаёт до бутстрапа моделей
Первый рендер страницы (мимо кеша)фиксированный бюджет (~15 запросов, контрактный тест)eager loading, кеш настроек/меню/областей, cms_block_usage materialized
setting() в любом количестве0 на ключгруппа настроек читается из кеша целиком; БД — только на первый прогрев группы
Админка (Filament)без page-cache, но N+1 = баг->with(), withCount(), контроль в CI

N+1 — это баг, а не «оптимизация потом» (code-quality spec). Контрактный тест ядра рендерит демо-страницу со всеми типами блоков и падает при превышении бюджета запросов — регресс производительности ловится как обычный красный тест.

Слои кеша (сводная карта)

СлойЧто хранитКлюч/тегИнвалидацияПрогрев
Page-cache (теговый)HTML published-страницpage:{id} + теги областей + locale/city в ключесобытия PagePublished, SettingChanged (зависимые), WidgetSaved (по тегу области)очередь после инвалидации, cms:postupgrade
Областифрагменты <x-cms-area>area:{name}WidgetSaved/WidgetDeleted — только свой тегвместе со страницами
Настройкигруппа cms_settings целикомsettings:{group} (+locale/city)SettingChanged — только своя группадеплой / первое обращение
Менюдерево пунктовmenu:{id}PageSaved/правка менюдеплой
SEO-агрегаты{count}, {price_from} и др. вычисляемые переменныеключ по scopeкороткий TTL (минуты) — допустима лёгкая несвежестьленивый
Резолв шаблоновкарта «view → путь файла» по каскаду темкомпилированный манифестактивация темы / деплой (контракт темы)cms:theme:use, деплой
Реестрыскомпилированные BlockRegistry/WidgetRegistry/FieldTypeRegistryманифестдеплой, enable/disable модуляавтоматом
ИнфраструктурныйOPcache, config:/route:/view:/event:cacheшаг 9–11 процедуры обновлениядеплой

Правила:

  • инвалидация только тегамиCache::tags([...])->flush(); глобальный Cache::flush() запрещён линтером (сбрасывает сессии и чужие слои);
  • каждая инвалидация — событие (CacheFlushRequested с тегами) → лог + слушатели (CDN-purge, прогрев); «кто сбросил кеш и почему» всегда восстановимо из журнала;
  • ключ page-cache обязан включать tenant/site (при соответствующих модулях), locale и city (заготовка Q6; ревизия 14.07.2026: tenant/site резолвятся до обращения к page-cache), плюс задекларированные модулями измерения сегментации (полностраничный режим персонализации — вариант A/B и т.п., с лимитом кардинальности) — иначе мультигород/мультиязычность отравят кеш;
  • страница с персональными данными в HTML (цены группы пользователя, имя) не попадает в общий page-cache: фрагмент выносится в остров/ESI-фрагмент либо ключ сегментируется по группе (модель цен).

Настройки модулей: множество, но дёшево

Каждый модуль привозит десятки настроек в БД (SettingSchema, ось 3). Чтобы это не стало нагрузкой на БД:

  • чтение — всегда через кеш группы: первый setting('catalog.*') поднимает группу одним запросом, дальше — память процесса + Redis;
  • прогрев всех групп — на деплое и в cms:postupgrade (сайт не встречает трафик холодным);
  • SettingChanged инвалидирует свою группу и зависимые страницы (декларация affectsPageCache: true на настройке — не каждая настройка сбрасывает page-cache);
  • запрещено читать cms_settings мимо фасада setting() (контрактный тест).

Защита от штампида и гонок

  • Cache stampede: тяжёлые значения прогреваются под Cache::lock() — один процесс считает, остальные ждут/отдают stale (Cache::flexible() — stale-while-revalidate для агрегатов);
  • инвалидация → прогрев через очередь, не в HTTP-запросе изменившего;
  • идемпотентные джобы прогрева (uniqueness по ключу) — волна правок не порождает шторм;
  • локи — только точечные и короткие (Redis), никаких глобальных блокировок в рантайме ядра.

Многоядерные CPU: где берётся параллелизм

PHP — процессная модель (не потоки); ядра утилизируются пулами процессов, и это надо настроить, а не надеяться:

УровеньМеханизмПравило
HTTPPHP-FPM пул (pm.max_children ≈ ядра × запас) — базовый режим; Octane (Swoole/RoadRunner/FrankenPHP worker-mode) — опция для нагруженных сайтовядро пишется Octane-safe с первого дня (см. ниже), но включается воркер-режим отдельным шагом после аудита готовности, не требование по умолчанию
ОчередиHorizon: пулы воркеров per-очередь (default/notifications/heavy/integrations), auto-balanceтяжёлая очередь не забирает ядра у быстрых
Пакетные задачиBus::batch() — импорт/экспорт/массовые миграции чанками параллельночанк идемпотентен; прогресс в UI
БДPostgreSQL: индексы под WHERE/ORDER BY, лёгкие выборкиmax_parallel_workers_per_gather консервативно (правила инфры студии — риск OOM); тяжёлая аналитика — в очередь, не в запрос

Ядро не хранит состояние в статике процесса (кроме кешей с явной инвалидацией) — любой воркер взаимозаменяем, деплой перезапускает пулы (queue:restart).

Laravel Octane (воркер-режим): когда и как

Octane держит приложение забутстрапленным в долгоживущих воркерах — фреймворк не поднимается заново на каждый запрос. Для read-heavy публичного сайта (зона 1) это крупнейший разовый выигрыш латентности после page-cache: убирается стоимость boot'а (регистрация провайдеров, каскад тем, реестры блоков/полей/виджетов) на каждом хите. Но это смена рантайма, а не библиотека, — включается осознанно и только на Octane-safe кодовой базе.

Почему после page-cache, а не вместо. Порядок выигрышей: сначала бюджет запросов и page-cache (хит без БД) — они дают больший эффект и проще откатываются; Octane — следующий множитель на том, что уже быстрое и без утечек. Логичный этап внедрения — после волны page-cache/бюджета запросов, когда дисциплина «состояние только в кеше с инвалидацией» уже проверена контрактными тестами.

Готовность (Octane-safety) — обязательный аудит перед включением:

  • никакого request-состояния в синглтонах и статике. Синглтон, живущий между запросами, не должен копить данные текущего запроса. Пример из ядра: пер-запросный мемо резолва шаблонов в ThemeManager — под FPM безвреден, под Octane протёк бы между запросами. Такие мемо-кеши сбрасываются на RequestReceived/RequestTerminated (слушатель Octane) либо выносятся из синглтона в scoped-контейнер (app()->scoped()).
  • реестры (блоки/поля/виджеты/темы) — иммутабельны после boot и состояния запроса не несут: их удержание между запросами — плюс (ради него и берём Octane), не риск.
  • фасады Auth/Request/URL и config() не кешировать в свойствах объектов, переживающих запрос; брать из контейнера на каждый вызов.
  • сбрасывать сторонние синглтоны с состоянием через octane.flush / warm (конфиг), следить за утечкой памяти воркеров (--max-requests, перезапуск).
  • контрактный тест «нет утечки между запросами»: два последовательных запроса с разной темой/локалью/пользователем в одном воркере не видят чужого состояния (гоняется под Octane-тест-раннером).

Выбор драйвера:

ДрайверПлюсыМинусы / когда
FrankenPHPодин бинарь (сервер+worker), HTTP/2-3, статика, проще Docker; совпадает с базовым образомсамый молодой; дефолт студии для новых сайтов
Swoole/OpenSwooleмаксимум throughput, корутины, тикеры/таблицы в памятиPECL-расширение, тяжелее в отладке; для реально нагруженных
RoadRunnerGo-супервизор, стабильный, гибкие воркер-пулыотдельный бинарь/конфиг; когда нужен зрелый воркер-менеджер без Swoole

Рекомендация: FrankenPHP как основной (совпадает с продовым образом и HTTP/3), Swoole — точечно для самых нагруженных сайтов парка. RoadRunner — совместимая альтернатива. Профиль сайта в манифесте/инфре решает, включён ли воркер-режим; ядро одинаково работает и под FPM, и под Octane.

Совместное с остальным: Horizon/очереди и Bus::batch не меняются; queue:restart дополняется octane:reload на деплое (грациозный рестарт воркеров без даунтайма); Pulse и health-check «cache-alive» продолжают мерить p95 — по нему и виден выигрыш от Octane.

Отладка кеширования (обязательный инструментарий)

Кеш без отладки — источник «мистических» багов. Ядро обязано дать:

  • cms:cache:inspect {url|key} — для страницы/ключа: слой, теги, когда положено, каким событием будет инвалидировано;
  • debug-заголовки (только APP_DEBUG/по правам): X-Cms-Cache: hit|miss|bypass, X-Cms-Cache-Tags: page:5,area:footer,...;
  • панель «Кеш» в Filament: hit-rate по слоям, объём, журнал последних инвалидаций (тег, событие-причина, инициатор);
  • cms:cache:flush --tag=... — точечный сброс из CLI; кнопки точечного сброса в панели (не «сбросить всё»);
  • bypass для отладки: подписанный параметр (аналог preview-токена) рендерит страницу в обход кеша, не трогая его;
  • контрактный тест инвалидации: правка сущности сбрасывает ровно свои теги — и не меньше (стейл), и не больше (штормы).

Телеметрия производительности

  • laravel/pulse на сайтах парка — медленные запросы/джобы/эндпоинты дёшево и локально;
  • health-check «cache-alive»: Redis доступен, hit-rate page-cache не упал ниже порога (деградация кеша видна до жалоб клиента);
  • метрики в суточный пинг телеметрии (центр обновлений): p95 времени ответа, hit-rate — флит-дашборд видит деградацию после волны обновлений.

Чеклист фазы 0 (производительность)

  • [ ] Бюджет запросов к БД на демо-странице — контрактный тест.
  • [ ] Карта слоёв кеша + правило «инвалидация только тегами», Cache::flush() запрещён.
  • [ ] Ключ page-cache включает tenant/site/locale/city (+измерения сегментации модулей); персонализация — только два штатных режима из ТЗ ядра: блочный (personalized-блок → каркас в кеше, фрагмент островом/фрагмент-кешем) и полностраничный (доп. измерение ключа с лимитом кардинальности).
  • [ ] Кеш групп настроек + прогрев на деплое; чтение мимо setting() запрещено.
  • [ ] Stampede-защита: Cache::lock/flexible, прогрев через очередь, идемпотентность.
  • [ ] Пулы: FPM под ядра, Horizon per-очередь, Bus::batch для тяжёлого.
  • [ ] Octane-safety: нет request-состояния в синглтонах/статике; мемо-кеши сбрасываются на RequestTerminated; контрактный тест «нет утечки между запросами». Включение воркер-режима (FrankenPHP по умолчанию, Swoole/RoadRunner опция) — после page-cache.
  • [ ] cms:cache:inspect, debug-заголовки, панель «Кеш», точечный flush, bypass-токен.
  • [ ] Контрактный тест точности инвалидации (не меньше и не больше своих тегов).
  • [ ] Pulse + health-check «cache-alive» + метрики в телеметрию.

Связи

Внутренняя база знаний студии