Тема
Фаза 0 — Производительность и кеширование
Статус: спека фазы 0, сквозная. Требования: публичный сайт держит нагрузку при минимальных запросах к БД; кеширование эффективно, инвалидируется точно и отлаживается инструментами, а не гаданием; система утилизирует многоядерные CPU. Стыкуется с тремя осями (кеш настроек, события инвалидации), WidgetRegistry (кеш областей), контрактом темы (кеш резолва шаблонов) и центром обновлений (прогрев после апгрейда).
Главный инвариант: бюджет запросов к БД
| Сценарий | Бюджет БД | Чем обеспечивается |
|---|---|---|
| Публичная страница из page-cache | 0 запросов | теговый 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 — процессная модель (не потоки); ядра утилизируются пулами процессов, и это надо настроить, а не надеяться:
| Уровень | Механизм | Правило |
|---|---|---|
| HTTP | PHP-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-расширение, тяжелее в отладке; для реально нагруженных |
| RoadRunner | Go-супервизор, стабильный, гибкие воркер-пулы | отдельный бинарь/конфиг; когда нужен зрелый воркер-менеджер без 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» + метрики в телеметрию.
Связи
- Три оси — settings-кеш, события инвалидации.
- WidgetRegistry — кеш областей.
- Контракт темы — компиляция каскада.
- Центр обновлений — прогрев в
cms:postupgrade, метрики волны. - Модель e-commerce — сегментация кеша цен по группам.