Skip to main content
Back to course
Архитектура высоконагруженных веб-приложений
5 / 1145%

Кэширование: контроль, инвалидация и оборона от stampede

80 минут

Для кого: инженеры, которые устали слушать «добавьте кэш» и хотят наконец-то управлять кэшами так же строго, как БД. Если вы хотя бы раз говорили «кэш сам почистится» — эта глава должна выбить вам это из головы.

Провокация №1: вы знаете hit ratio по ключевым эндпоинтам?

  • Если нет: вы не управляете кэшем. Hit ratio = экономика кэша. Без метрик кэш = случайный успех.
  • Проверьте: в мониторинге есть cache_hits, cache_misses, evictions? Если Grafana пустая — начинайте с этого.
  • Цель: для каждого слоя кэша (in-process, Redis, CDN) знать hit ratio, latency и объём памяти. Но hit ratio = не панацея. Считайте стоимость промаха:

miss_cost = (latency_miss - latency_hit) * miss_rate

Иногда 70% hit ratio выгоднее 95%, если эти 70% закрывают самые дорогие запросы.

1. Зачем кэш и когда он помогает

СценарийЧто даёт кэшЧто пойдёт не так
Напряжённая БДСнижает RPS на БДГорячие ключи → stampede
Глобальный контент (CDN)Убирает latency 300ms→30msИнвалидация/пурж статики
Сложные агрегатыУменьшают CPUУстаревшие данные, нужны TTL
Rate limitingТочность в реальном времениНе единый источник правды

Стратегия: кэш — это договор между бизнесом и инженерами. Бизнес говорит, сколько миллисекунд допускается «устаревания». Инженеры реализуют.

Провокация №2: вы знаете, как инвалидация ломает логистику?

Каждый кэш вводит eventual consistency. Вопрос не «обновится ли кэш», а «сколько минут мы укладываемся».

2. Три уровня кэша

2.1 In-process

  • Быстрый, но volatile. Умирает при деплое.
  • Используйте LRU/TTL (lru-cache, @apollo/cache-control). Лимитируйте память.
  • Идеально для дешёвых вычислений, частых повторений.

2.2 Distributed (Redis/Memcached)

  • Держите данные вне приложения. Помните про репликацию и persistence.
  • Архитектуры: Standalone → Sentinel → Cluster.
  • Храните «годится для чтения» state: профили, результаты запросов, rate limiting, счетчики.
КритерийRedisMemcached
Структуры данныхHash, set, sorted set, streamТолько строки
PersistenceЕсть (RDB/AOF, snapshot)Нет
Memory overheadВыше из-за метаданныхНиже, эффективен
СкоростьЧуть медленнееМаксимально быстрая
СложностьКластер, SentinelПроще, но без HA
Когда братьНужны структуры/события/persistenceМаксимальная производительность, простые ключи

2.3 Edge/CDN (CloudFront/Fastly)

  • Кэширует статику, API через Cache-Control, GraphQL CDN.
  • Нужны purge hooks, особое отношение к авторизованным запросам (signed cookies/headers).

3. TTL, инвалидация, consistency

3.1 TTL как политика

  • TTL = компромисс. 5 минут? 1 час? Зависит от бизнес-требований.
  • Auto-expire vs manual invalidation: комбинируйте. Данные, изменяемые вручную — manual + short TTL.

3.2 Паттерны инвалидации

  1. Write-through: пишем в кэш и БД одновременно. Латентность высокой записи.
  2. Write-behind: пишем в кэш, периодически сбрасываем в БД. Опасно без гарантии сброса.
  3. Read-through: кэш сам ходит в БД при miss. Актуален в библиотеках (spring-cache).
  4. Explicit purge: событие «данные обновлены» → DEL key. Требует message bus (Redis Pub/Sub, Kafka).

Техника: если TTL > 0, «битые» данные будут жить весь TTL. Решайте, кто лучше перенесёт задержку.

Провокация №3: у вас был cache stampede?

Если нет — это вопрос времени. Популярный товар, TTL истёк → сотни запросов одновременно рвутся в БД.

3.3 Защита от stampede

  • Mutex per key: при miss первый запрос ставит lock (SETNX lock:key). Остальные ждут или получают stale данные.
  • Stale-while-revalidate: отдаём устаревший ответ + асинхронно обновляем кэш.
  • Request coalescing: singleflight (Go), Groupcache, Redis WAIT.
  • Jitter: TTL + random (±10%) → размазывайте инвалидацию.
async def get_with_mutex(key, ttl=300):
    cached = await redis.get(key)
    if cached:
        return cached
 
    lock_acquired = await redis.set(f"lock:{key}", "1", nx=True, ex=10)
    if not lock_acquired:
        await asyncio.sleep(0.1)
        return await get_with_mutex(key, ttl)
 
    try:
        fresh = await fetch_from_db(key)
        await redis.set(key, fresh, ex=ttl)
        return fresh
    finally:
        await redis.delete(f"lock:{key}")

4. Экономика кэша: считаем ROI и окупаемость

Правило: Кэш без ROI расчета — это не оптимизация, а догадка. Каждый кэш должен окупаться.

4.1 Стоимость кэша: Redis vs Memcached

Сценарий: Нужно 64GB кэша для горячих данных.

Вариант A: Redis Cluster (managed — AWS ElastiCache)

Конфигурация:

  • 3× cache.r6g.xlarge (3 ноды для отказоустойчивости)
  • Memory per node: 26.32GB
  • Total memory: ~79GB (с учетом overhead)
  • Цена: $0.252/час за ноду

Стоимость:

  • 3 ноды × $0.252/час × 730 час/мес = $552/месяц
  • Data transfer (intra-AZ): бесплатно
  • Backups (optional): +$30/мес
  • Total: $582/месяц

Плюсы:

  • ✅ Managed (патчи, failover, backups)
  • ✅ Multi-AZ availability
  • ✅ Persistence (RDB/AOF)

Минусы:

  • 🔴 Дорого
  • 🔴 Vendor lock-in

Вариант B: Redis self-hosted (на EC2)

Конфигурация:

  • 3× r6g.xlarge (32GB RAM каждый)
  • Цена: $0.252/час за инстанс (on-demand)

Стоимость:

  • 3 × $0.252/час × 730 = $552/месяц (серверы)
  • EBS volumes (3×100GB gp3): $24/мес
  • Data transfer: бесплатно (intra-AZ)
  • Total: $576/месяц

Но добавьте DevOps costs:

  • Setup time: 40 часов ($2,000 one-time)
  • Maintenance: 5 часов/месяц ($500/мес)
  • Total with DevOps: $1,076/месяц

Плюсы:

  • ✅ Full control
  • ✅ Customization

Минусы:

  • 🔴 Операционная сложность
  • 🔴 Нужен DevOps

Вариант C: Memcached (managed — AWS ElastiCache)

Конфигурация:

  • 2× cache.r6g.2xlarge (52.82GB каждый)
  • Total memory: ~106GB
  • Цена: $0.504/час за ноду

Стоимость:

  • 2 × $0.504/час × 730 = $736/месяц

Плюсы:

  • ✅ Простота
  • ✅ Высокая производительность

Минусы:

  • 🔴 No persistence (только in-memory)
  • 🔴 No complex data structures

Сравнение стоимости

ВариантMonthly costSetup costDevOps overheadTotal (1 год)
Redis managed (AWS)$582$00%$6,984
Redis self-hosted$576$2,000$500/мес$8,912
Memcached managed$736$00%$8,832

Вывод: Redis managed выигрывает для команд <20 инженеров.


4.2 ROI кэша: формула и реальный расчет

Формула:

ROI = (DB_cost_saved - Cache_cost) / Cache_cost × 100%
 
DB_cost_saved = (queries_without_cache - queries_with_cache) × cost_per_query

Реальный кейс: E-commerce API

До кэша:

  • RDS PostgreSQL: db.r5.2xlarge ($730/мес)
  • Queries: 50M/месяц
  • CPU utilization: 85% (близко к лимиту)
  • P99 latency: 450ms

Проблема: Нужно масштабировать БД до db.r5.4xlarge ($1,460/мес) или добавить read replica ($730/мес).

Решение: Добавить Redis кэш

После кэша:

  • Redis: 3× cache.r6g.large ($276/мес)
  • Hit ratio: 85% (кэшируем топ-товары, категории)
  • Queries to DB: 50M → 7.5M (-85%)
  • CPU utilization: 85% → 35%
  • P99 latency: 450ms → 50ms

Расчет ROI:

Cache_cost = $276/мес
 
DB scaling avoided:
- Вариант без кэша: db.r5.4xlarge = $1,460/мес
- Вариант с кэшем: db.r5.2xlarge = $730/мес
DB_cost_saved = $1,460 - $730 = $730/мес
 
ROI = ($730 - $276) / $276 × 100% = 164%
 
Payback period: немедленно (избегаем апгрейда БД)

Вывод: Кэш окупился в первый месяц и дает 164% ROI.


4.3 Managed vs Self-hosted: когда что выбирать

Managed (ElastiCache, Redis Cloud, MemoryDB):

Когда выбирать:

  • ✅ Команда <20 инженеров
  • ✅ Нет dedicated DevOps для кэша
  • ✅ Критична high availability
  • ✅ Хотите focus on product, not infra

Цена:

  • AWS ElastiCache: $0.252-0.504/час за ноду (зависит от типа)
  • Redis Cloud: от $0.07/GB/час (~$50/мес за 10GB)
  • Azure Cache: аналогично AWS

Self-hosted (на EC2, Kubernetes):

Когда выбирать:

  • ✅ Большой масштаб (50+ нод кэша)
  • ✅ Есть DevOps команда
  • ✅ Специфические требования (custom modules)
  • ✅ Multi-cloud или on-premise

Цена:

  • Серверы: $0.252/час (r6g.xlarge)
  • DevOps: $500-1,000/мес на поддержку
  • Break-even: ~20+ нод кэша

4.4 Стоимость разных стратегий кэширования

Сценарий: API с 10M запросов/день, нужен кэш.

Стратегия 1: In-memory (process cache)

Реализация: Node.js lru-cache, Python functools.lru_cache

Стоимость:

  • $0 (используем RAM серверов)

Плюсы:

  • ✅ Бесплатно
  • ✅ Быстро (микросекунды)

Минусы:

  • 🔴 Умирает при деплое
  • 🔴 Не shared между инстансами
  • 🔴 Ограничена RAM сервера

Когда использовать: Маленькие данные (<100MB), короткие TTL (<5 минут), stateless приложения.


Стратегия 2: Redis (distributed cache)

Реализация: Redis Cluster (3 ноды)

Стоимость:

  • $276-582/месяц (зависит от размера)

Плюсы:

  • ✅ Shared между инстансами
  • ✅ Persistence
  • ✅ Переживает деплои

Минусы:

  • 🔴 Latency ~1-2ms (vs микросекунды in-memory)
  • 🔴 Стоимость

Когда использовать: Критичные данные, нужен shared state, hit ratio >80%.


Стратегия 3: CDN (edge cache)

Реализация: CloudFront / Fastly / Cloudflare

Стоимость:

  • CloudFront: $0.085/GB (первые 10TB)
  • При 500GB/мес трафика = $42.5/месяц

Плюсы:

  • ✅ Глобальное распределение
  • ✅ Минимальная latency для пользователей

Минусы:

  • 🔴 Только для статики или cacheable API
  • 🔴 Инвалидация сложнее

Когда использовать: Статика, публичные API, географически распределенные пользователи.


Сравнение стратегий

СтратегияLatencyCost/monthHit ratio targetBest for
In-memory0.1ms$0любойHot data, короткий TTL
Redis1-2ms$276-582>80%Shared state, persistence
CDN10-50ms$42+>90%Статика, public API
Multi-layer0.1-50ms$318+>95%Максимальная производительность

4.5 Multi-layer caching: стоимость и эффект

Архитектура:

  1. In-memory (LRU, 100MB) — горячие данные
  2. Redis (10GB) — shared cache
  3. CDN (CloudFront) — статика и cacheable API

Стоимость:

  • In-memory: $0
  • Redis: 3× cache.r6g.large = $276/мес
  • CDN: 500GB/мес = $42/мес
  • Total: $318/месяц

Эффект (реальный кейс):

МетрикаБез кэшаС multi-layerУлучшение
P50 latency150ms5ms-97%
P99 latency800ms50ms-94%
DB queries/sec1,500200-87%
DB cost$1,460/мес$730/мес-$730/мес
Infrastructure total$1,460$1,048-$412/мес

ROI: $412 экономии - $318 кэш = $94/мес profit + огромный прирост производительности.


4.6 Реальный кейс: оптимизация кэша сэкономила $50k/год

Компания: SaaS B2B платформа, 500k пользователей

Было:

  • Redis: 10× cache.r5.2xlarge = $3,650/месяц
  • Hit ratio: 60% (низкий!)
  • Evictions: 1M/день
  • Problem: over-provisioned + неправильный TTL

Проблемы:

  • ❌ Кэшировали всё подряд (даже редкие запросы)
  • ❌ TTL = 1 час (слишком долго для динамичных данных)
  • ❌ No eviction policy tuning

Что сделали:

  1. Анализ hit ratio по ключам:

    • Топ-10% ключей = 90% хитов
    • 70% ключей никогда не читались повторно
  2. Оптимизация:

    • Кэшируем только топ-10% горячих данных
    • TTL: 1 час → 10 минут (для динамичных) / 4 часа (для статичных)
    • Eviction policy: allkeys-lruvolatile-lru (только с TTL)
  3. Right-sizing:

    • 10× cache.r5.2xlarge → 3× cache.r6g.xlarge
    • Memory: 200GB → 79GB (достаточно для горячих данных)

Результат:

  • Стоимость: $3,650 → $552/месяц (-85%)
  • Hit ratio: 60% → 92% (+32%)
  • Evictions: 1M/день → 10k/день (-99%)
  • P99 latency: без изменений (50ms)

Экономия: $3,098/месяц × 12 = $37,176/год ROI работы: Оптимизация заняла 2 недели ($4k зарплат). Окупилось за 1.3 месяца.


4.7 Cost optimization checklist для кэша

  • Измерьте hit ratio: Если <80%, кэш неэффективен
  • Проверьте evictions: Если >1% от записей/день, нужно больше RAM
  • Анализ ключей: 80/20 rule — 20% ключей дают 80% хитов. Кэшируйте только их.
  • TTL tuning: Не кэшируйте дольше, чем нужно бизнесу
  • Right-sizing: Проверьте memory utilization (должно быть 70-80%)
  • Managed vs Self-hosted: При <10 нодах managed выгоднее
  • Reserved Instances: Если кэш стабильный, экономьте 40% через RI
  • Multi-layer: In-memory + Redis часто дешевле, чем огромный Redis

4.8 Формула: стоит ли добавлять кэш?

def should_add_cache(db_queries_per_month, db_cost_per_month, expected_hit_ratio):
    """
    Решает, окупится ли кэш
    """
    # Стоимость кэша (базовый Redis кластер)
    cache_cost = 276  # $276/мес (3× cache.r6g.large)
 
    # Queries after cache
    queries_after_cache = db_queries_per_month * (1 - expected_hit_ratio)
 
    # DB cost saved (пропорционально снижению queries)
    db_load_reduction = 1 - (queries_after_cache / db_queries_per_month)
 
    # Если снижаем нагрузку >50%, можем downgrade БД
    if db_load_reduction > 0.5:
        db_cost_saved = db_cost_per_month * 0.5  # Экономим половину
    else:
        db_cost_saved = 0  # Пока не можем downgrade
 
    # ROI
    monthly_profit = db_cost_saved - cache_cost
    roi = (monthly_profit / cache_cost) * 100 if cache_cost > 0 else 0
 
    # Decision
    if monthly_profit > 0:
        return True, f"ROI: {roi:.0f}%, profit: ${monthly_profit}/мес"
    else:
        return False, "Кэш не окупается"
 
# Пример
should_add_cache(
    db_queries_per_month=50_000_000,
    db_cost_per_month=730,  # db.r5.2xlarge
    expected_hit_ratio=0.85
)
# Output: (True, "ROI: 164%, profit: $454/мес")

Провокация №4: зачем вам два кэша подряд?

  • Multi-layer caching: in-process (микросекунды) → Redis (миллисекунды) → CDN (десятки миллисекунд).
  • Consistency: каждый слой добавляет задержку. Нужно мониторить разницу.

4.1 Пример: API → Redis → CDN

  1. Клиент → CDN (hit? отдаём за 30 мс).
  2. Miss → API + Redis.
  3. В API сначала проверяем локальный кэш (LRU), чтобы не ударить Redis без надобности.

Логика: CDN ttl = 60 секунд, Redis ttl = 10 минут, локальный кэш = 30 секунд. Решайте, когда данные «достаточно свежие» для бизнеса.

4.2 Cache warming

  • За 5–10 минут до пика прогрейте топ-N ключей (например, топ-1000 товаров).
  • Используйте очереди/воркеры для параллельной загрузки.
  • Метрика: hit ratio > 80% уже в первую минуту пика.

4.3 Hot keys

  • Мониторьте горячие ключи: redis-cli --hotkeys или INFO hotkeys.
  • Если hot key перегревает Redis, реплицируйте данные локально (in-process LRU) или используйте sharding по ключам.

5. Аннотация паттернов

ПаттернКогда применятьОпасности
Cache-asideБазовый, гибкийИнвалидация вручную
Write-throughСогласованностьВысокая latency при записи
Write-behindБыстрые записиПотеря данных, нужен durable queue
Stale-while-revalidateUX > консистентностьУвидите старые данные
Bloom filter перед кэшемДля предотвращения попадания несуществующих ключейТребует синхронизации

Провокация №5: у вас кэш без observability?

Если нет дашбордов для hit ratio, evictions, latency, вы только строите замок на песке.

5.1 Метрики

  • cache_hits, cache_misses, hit_ratio = hits / (hits + misses).
  • evictions, keys_removed, used_memory.
  • latency_ms, connection_errors, CPU на нодах Redis.
  • cmdstat_* (Redis) — разбейте по операциям (get, set, incr).
  • cache_freshness_seconds — сколько времени прошло с последнего обновления ключа.
  • stale_reads_per_second — если используете stale-while-revalidate.

5.2 Алерты

МетрикаПорог
Hit ratio < 85%кэш бесполезен
Evictions > 100/sувеличьте память или TTL
Redis latency > 5 msищите сетевые проблемы
Replication lag > 100 msриск потерять данные

Практика: без этого кэш = игрушка

  1. Observability:
    • Настройте дашборды hit ratio / latency для всех кэшей.
    • Добавьте alert на hit ratio < 85%.
  2. Stampede drill:
    • Создайте популярный ключ, истощите TTL, запустите 1k запросов.
    • Реализуйте mutex или stale-while-revalidate и повторите.
  3. Инвалидация:
    • Реализуйте событие «data changed» → DEL key через очередь.
    • Проверьте, что purge работает (<5 секунд).
  4. Экономика:
    • Посчитайте стоимость RAM и выгоду. Докажите бизнесу, что кэш окупается (или нет).
  5. Многоуровневый кэш:
    • Внедрите локальный LRU перед Redis на одном эндпоинте.
    • Измерьте снижение нагрузки на Redis.

Безопасность: не храните персональные данные в кэше без шифрования. Ограничьте TTL для чувствительной информации (минуты). Регулярно проверяйте, что удалённые данные действительно удалены из всех слоёв.

Что дальше

Дальше — очереди и событийные системы. Вы уже умеете контролировать state через кэш, теперь пора научиться управлять потоком событий, backpressure и распределёнными транзакциями. Ждите такой же беспощадной конкретики.

Кэширование: контроль, инвалидация и оборона от stampede — Архитектура высоконагруженных веб-приложений — Potapov.me