Тема
Инфраструктурная производительность — кеширование и тюнинг стека
Парный к performance.md (application-слой: N+1, кеш-теги, очереди, Octane). Здесь — инфраструктурный слой: уровни кеширования, выделение памяти, тюнинг PostgreSQL / PHP-FPM / Redis / NGINX / Docker.
Формулы и принципы, не привязка к железу. Конкретные числа считаются по формулам под фактическую RAM/CPU (бюджет сервера — в
E:\Clients\CLAUDE.md, WSL2-лимит и зафиксированные PG-параметры — там же).Порядок секций — по пути HTTP-запроса: от пользователя (CDN/NGINX) вниз до диска (PostgreSQL).
Иерархия уровней кеширования
Запрос проходит сверху вниз; каждый уровень, отдавший ответ, экономит все нижние. Цель — поймать запрос как можно выше.
| Уровень | Где | Что кеширует | TTL | Инвалидация | Секция |
|---|---|---|---|---|---|
| CDN / edge | вне сервера | статика, иногда анонимный HTML | часы–сутки | purge по URL / hash в имени файла | NGINX |
| HTTP / FastCGI | NGINX | ответ целиком (анонимный GET) | секунды–минуты | expires, micro-cache TTL | NGINX |
| OPcache | PHP RAM | скомпилированный байткод PHP | до деплоя | cachetool / restart FPM | PHP-FPM |
| Application | Redis | дорогие выборки, рендеры, агрегаты | минуты–часы | Cache::tags([...])->flush() | Redis + performance.md |
| Query / object | Redis | результаты тяжёлых SQL-запросов | минуты | по тегу при изменении модели | Redis |
| Buffer cache | PostgreSQL | горячие страницы таблиц/индексов | LRU автоматически | — (управляется PG) | PostgreSQL |
| OS page cache | ядро (хост) | файлы данных PG, не попавшие в shared_buffers | LRU автоматически | — | память |
Принципы:
- Не дублировать назначение уровней. Статику — на NGINX/CDN, не в Redis. SQL-результаты — в Redis, не в FastCGI-cache.
- Чем выше уровень — тем грубее инвалидация. FastCGI-cache персонализированной страницы = утечка чужих данных между пользователями.
- Кеш-теги (Redis) дают точечную инвалидацию — основной рабочий слой для Laravel. Детали и код — в performance.md.
- Tags работают только на Redis/Memcached, не на file/database driver.
Выделение памяти — бюджет сервера
Главное правило: сумма верхних границ всех потребителей RAM ≤ физическая RAM. Превышение → ядро убивает процесс по OOM (типично — PostgreSQL под нагрузкой ETL).
Потребители делят RAM:
RAM = PostgreSQL (shared_buffers + N×work_mem + maintenance_work_mem)
+ Redis (maxmemory)
+ PHP-FPM (pm.max_children × средний_процесс)
+ OS page cache + хост (~15–25%, резерв)- work_mem множится на число одновременных сортировок/хешей × параллельные воркеры — это самая частая причина недооценки PG в бюджете (см. PostgreSQL ниже).
- Резерв хосту/OS обязателен. OS page cache — это «бесплатный» второй уровень кеша PostgreSQL; забрать всю RAM под shared_buffers/Redis = лишить PG этого кеша.
- В Docker/WSL2 бюджет считается от лимита WSL2, не от физической RAM хоста (конфиг
.wslconfig— вE:\Clients\CLAUDE.md). - Менять параметры памяти — по одному, с замером. «Поднял всё разом» → OOM без понимания виновника.
PostgreSQL
Формулы (считать от RAM, выделенной под БД-инстанс, а не всего сервера):
| Параметр | Формула / значение | Restart? | Зачем |
|---|---|---|---|
shared_buffers | 25–40% RAM (не выше 40% — дальше работает OS-кеш) | да | собственный буферный кеш PG |
effective_cache_size | 50–75% RAM | reload | подсказка планировщику (index vs seq scan), память не выделяет |
work_mem | (RAM − shared_buffers) / (16 × cores) | reload | сортировки, hash join; множится на операции и воркеры |
maintenance_work_mem | 5–10% RAM (потолок ~1–2 GB) | reload | VACUUM, CREATE INDEX, ANALYZE |
wal_buffers | 16 MB (или −1 = авто) | да | буфер WAL перед сбросом на диск |
max_parallel_workers_per_gather | 1–2; снизить до 1 на время ETL | reload | каждый воркер = ещё один work_mem |
Правила:
shared_buffers— единственный из перечисленных, что требует restart (в Docker — рестарт контейнера PG). Остальные —SELECT pg_reload_conf();.work_mem— не «чем больше, тем лучше». Тяжёлый запрос с несколькими сортировками × параллельные воркеры легко съестwork_mem × 10. При массовых UPDATE/INSERT снижатьmax_parallel_workers_per_gather=1, потом вернуть.- После массового ETL/импорта —
ANALYZE table_name: статистика планировщика устаревает, иначе seq scan вместо индекса. - Проверять buffer cache hit ratio и temp-file usage — основной сигнал, что shared_buffers/work_mem малы.
- Индексы — первый рычаг до кеширования, не после. Детали (GIN/partial/covering/
CONCURRENTLY, аудит) — в ../migrations.md.
PHP-FPM + OPcache
Pool (процессы)
pm.max_children = RAM_под_PHP / средний_размер_процессаСредний размер процесса замерять реально (ps, обычно 30–80 MB для Laravel), не угадывать.
| Параметр | Значение | Зачем |
|---|---|---|
pm | dynamic (или static при стабильной нагрузке и known RAM) | стратегия пула |
pm.max_children | по формуле выше | потолок параллельных запросов = главный лимит RAM |
pm.start_servers | ~25% от max_children | стартовый прогрев |
pm.min_spare_servers | ~25% от max_children | минимум готовых |
pm.max_spare_servers | ~75% от max_children | максимум простаивающих |
pm.max_requests | 500–1000 | перезапуск воркера → лечит утечки памяти |
max_childrenслишком велик → при пике процессы съедают RAM → OOM или swap. Лучше упереться в очередь FPM (502/504), чем уронить сервер.pm=staticэкономит CPU на форке, но резервирует RAM сразу под все процессы — только когда RAM посчитана точно.
OPcache (prod)
| Параметр | Значение | Зачем |
|---|---|---|
opcache.enable | 1 | кеш байткода — даёт до ~70% экономии CPU |
opcache.memory_consumption | 128–256 MB | размер под скомпилированный код |
opcache.interned_strings_buffer | 16–32 MB | пул строк |
opcache.max_accelerated_files | 20000+ (≥ число PHP-файлов проекта) | иначе вытеснение из кеша |
opcache.validate_timestamps | 0 в prod, 1 в dev | 0 = не проверять mtime → после деплоя нужен reload FPM |
opcache.jit / jit_buffer_size | для CPU-bound (вычисления) | на типичном I/O-bound web эффект мал |
validate_timestamps=0обязателен в prod, но требуетcachetool opcache:resetили restart FPM после каждого деплоя — иначе старый код.- Preloading горячего кода — дополнительный буст для часто используемых классов.
Redis
| Параметр | Значение | Зачем |
|---|---|---|
maxmemory | 50–75% RAM, выделенной под Redis | без лимита Redis съест всю RAM → OOM |
maxmemory-policy | зависит от назначения (см. ниже) | поведение при заполнении |
Критично — политика зависит от роли инстанса/базы:
| Роль | policy | Почему |
|---|---|---|
| Cache | allkeys-lru | можно терять — вытеснять холодное |
| Queue (jobs) | noeviction | нельзя терять задачи — вытеснение = потерянные jobs |
| Session | noeviction (или volatile-lru с TTL) | вытеснение = разлогин пользователей |
- Разделять назначения — отдельные Redis-инстансы или базы (
databaseindex) для cache / queue / session. Однаallkeys-lruна всё = очередь и сессии под угрозой вытеснения. - Persistence:
- Чистый кеш — persistence часто не нужна (после рестарта кеш прогреется заново; экономит I/O).
- Очередь/сессии — нужна durability: AOF с
appendfsync everysec(компромисс скорость/надёжность) или hybrid RDB+AOF (быстрый рестарт + write-by-write).
Cache::flush()бьёт по всей базе — если cache и session в одной, убьёт сессии. ТолькоCache::tags([...])->flush()(см. performance.md).
NGINX
На периметре TLS терминирует Caddy (
E:\Clients\Windows\proxy). NGINX здесь — внутренний web/FastCGI-слой контейнера сайта. HTTP/2-3 и LE-сертификаты — на Caddy.
nginx
worker_processes auto; # один на ядро
events {
worker_connections 8192;
use epoll;
multi_accept on;
}Сжатие
| Контент | Подход |
|---|---|
| Динамика (HTML/JSON) | gzip уровень 5–6 / brotli 4–6 (баланс CPU/размер) |
| Статика (CSS/JS) | pre-compressed: gzip_static on / brotli 11 при сборке, отдавать готовое |
gzip даёт −60–80% размера, brotli — ещё −15–25%. Не сжимать уже сжатое (jpg/png/woff2).
Статика
nginx
location ~* \.(css|js|woff2|jpg|png|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
gzip_static on;
}immutable + хеш в имени файла (Vite manifest) = браузер никогда не перепроверяет — версия меняется через новое имя.
FastCGI micro-cache (осторожно)
Кеш ответа PHP-FPM на секунды — до ~1000x на анонимный трафик. Только для полностью анонимных GET:
- ❌ НЕ применять к страницам с сессией, CSRF-токеном, корзиной, персонализацией — отдаст ответ одного пользователя другому.
- ✅ Обходить кеш при наличии cookie сессии / для POST.
- TTL — секунды (micro-cache): сглаживает пики, не «замораживает» контент надолго.
- На большинстве Laravel-страниц предпочтительнее application-кеш (Redis tags) с точечной инвалидацией.
Docker — ресурсы и память
mem_limitконтейнера ≥ внутренний лимит сервиса. PGshared_buffers(+ work_mem-пики) должен помещаться вmem_limitконтейнера PG, иначе ядро убьёт процесс внутри.- Сумма
mem_limitвсех контейнеров ≤ лимит WSL2 (не физическая RAM хоста). Конфиг WSL2 — вE:\Clients\CLAUDE.md. cpus/cpuset— ограничивать, чтобы один сервис не вытеснял остальные; PG и PHP-FPM — основные потребители.tmpfsдля PG temp / Laravelstorage/framework/cacheсессий — снижает диск I/O (данные эфемерны).healthcheckна каждый сервис — оркестратор не шлёт трафик в неготовый контейнер (холодный OPcache/buffer pool).swapмаленький (2 GB): большой swap маскирует OOM и замедляет — лучше явный OOM, чем тихая деградация.
Индексы БД
Индекс — первый рычаг производительности до любого кеширования: правильный индекс устраняет seq scan, и запрос может вообще не нуждаться в кеше.
Полностью покрыто в ../migrations.md: partial / functional / GIN на tsvector / covering (INCLUDE), CREATE INDEX CONCURRENTLY на больших таблицах, db:index-audit для поиска неиспользуемых.
Связь со слоями: профилируй (EXPLAIN) → добавь индекс → если запрос всё ещё дорогой и часто повторяется → кешируй результат в Redis. Не наоборот.
Профилировать до тюнинга
Любой параметр меняется по замеру, не по наитию:
| Что смотреть | Инструмент |
|---|---|
| Медленные SQL, seq scan | EXPLAIN (ANALYZE, BUFFERS), pg_stat_statements |
| PG buffer hit ratio, temp files | pg_stat_database, db:health-check |
| Slow queries/jobs в prod | Laravel Pulse (легче Telescope) |
| N+1, request timeline (dev) | Telescope, Debugbar |
| OPcache hit rate, память | cachetool opcache:status |
| Redis память/вытеснения | INFO memory, evicted_keys |
| PHP-FPM очередь/active | pm.status_path |
Anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
| Сумма лимитов сервисов > RAM | OOM под нагрузкой | бюджет: Σ лимитов + резерв ≤ RAM |
shared_buffers > 40% RAM | лишает PG OS page cache, не ускоряет | 25–40%, остаток — OS-кешу |
work_mem без учёта воркеров | × параллельные операции → OOM | формула + снизить parallel на ETL |
Нет ANALYZE после ETL | устаревшая статистика → seq scan | ANALYZE после массовых изменений |
allkeys-lru на Redis с очередью | вытеснение = потерянные jobs | noeviction, отдельный инстанс/база |
| Persistence на чисто-кеш Redis | лишний I/O, кеш и так прогреется | без persistence для cache |
Cache::flush() при общей базе | убивает сессии/throttle | Cache::tags([...])->flush() |
| FastCGI-cache персонализированных страниц | утечка данных между юзерами | только анонимный GET, обход по cookie |
mem_limit контейнера < лимита сервиса | ядро убивает процесс внутри | mem_limit ≥ shared_buffers/maxmemory |
pm.max_children слишком велик | пик процессов → OOM/swap | формула RAM/процесс; лучше очередь FPM |
validate_timestamps=1 в prod | лишние stat() на каждый include | 0 + reset FPM на деплое |
| Тюнинг без профилирования | гадание, риск регресса | сначала EXPLAIN/Pulse/INFO, потом крутить |
Авторитетные источники (сверено 2026-05)
- PostgreSQL: wiki Tuning Your Server, Percona, EDB memory tuning, Mydbops 2025
- PHP-FPM / OPcache: helpsysadmin max_children, oneuptime FPM tuning
- Redis: персистентность (docs), memory optimization (docs), Percona best practices
- NGINX: FastCGI cache для Laravel, gzip/brotli (NGINX docs)
- Уровни кеша: Opcode/Object/Edge stack, Redis cache optimization