Перейти к содержанию

B04 · Кэширование#

Самый дешёвый способ ускорить чтение — и самый частый источник ответов «данные не обновились». Кэш вводит вторую копию данных, а вместе с ней вопрос согласованности, которого до этого не было.

Карточки идут от уровней кэша к стратегиям чтения и записи, инвалидации, патологиям и Redis.

Навигация: CACHE-01 · CACHE-02 · CACHE-03 · CACHE-04 · CACHE-05


CACHE-01 · Уровни кэша: что и где кэшировать#

Шаг W5

TL;DR. Кэш есть на каждом уровне пути запроса. Самый дешёвый — тот, что ближе к пользователю; самый управляемый — тот, что ближе к данным.

Проблема. «Добавим кэш» — не одно решение, а выбор из шести мест. У каждого своя цена, свой радиус действия и свои проблемы с устареванием. Кэш не на своём месте либо не даёт эффекта, либо создаёт неуправляемое устаревание у миллионов клиентов.

Как работает — уровни от клиента к данным.

Уровень Что кэширует Управляемость Радиус
Браузер / приложение клиента Статику, ответы API по заголовкам Низкая: нельзя инвалидировать удалённо Один пользователь
CDN / edge (EDGE-05) Статику, публичные ответы Средняя: есть purge (принудительная очистка) Регион
Reverse proxy (обратный прокси: nginx, Varnish) Ответы целиком Высокая Дата-центр
Кэш в памяти процесса Горячие объекты, конфигурацию Полная, но у каждого экземпляра свой Один экземпляр
Распределённый кэш (Redis, Memcached) Объекты, сессии, результаты запросов Полная, общая для всех Кластер
Буферный пул БД Страницы данных и индексов Автоматическая Одна БД

Что кэшировать. Хорошие кандидаты: данные, которые читают гораздо чаще, чем меняют; результаты дорогих вычислений и агрегаций; ответы внешних сервисов. Плохие: данные, читаемые однократно; результаты, уникальные для каждого запроса; всё, где устаревание недопустимо.

Правило порядка. Сначала найти узкое место (W1, W5), потом кэшировать. Кэш поверх неоптимального запроса прячет проблему до первого промаха.

Trade-offs и режимы отказа. Каждый уровень добавляет копию со своим временем жизни. Одно и то же изменение доходит до пользователя через разные уровни за разное время, и со стороны это выглядит как хаотичное поведение. Кэш в памяти процесса при N экземплярах даёт N разных состояний: для конфигурации это приемлемо, для данных пользователя — опасно. Кэш с неправильной инвалидацией опаснее, чем отсутствие кэша.

Что мерить.

  • hit ratio (долю запросов, которые обслужил кэш) по каждому уровню: при низком hit ratio кэш бесполезен, но по-прежнему занимает память и вносит рассогласование;
  • задержку с попаданием и без;
  • объём и вытеснение;
  • возраст отдаваемых данных.

Связано: CACHE-02 стратегии · CACHE-03 инвалидация · EDGE-05 CDN · NET-06 HTTP-кэширование

Источники: AWS — Caching Best Practices · MDN — HTTP caching · ByteByteGo — Cache Systems Every Developer Should Know


CACHE-02 · Стратегии чтения и записи#

Шаг W5

TL;DR. Стратегия определяет, кто загружает и записывает данные — приложение или кэш. От неё зависит, что произойдёт при падении кэша и какие данные потеряются.

Проблема. Кэш и БД — два хранилища, и обновляются они не атомарно. Порядок операций определяет, какие рассогласования возможны и что теряется при отказе каждой стороны. Стратегия, выбранная наугад, даёт ошибки, которые воспроизводятся раз в тысячу запросов.

Стратегии чтения.

  • Cache-aside (lazy loading, ленивая загрузка) — приложение проверяет кэш, при промахе читает БД и кладёт результат в кэш. Самая распространённая стратегия: в кэше лежит только то, что действительно запрашивали, а падение кэша нагружает БД, но не теряет данные. При записи нужна явная инвалидация.
  • Read-through — приложение читает только через кэш, а при промахе кэш сам загружает данные. Логика загрузки собрана в одном месте, но её должна поддерживать библиотека или сервис кэша.
  • Refresh-ahead — популярные ключи обновляются заранее, до истечения TTL (time to live — срока жизни ключа). Это снижает задержку и защищает от одновременного истечения, но тратит ресурсы на данные, которые могут и не понадобиться.

Стратегии записи.

  • Write-through (сквозная запись) — запись идёт в кэш и в БД синхронно. Кэш актуален, но каждая запись дольше на одну операцию.
  • Write-behind (write-back, отложенная запись) — запись идёт в кэш, а в БД — позже, асинхронно и пачками. Самая быстрая запись — и единственная стратегия, при которой падение кэша означает потерю данных. Годится для счётчиков и телеметрии, не годится для денег.
  • Write-around — запись идёт мимо кэша, прямо в БД; кэш наполняется только при чтении. Подходит для данных, которые пишут часто, а читают редко.

Типичная комбинация. Cache-aside для чтения плюс инвалидация (удаление ключа) при записи. Строгой согласованности эта пара не даёт, зато ведёт себя предсказуемо и переживает падение кэша.

Альтернативы

Комбинация Когда лучше Цена
Cache-aside + delete on write Умолчание для большинства систем Гонки при параллельных чтении и записи (CACHE-03)
Write-through + read-through Нужна максимальная свежесть кэша Медленнее запись, кэш держит всё
Write-behind Огромный поток записи, потери допустимы Потеря данных при отказе кэша
Write-around Пишут много, читают мало Первое чтение — промах

Trade-offs и режимы отказа. У cache-aside есть окно для гонки. Процесс А прочитал из БД старое значение. Тем временем процесс Б обновил БД и удалил ключ. Затем А кладёт старое значение в кэш — после удаления, и оно живёт там до истечения TTL. Окно сужают короткий TTL, версионирование ключа или повторное удаление с задержкой. Write-behind без надёжно сохранённой очереди изменений при аварии гарантированно теряет данные.

Что мерить.

  • hit ratio (долю попаданий);
  • долю устаревших ответов — выборочным сравнением с БД;
  • задержку записи с кэшем и без;
  • объём и возраст неподтверждённых записей при write-behind.

Связано: CACHE-03 инвалидация · CACHE-04 патологии · MSG-07 outbox

Источники: AWS — Caching patterns · Scaling Memcache at Facebook (NSDI 2013) · ByteByteGo — Cache Systems Every Developer Should Know


CACHE-03 · Инвалидация и согласованность с БД#

Шаг W6

TL;DR. Инвалидировать кэш можно тремя способами: по времени, по событию и по версии ключа. Первый прост и переживает любые сбои, третий надёжен и исключает гонки, второй — посередине.

Проблема. Данные изменились в БД, а в кэше лежит старая копия. Простое «обновим кэш после записи» проигрывает гонку: два процесса могут записать значения в кэш в порядке, обратном порядку записи в БД. Простое «удалим ключ после записи» оставляет окно: параллельное чтение успевает вернуть в кэш устаревшее значение (CACHE-02).

Как работает.

По времени (TTL, time to live). Ключ живёт заданный срок. Способ простой, переживает любые сбои и не требует координации — поэтому его применяют чаще всего. Цена: данные устаревают на величину TTL, а массовое одновременное истечение вызывает всплеск (CACHE-04). На практике: короткий TTL для изменчивых данных плюс небольшой jitter (случайный разброс) срока жизни.

По событию. При изменении данных публикуется событие, а потребитель удаляет или обновляет ключи. Данные свежее, но появляется зависимость от доставки: потерянное событие оставляет кэш устаревшим бессрочно. Поэтому TTL всё равно оставляют как страховку. Надёжный источник событий — журнал изменений БД (CDC, change data capture — захват изменений данных; MSG-07). Вызов из кода приложения ненадёжен: при откате транзакции он может не выполниться.

По версии ключа. Ключ включает версию сущности или метку изменения: user:42:v17. Обновление данных меняет версию, и новый запрос не находит старый ключ: инвалидация не нужна вовсе, гонок нет. Старые ключи вытесняются сами. Цена: текущую версию нужно где-то дёшево читать — обычно она уже есть в строке БД или в отдельном счётчике.

Как выбирать.

  • устаревание на секунды допустимо → TTL;
  • свежесть нужна почти сразу, данные важные → CDC-события плюс TTL как страховка;
  • данные часто читают и редко меняют, гонки недопустимы → версионирование ключа.

Trade-offs и режимы отказа. Инвалидация из кода приложения происходит вне транзакции. Если транзакция откатилась, а кэш уже очищен, — не страшно. Если кэш уже обновлён новым значением — страшно: в кэше данные, которых нет в БД. Массовая инвалидация по событию (изменился справочник — сбросили тысячи ключей) вызывает лавину промахов. Если кэш инвалидируется только по событиям, то при сбое конвейера он отдаёт устаревшие данные неограниченно долго.

Что мерить.

  • возраст отдаваемых данных;
  • долю расхождений при выборочной сверке с БД;
  • всплески промахов после инвалидации;
  • отставание конвейера событий.

Связано: CACHE-02 стратегии · CACHE-04 патологии · MSG-07 CDC и outbox · DIST-03 модели согласованности

Источники: Scaling Memcache at Facebook (NSDI 2013) · Redis — Client-side caching · ByteByteGo — Caching Pitfalls Every Developer Should Know


CACHE-04 · Патологии: stampede, hot key, negative caching#

Шаг W7

TL;DR. Кэш редко деградирует плавно: он либо помогает, либо резко перестаёт помогать — и роняет БД, которая давно не рассчитана на полную нагрузку.

Проблема. БД под кэшем видит 5 % запросов. Если кэш исчезает или массово промахивается, на БД приходит нагрузка в 20 раз больше обычной. Она не справляется, ответы замедляются, клиенты повторяют запросы — и система входит в устойчивое состояние отказа. Из него она не выходит даже после восстановления кэша.

Патологии и лечение.

Cache stampede (dogpile). Популярный ключ истёк — тысяча параллельных запросов одновременно идёт в БД за одним и тем же значением.

  • Блокировка на пересчёт: первый запрос берёт короткую блокировку и пересчитывает значение, остальные ждут или получают устаревшее.
  • Request coalescing (объединение запросов) на уровне процесса: одна загрузка на все ожидающие корутины.
  • Probabilistic early expiration (вероятностное раннее обновление): чем ближе истечение TTL, тем выше шанс, что очередной запрос обновит значение заранее. Нагрузка размазывается во времени.
  • stale-while-revalidate: отдать устаревшее и обновить в фоне.

Массовое одновременное истечение. Ключи, созданные в один момент (после выкатки, после прогрева), истекают одновременно. Лечится добавлением jitter (случайного разброса) к TTL (±10–20 %).

Hot key (горячий ключ). Один ключ получает непропорциональную долю запросов (звезда в соцсети, товар на распродаже). Шардирование не помогает — ключ живёт на одном узле. Что помогает:

  • локальная копия в памяти процесса на короткий срок;
  • репликация hot key на несколько узлов и чтение со случайного;
  • разбиение значения на N подключей, если это счётчик или список.

Запросы за несуществующим. Ответ «данных нет» по умолчанию не кэшируется, и такие запросы каждый раз доходят до БД. Это обычный вектор перегрузки, в том числе намеренной. Лечится negative caching (негативным кэшированием) — кэшированием самого факта отсутствия с коротким TTL — и фильтром Блума перед хранилищем.

Холодный старт. После перезапуска кэш пуст, и полный трафик приходит на БД. Лечится прогревом ключевых данных перед вводом в ротацию (REL-05) и постепенным вводом трафика.

Сводка

Патология Признак Лечение
Cache stampede Всплеск обращений к источнику в момент истечения ключа Блокировка на пересчёт, request coalescing, probabilistic early expiration
Массовое одновременное истечение Периодические пики нагрузки с равным интервалом Jitter срока жизни ±10–20 %
Hot key Перекос обращений на один узел кэша Локальная копия, репликация ключа, разбиение значения
Запросы за несуществующим Промахи без роста попаданий Negative caching, фильтр Блума перед хранилищем
Холодный старт Всплеск на источнике после перезапуска Прогрев до ввода в ротацию, постепенный ввод трафика

Trade-offs и режимы отказа. Блокировка на пересчёт превращает популярный ключ в точку сериализации. Отдача устаревших данных при сбое (stale-if-error) повышает доступность ценой корректности; включать её или нет, решают по конкретным данным, а не по умолчанию. Локальные копии hot key возвращают проблему N разных состояний (CACHE-01).

Что мерить.

  • hit ratio (долю попаданий) и его провалы;
  • распределение обращений по ключам (топ-N);
  • число одновременных пересчётов одного ключа;
  • нагрузку на БД в момент падения hit ratio;
  • долю ответов, отданных устаревшими.

Связано: CACHE-03 инвалидация · REL-01 таймауты и повторы · EDGE-06 rate limiting · DATA-11 consistent hashing

Источники: Optimal Probabilistic Cache Stampede Prevention (VLDB 2015) · Google SRE Book, гл. 22 «Addressing Cascading Failures» · RFC 5861 — stale-while-revalidate · ByteByteGo — Caching Pitfalls Every Developer Should Know


CACHE-05 · Redis#

Шаг W5

TL;DR. Redis — хранилище структур данных в памяти, которое выполняет команды в одном потоке. Он быстр не вопреки однопоточности, а во многом благодаря ей.

Проблема. Кэш «ключ → строка» решает не всё: нужны счётчики, очереди, множества, рейтинги, rate limiting (ограничение частоты), распределённые примитивы. Реализовать это поверх обычного хранилища «ключ — значение» (KV) можно только с атомарностью, которой там нет.

Как работает.

  • Структуры данных как примитивы: строки, хэши, списки, множества, упорядоченные множества (sorted set), битовые карты, HyperLogLog (приблизительный подсчёт уникальных), геоиндексы, потоки (Streams). Операции над ними атомарны.
  • Однопоточное выполнение команд. Нет блокировок, нет переключений контекста, нет гонок — каждая команда атомарна по построению. Узкое место обычно не CPU, а сеть и память. Поэтому современные версии выносят сетевой ввод-вывод в отдельные потоки, но сами команды по-прежнему выполняют последовательно.
  • Персистентность: RDB (периодический снимок) и AOF (журнал команд). Даже с AOF и fsync everysec можно потерять последнюю секунду записей: по умолчанию Redis не даёт durability — сохранности записанного — на уровне БД.
  • Политики вытеснения при исчерпании памяти: allkeys-lru, volatile-ttl, noeviction и другие. Для кэша обычно LRU/LFU (вытеснение давно или редко используемых ключей) по всем ключам; для очередей и данных, которые нельзя терять, — noeviction и мониторинг.
  • Масштабирование: реплики для чтения; Redis Cluster шардирует ключи по 16 384 слотам; failover — переключение на реплику при отказе — через Sentinel или Cluster.

Типовые сценарии.

  • кэш;
  • сессии;
  • счётчики и rate limiting (EDGE-06);
  • рейтинги на sorted set;
  • очереди и Streams;
  • lease (аренда) и блокировки — с оговорками из DIST-07;
  • pub/sub (издатель — подписчик) для уведомлений внутри системы.

Когда применять. Когда нужны скорость и атомарные операции над структурами. Когда нет: как единственное хранилище важных данных; для больших объёмов холодных данных (память дорога); для сложных запросов и аналитики.

Альтернативы

Вариант Когда лучше Цена
Redis Нужны структуры и атомарность Память, ограниченная durability
Memcached Только простой кэш, максимум простоты Нет структур, нет персистентности
Кэш в памяти процесса Минимальная задержка, мало данных N состояний, нет общего доступа
БД + индекс Данные и так помещаются, нагрузка умеренная Медленнее на порядки

Trade-offs и режимы отказа. Одна медленная команда блокирует всех: KEYS *, большой SORT, FLUSHALL на большом наборе — типовой способ уронить прод. Большие значения и большие коллекции («big keys») дают всплески задержки. Синхронный fsync на каждую запись резко снижает производительность, а без него теряются данные. Redis Cluster не выполняет операции над ключами из разных слотов — это приходится учитывать в модели данных.

Что мерить.

  • задержку команд: p99 и медленные команды через slowlog;
  • hit ratio (долю попаданий);
  • использование памяти и фрагментацию;
  • число вытеснений;
  • лаг репликации;
  • число подключений.

Связано: CACHE-01 уровни кэша · CACHE-04 патологии · EDGE-06 rate limiting · DIST-07 распределённые блокировки

Источники: Redis — Documentation · Redis — Key eviction · Redis — Persistence · ByteByteGo — Top 5 Redis Use Cases · ByteByteGo — System Design: Why is single-threaded Redis so fast? · ByteByteGo — What Is Redis Really About? Why Is It So Popular?