Программа курса

Модуль 02 / Урок 9 из 27

Проектирование кеша под нагрузкой

Теория и практика

Собираем модуль в инженерную методику: расчёт памяти, целевые метрики, выбор TTL, стратегии инвалидации и разборный пример кеша карточки товара на 50k RPS.

Шаг 1. Оценка памяти до запуска

Грубая модель: память ≈ N_ключей × (ключ + значение + ~70 байт накладных). Накладные — robj, запись словаря, SDS-заголовки, выравнивание аллокатора (см. практикум 1.5).

Пример: 10 млн ключей, ключ 20 байт, значение 1 КБ → (20+1024+70) × 10М ≈ 11.2 ГБ. Значит инстанс с 16 ГБ RAM — впритык с учётом фрагментации и fork-COW (модуль 4). Правило: держите used_memory ≤ 70–80% RAM хоста.

Проверяйте модель замером на выборке: MEMORY USAGE key на сотне реальных ключей разных типов, экстраполяция — точнее любой формулы.

Шаг 2. Целевые метрики

INFO stats
keyspace_hits:1250000
keyspace_misses:250000    # hit rate = 1250/(1250+250) = 83.3%
evicted_keys:0            # должно быть 0 или стабильно мало
МетрикаЦельЕсли хуже
Hit rate85–95%+пересмотреть TTL/гранулярность/префиксы
evicted_keys~0мало памяти или кешируется хвост
Latency p99 GET< 1–2 мссмотреть slowlog/сеть/джиттеры (модуль 6)
used_memory/maxmemory< 85%расширять или чистить

Hit rate ниже плана почти всегда означает одну из трёх причин: слишком короткий TTL, слишком широкая гранулярность (кешируем то, что редко читается) или трафик по «хвосту» данных, который лучше не кешировать вовсе.

Шаг 3. TTL-стратегия

  • Базовый TTL = максимум приемлемой несвежести, а не «какое-то число»;
  • Разные классы данных — разные TTL: справочники — часы, профили — минуты, персонализированное — секунды;
  • Джиттер (урок 2.3) на всём;
  • Событийная инвалидация сокращает нужный TTL: если обновления данных доходят до кеша событием (Streams, модуль 3), TTL становится страховкой, а не основным механизмом.

Шаг 4. Инвалидация: три стратегии

СтратегияМеханикаСложностьКогда
TTL-onlyждём истеченияминимальнаянесвежесть терпима
Прямая (по записи)update → DELETE известных ключейнизкая, если список ключей известен1 запись → 1–N ключей
ТегированнаяSET тег → список зависимых ключей; событие → DELETE по сетусредняяобъект порождает много ключей (каталог, страницы)
# Тегированная инвалидация: пересчёт каталога затрагивает N страниц
SADD tag:catalog:42 cache:product:42 page:cat:42:1 page:search:q42
# при изменении каталога 42:
DEL cache:product:42 page:cat:42:1 page:search:q42   # + DEL tag:catalog:42

Разборный пример: кеш карточки товара под 50k RPS

Вводные: 2 млн SKU, топ-10% дают 80% трафика, чтение из БД — 40 мс p99, допустимая несвежесть цены — 60 секунд.

  • Формат: hash product:{sku} (поля: title, price, stock…) — обновление цены без перезаписи всего; либо JSON-строка, если читается всегда целиком;
  • Память: ~300 байт/ключ × 2 млн ≈ 0.7 ГБ — влезает в 4 ГБ с запасом;
  • Политика: allkeys-lfu, maxmemory 3 ГБ;
  • TTL: 5 минут + джиттер; цена — отдельный ключ price:{sku} с TTL 60с (у цены свой ритм изменений);
  • Инвалидация: сервис каталога публикует событие «sku изменён» в Streams; инвалидатор читает и DELETE'ит ключи (tag-сет не нужен — зависимость прямая);
  • Stampede: мьютекс на пересчёт + прогреватель для топ-1000 SKU;
  • Hot keys: топ-карточки (распродажа) — локальный микро-кеш 2с в подах;
  • Мониторинг: hit rate по префиксам (через счётчики в приложении), p99 GET, evicted_keys=0, длина очереди инвалида.
💡 Обратите внимание на декомпозицию: цена и карточка в разных ключах потому, что у них разные требования к свежести. Разный TTL на разные поля — типичный приём, экономящий и память, и инвалидации.

Практика

  1. Посчитайте модель памяти для вашего реального домена: N объектов × размер × накладные — и проверьте замером MEMORY USAGE на выборке.
  2. Реализуйте тегированную инвалидацию (теги в сетах) и проверьте атомарность: пусть тег обновляется «посреди» добавления страницы — какой race видите и как смягчить (подсказка: хранить tag-версию в ключе страницы).
  3. Соберите «карточку товара» из примера: два ключа, разные TTL, событие-инвалидатор через Streams; нагрузите и снимите hit rate.
Мини-квиз

1. Почему used_memory должен быть ≤ 70–80% RAM? — Запас на фрагментацию, пики, бэкграунд-форк COW при BGSAVE.

2. Hit rate 70%: что делать первым делом? — Посмотреть распределение miss по префиксам: обычно видно, какой класс данных не должен был кешироваться.

3. Зачем разделять ключи по свежести? — Разные TTL и разные инвалидации без перезаписи всей сущности.