Skip to content

Инфраструктурная производительность — кеширование и тюнинг стека

Парный к 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 / FastCGINGINXответ целиком (анонимный GET)секунды–минутыexpires, micro-cache TTLNGINX
OPcachePHP RAMскомпилированный байткод PHPдо деплояcachetool / restart FPMPHP-FPM
ApplicationRedisдорогие выборки, рендеры, агрегатыминуты–часыCache::tags([...])->flush()Redis + performance.md
Query / objectRedisрезультаты тяжёлых SQL-запросовминутыпо тегу при изменении моделиRedis
Buffer cachePostgreSQLгорячие страницы таблиц/индексовLRU автоматически— (управляется PG)PostgreSQL
OS page cacheядро (хост)файлы данных PG, не попавшие в shared_buffersLRU автоматическипамять

Принципы:

  • Не дублировать назначение уровней. Статику — на 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_buffers25–40% RAM (не выше 40% — дальше работает OS-кеш)дасобственный буферный кеш PG
effective_cache_size50–75% RAMreloadподсказка планировщику (index vs seq scan), память не выделяет
work_mem(RAM − shared_buffers) / (16 × cores)reloadсортировки, hash join; множится на операции и воркеры
maintenance_work_mem5–10% RAM (потолок ~1–2 GB)reloadVACUUM, CREATE INDEX, ANALYZE
wal_buffers16 MB (или −1 = авто)дабуфер WAL перед сбросом на диск
max_parallel_workers_per_gather1–2; снизить до 1 на время ETLreloadкаждый воркер = ещё один 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), не угадывать.

ПараметрЗначениеЗачем
pmdynamic (или 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_requests500–1000перезапуск воркера → лечит утечки памяти
  • max_children слишком велик → при пике процессы съедают RAM → OOM или swap. Лучше упереться в очередь FPM (502/504), чем уронить сервер.
  • pm=static экономит CPU на форке, но резервирует RAM сразу под все процессы — только когда RAM посчитана точно.

OPcache (prod)

ПараметрЗначениеЗачем
opcache.enable1кеш байткода — даёт до ~70% экономии CPU
opcache.memory_consumption128–256 MBразмер под скомпилированный код
opcache.interned_strings_buffer16–32 MBпул строк
opcache.max_accelerated_files20000+ (≥ число PHP-файлов проекта)иначе вытеснение из кеша
opcache.validate_timestamps0 в prod, 1 в dev0 = не проверять 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

ПараметрЗначениеЗачем
maxmemory50–75% RAM, выделенной под Redisбез лимита Redis съест всю RAM → OOM
maxmemory-policyзависит от назначения (см. ниже)поведение при заполнении

Критично — политика зависит от роли инстанса/базы:

РольpolicyПочему
Cacheallkeys-lruможно терять — вытеснять холодное
Queue (jobs)noevictionнельзя терять задачи — вытеснение = потерянные jobs
Sessionnoeviction (или volatile-lru с TTL)вытеснение = разлогин пользователей
  • Разделять назначения — отдельные Redis-инстансы или базы (database index) для 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 контейнера ≥ внутренний лимит сервиса. PG shared_buffers (+ work_mem-пики) должен помещаться в mem_limit контейнера PG, иначе ядро убьёт процесс внутри.
  • Сумма mem_limit всех контейнеров ≤ лимит WSL2 (не физическая RAM хоста). Конфиг WSL2 — в E:\Clients\CLAUDE.md.
  • cpus / cpuset — ограничивать, чтобы один сервис не вытеснял остальные; PG и PHP-FPM — основные потребители.
  • tmpfs для PG temp / Laravel storage/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 scanEXPLAIN (ANALYZE, BUFFERS), pg_stat_statements
PG buffer hit ratio, temp filespg_stat_database, db:health-check
Slow queries/jobs в prodLaravel Pulse (легче Telescope)
N+1, request timeline (dev)Telescope, Debugbar
OPcache hit rate, памятьcachetool opcache:status
Redis память/вытесненияINFO memory, evicted_keys
PHP-FPM очередь/activepm.status_path

Anti-patterns

ПаттернПроблемаРешение
Сумма лимитов сервисов > RAMOOM под нагрузкойбюджет: Σ лимитов + резерв ≤ RAM
shared_buffers > 40% RAMлишает PG OS page cache, не ускоряет25–40%, остаток — OS-кешу
work_mem без учёта воркеров× параллельные операции → OOMформула + снизить parallel на ETL
Нет ANALYZE после ETLустаревшая статистика → seq scanANALYZE после массовых изменений
allkeys-lru на Redis с очередьювытеснение = потерянные jobsnoeviction, отдельный инстанс/база
Persistence на чисто-кеш Redisлишний I/O, кеш и так прогреетсябез persistence для cache
Cache::flush() при общей базеубивает сессии/throttleCache::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() на каждый include0 + reset FPM на деплое
Тюнинг без профилированиягадание, риск регрессасначала EXPLAIN/Pulse/INFO, потом крутить

Авторитетные источники (сверено 2026-05)

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