Стратегии кеширования
1) Зачем кешировать и где это делать
Кеш — это слой быстрой памяти, уменьшающий латентность и нагрузку на дорогие ресурсы (CPU/БД/внешние API). Важные цели:- Скорость (p95/p99 ниже), стоимость (меньше egress/CPU), устойчивость (меньше зависимостей под пиком).
- Сглаживание пиков и изоляция от “шумных соседей”.
1. Клиент (браузер/мобильный) — HTTP-кеш, IndexedDB, local storage.
2. Edge/CDN — POP-узлы ближе к пользователю, кэшируют статику и часть API.
3. L7-шлюз/Reverse-proxy — Nginx/Envoy/Varnish (микрокеш, SWR).
4. Сервисный кэш — Redis/Memcached внутри кластера.
5. Внутрипроцессный — in-memory (Caffeine/Guava/LRU-map).
6. Кэш в БД — материальные представления, вторичные индексы.
Правило: кешируйте как можно ближе к потребителю, но истину храните единожды.
2) Паттерны кеширования
2.1 Cache-aside (“lazy loading”)
Приложение сначала читает из кэша; при промахе — из источника, затем пишет в кэш.
Плюсы: простота, контроль. Минусы: холодные старты, окна рассогласования.
2.2 Read-through
Чтение всегда через кэш, который сам ходит к источнику при промахе (библиотека/проксирующий слой).
Удобно централизовать политики TTL/серилизацию.
2.3 Write-through / Write-back (write-behind)
Write-through: запись в кэш и источник синхронно → согласованность выше, латентность выше.
Write-back: запись в кэш, асинхронная флеш-запись в источник → быстро, но риски потерь и конфликтов.
2.4 Refresh-ahead (proactive)
Предсказывает “скоро истечет TTL” и обновляет ключ в фоне, предотвращая stampede.
2.5 Negative caching
Кеширование “нет данных/404/пусто” на короткий TTL снижает нагрузку на источник.
2.6 Micro-caching
Очень короткие TTL (0.5–5 с) на L7 для “почти динамики” (списки, главная) — резко снижает хвосты.
3) HTTP-кеш: заголовки и контроль
3.1 Базовые заголовки
`Cache-Control`: `max-age`, `s-maxage` (для shared кэшей), `public/private`, `no-store`, `stale-while-revalidate`, `stale-if-error`.
Валидаторы: `ETag` (хэш содержимого), `Last-Modified`.
Запросы с условиями: `If-None-Match`, `If-Modified-Since` → 304 Not Modified.
3.2 Vary и ключи
`Vary: Accept-Encoding, Authorization, Cookie, Accept-Language` — формирует разные варианты кэша. Минимизируйте `Vary`, чтобы не “взорвать” кардинальность.
3.3 Пример HTTP-ответа
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60
ETag: "a1b2c3"
Vary: Accept-Encoding
4) Проектирование ключей и TTL
4.1 Ключи
Структурируйте: `tenant:user:{id}:profile:v3` (включайте версию схемы).
Избегайте PII в ключе.
Для коллекций — ключ + параметры запроса (нормализованные и отсортированные).
4.2 TTL и согласованность
Короткий TTL снижает рассогласование, но увеличивает промахи.
Для критичных данных — валидаторы (`ETag`) и SWR (stale-while-revalidate).
Для редко меняющихся — долгий TTL + “бомбочки” инвалидации.
4.3 Версионирование/бастинг
При несовместимых изменениях — меняйте префикс/версию ключа (`v2 → v3`).
Для статических ресурсов — content hash в имени файла.
5) Инвалидация: стратегии и практики
5.1 Прямое удаление
`DEL key` / `PURGE` на прокси. Опасность: гонки между удалением и многократными читателями.
5.2 Тэги/Surrogate keys
Связывайте документ с набором тегов (категория/автор). Инвалидация — по тегу.
В Varnish/Edge — `Surrogate-Key: article:42 tag:author:7` + `BAN tag:author:7`.
5.3 Event-driven инвалидация
Pub/Sub (Kafka/NATS): при изменении источника — публикуем событие “invalidate”.
Консьюмеры кэша слушают и удаляют/обновляют ключи.
5.4 Двуфазная
Сначала помечаем ключ устаревшим (soft TTL), обслуживаем stale, в фоне обновляем и атомарно заменяем.
6) Борьба со stampede/dogpile и горячими ключами
6.1 Request coalescing (singleflight)
Один продюсер обновляет ключ, остальные ждут результат (мьютекс/лейбл “обновляется”).
6.2 Jitter к TTL
Добавляйте случайность (±10–20%) к TTL, чтобы избежать синхронного протухания.
6.3 Soft-TTL + hard-TTL
До soft-TTL обслуживаем из кэша, параллельно триггерим refresh; по hard-TTL — считаем промах.
6.4 Горячие ключи
Локальные кэши поверх общих (two-tier).
Репликация горячего ключа в несколько шардов и рандомный выбор (только для read-only).
Rate limit на обновление конкретного ключа.
6.5 Пример Redis + Lua (singleflight-эскиз)
lua
-- SETNX lock with TTL to avoid deadlocks local ok = redis. call("SET", KEYS[1], "1", "NX", "EX", ARGV[1])
if ok then return "LOCKED"
else return "WAIT"
end
7) Политики вытеснения и прием в кэш
7.1 Eviction
LRU: просто и хорошо для локальности.
LFU: лучше при “долгоживущих” горячих ключах.
ARC/TinyLFU: баланс recency/frequency.
7.2 Admission (впуск)
Не пускайте гигантские редкие объекты (TinyLFU/Bloom-фильтры).
Компрессия больших значений (LZ4/Zstd) на границе “размер/латентность”.
8) Шардирование и топологии
8.1 Consistent hashing
Стабильно распределяет ключи по нодам, уменьшает перемещения при росте/сжатии кластера.
8.2 Топологии Redis/Memcached
Redis Cluster (слоты/шарды), Sentinel (фейловер), репликация read-only.
Memcached — клиент-сайд шардинг (ketama hashing), без репликации на уровне сервера.
8.3 Локальный + распределенный
Каскад: in-proc (микро-TTL/LRU) → Redis (TTL длиннее) → источник.
Будьте осторожны с двоеточиями TTL и кэш-валидаторами.
9) Edge, CDN и L7-кеш
9.1 Micro-cache на Nginx
nginx proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api:100m inactive=10m;
map $request_method $skip_cache { default 0; POST 1; PUT 1; DELETE 1; }
server {
location /api/list {
if ($skip_cache) { add_header Cache-Control "no-store"; }
proxy_cache api;
proxy_cache_valid 200 2s; # micro-cache proxy_cache_use_stale error timeout updating;
proxy_cache_background_update on; # SWR add_header X-Cache $upstream_cache_status;
proxy_pass http://upstream;
}
}
9.2 Envoy (SWR и условия)
yaml http_filters:
- name: envoy. filters. http. cache typed_config:
"@type": type. googleapis. com/envoy. extensions. filters. http. cache. v3. CacheConfig typed_config:
"@type": type. googleapis. com/envoy. extensions. http. cache. file_system_http_cache. v3. FileSystemHttpCacheConfig cache_path: "/var/cache/envoy"
9.3 Varnish (Surrogate keys)
Используйте `Surrogate-Key` и `ban` по тегам для пакетной инвалидации.
10) Кеш и согласованность данных
10.1 Read-your-writes
Для пользовательских профилей/корзины обеспечьте либо короткие TTL, либо запись сквозь кэш (write-through), либо маркировку клиента (bypass на N секунд после записи).
10.2 Eventual vs Strong
Для рекомендательных/аналитических — eventual + длинный TTL.
Для денег/статусов заказов — короткий TTL, валидация, иногда без кэша на критических путях.
10.3 Инварианты
Не кешируйте поля, влияющие на безопасность/ACL, без строгих TTL и повторной проверки.
11) Наблюдаемость, SLO и управление
11.1 Метрики
hit_ratio (общий и per-route), byte_hit_ratio, miss_rate.
stampede_prevented_total, refresh_ahead_total, ban/purge_total.
Латентность: p50/p95/p99 из кэша vs из источника.
hot_keys_topN и их QPS/байты.
11.2 Логи и трассировки
Логируйте `X-Cache: HIT/MISS/STALE/UPDATING`.
В трейсах отмечайте источник ответа (`cache=true`, `tier=edge|service|local`).
11.3 SLO-подход
Пример: “для API /catalog p99 ≤ 250 мс, cache hit ≥ 85%, stampede ≤ 0.1% запросов”.
11.4 Runbooks
“Промахи растут” → проверить TTL, прогрев/инвалидации, hot-keys, размер кэша и политику принятия.
12) Безопасность и мульти-тенантность
Встраивайте tenant-id в ключи (и в `Vary` при HTTP).
Не кешируйте приватные ответы как `public`.
Шифруйте кэш с чувствительными данными или храните только не-PII/ID.
13) Типовые рецепты
13.1 Каталог/лента (почти динамика)
Edge-микрокеш 1–3 с + SWR, внутри — Redis на 15–60 с, инвалидация по событиям обновления.
13.2 Профиль пользователя
Cache-aside с TTL 30–120 с, bypass 5–10 с после обновления профиля (cookie/хедер), либо write-through.
13.3 Валюто-курсы/справочники
Длинные TTL (минуты-часы) + целевая инвалидация при публикации новых данных; `ETag` для условных GET.
13.4 Поисковая выдача
Edge-микрокеш 1–2 с, внутри — refresh-ahead и coalescing, нормализация query-параметров в ключе.
14) Анти-паттерны
Кэш без инвалидации: надежда только на TTL → долгие окна неактуальности.
Гигантский `Vary`: “взрыв” вариантов → низкий hit-rate.
Единый кэш для prod/experiments → загрязнение.
Нет защиты от stampede → пики на источнике при истечении TTL.
Кэш денег/прав/ACL без строгих гарантий.
Компрессия “всего подряд” — лишние CPU, ухудшение p99 на мелких объектах.
15) Чек-лист внедрения
- Определите уровни кеша и их цели (edge/service/local).
- Спроектируйте ключи (версионирование, tenant, нормализация параметров).
- Выберите паттерн (cache-aside/read-through/refresh-ahead).
- Настройте TTL/soft-TTL/jitter, включите SWR.
- Реализуйте coalescing/singleflight, защиту от stampede.
- Организуйте инвалидацию (события, теги, purge/ban).
- Введите метрики hit-ratio/латентности и дашборды `X-Cache`.
- Проведите нагрузочные тесты с горячими ключами.
- Пропишите SLO и runbooks.
- Проверьте безопасность/tenant-изоляцию и `Vary`.
16) FAQ
Q: Что выбрать — cache-aside или read-through?
A: Для простых сервисов — cache-aside. Нужна централизация и единая политика — read-through.
Q: Как понять оптимальный TTL?
A: Отталкивайтесь от допустимой устарелости, частоты обновлений и целевого hit-rate; добавьте jitter и наблюдайте p95/p99/стоимость.
Q: Когда уместен write-back?
A: Для высоконагруженных потоков, где приемлема eventual-консистентность и есть надежная очередь/лог для “дописывания”.
Q: Можно ли кешировать авторизованные ответы?
A: Да, но помечайте `private` и/или включайте tenant/user в ключ/`Vary`. Для truly-private — клиентский кэш.
Q: Как прогревать кэш?
A: Списки популярных ключей, бэкграунд-вормер, реплай с логов, прогрев перед релизом/пиком (черная пятница и т.п.).
17) Итоги
Эффективное кеширование — это дизайн ключей + разумные TTL + грамотно выбранный паттерн, усиленные инвалидацией по событиям, SWR/refresh-ahead и защитой от stampede. Разнесите кэш по уровням (клиент/edge/сервис), добавьте наблюдаемость и SLO — и получите стабильные хвосты латентности, предсказуемую стоимость и устойчивость к пиковым нагрузкам.