Стратегії кешування
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 і спостерігайте р95/р99/вартість.
Q: Коли доречний write-back?
A: Для високонавантажених потоків, де прийнятна eventual-консистентність і є надійна черга/лог для «дописування».
Q: Чи можна кешувати авторизовані відповіді?
A: Так, але позначайте'private'і/або включайте tenant/user в ключ/' Vary'. Для truly-private - клієнтський кеш.
Q: Як прогрівати кеш?
A: Списки популярних ключів, бекграунд-вормер, реплай з логів, прогрів перед релізом/піком (чорна п'ятниця тощо).
17) Підсумки
Ефективне кешування - це дизайн ключів + розумні TTL + грамотно обраний патерн, посилені інвалідацією по подіям, SWR/refresh-ahead і захистом від stampede. Рознесіть кеш по рівнях (клієнт/edge/сервіс), додайте спостережуваність і SLO - і отримаєте стабільні хвости латентності, передбачувану вартість і стійкість до пікових навантажень.