Как устроено кэширование в распределенных системах?

«Как устроено кэширование в распределенных системах?» — вопрос из категории Распределенные системы, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Кэширование в распределенных системах — это критически важный механизм для повышения производительности, доступности и снижения нагрузки на первичные хранилища (БД). Я рассматриваю его на нескольких уровнях.

1. Стратегии записи и обновления кэша:

  • Cache-Aside (Lazy Loading): Наиболее распространенный подход. Приложение сначала проверяет кэш. При промахе (cache miss) — загружает данные из БД, сохраняет в кэш и возвращает. Запись идет напрямую в БД, а затем инвалидируется соответствующая запись в кэше. Простота, но возможна race condition при конкурентном обновлении.
  • Write-Through: Запись происходит синхронно и в кэш, и в БД. Гарантирует консистентность, но увеличивает latency записи.
  • Write-Behind (Write-Back): Запись идет сначала в кэш, а затем асинхронно батчами сбрасывается в БД. Максимальная производительность записи, но риск потери данных при падении кэша.

2. Архитектура распределенного кэша:

  • Client-Side кэширование: Кэш находится в памяти самого приложения (например, HashMap, Guava Cache). Быстро, но память ограничена, и кэш не разделяется между инстансами.
  • Выделенный кэш-сервер (например, Redis, Memcached): Отдельный кластер, к которому обращаются все ноды приложения. Решает проблему разделения состояния. Требует настройки репликации и шардирования.
    • Шардирование: Данные распределяются по нодам кэша (например, по ключу) для горизонтального масштабирования.
    • Репликация: Каждый шард имеет реплики для отказоустойчивости.

3. Ключевые проблемы и их решения:

  • Инвалидация кэша: Как и когда данные в кэше становятся неактуальными? Использую TTL (time-to-live), инвалидацию по событиям (из БД или через брокер сообщений) или комбинацию.
  • Горячие ключи (Hot Keys): Один ключ с огромным количеством запросов может перегрузить один шард кэша. Решение: локальный client-side кэш для таких ключей или их дублирование (добавление случайного суффикса).
  • Проблема «Грозового стада» (Thundering Herd): При истечении TTL у популярного ключа множество запросов одновременно идут в БД. Решение: «ленивое» обновление кэша (один поток обновляет, остальные ждут) или использование механизма «блокировки» в Redis (SETNX).
  • Консистентность: Выбор между сильной и eventual consistency. Часто в кэше приемлема eventual consistency, а гарантии дает первичная БД.

На практике я чаще всего использую Redis в режиме Cache-Aside с разумным TTL и инвалидацией по событиям для критически важных данных.