Кэширование – одна из важнейших практик в проектировании современных высоконагруженных IT-систем.
Что такое кэширование и для чего оно нужно?
Представим стартап с небольшим новостным сайтом. Система пока маленькая: приложение развернуто на одном сервере, есть одна база данных. Помимо новостного сервиса, есть несколько сторонних backend-ов (сервис погоды, сервис подсчета лайков к новостям).
Со временем приложение растет, количество пользователей увеличивается. Для справки с возрастающей нагрузкой, мы масштабируемся горизонтально (развертываем дополнительные экземпляры приложения и баз данных). Такая архитектура хорошо работает, пока трафик растет равномерно. Однако, при выходе эксклюзивной новости, все пользователи одновременно открывают страницу, комментируют и ставят лайки. Система не справляется: фронтенд шлет много запросов, страницы не открываются, сервисы не успевают отвечать. Один из паттернов, позволяющих эффективно работать в таких случаях – кэширование (caching).
Кэширование – механизм оптимизации производительности IT-систем. Суть заключается в добавлении буфера между приложением и базой данных (или приложением и сервисом) для хранения часто используемых данных. Ключевая идея – получать данные намного быстрее, чем из основного источника. Кэш обычно гораздо меньше основного источника, поэтому данные в кэше актуальны в пределах ограниченного периода времени.
Подходы к кэшированию
Кэширование – обширная тема. Рассмотрим основные подходы к его реализации, классифицируя их по нескольким признакам:
Расположение кэша:
- Браузерный кэш: хранится на уровне фронтенда, на устройстве пользователя.
- Прокси-кеш: размещается между фронтендом и приложением или между приложением и backend-ом. Фильтрует запросы, часть направляет по исходному пути, на часть отвечает кэшированными данными.
- Кэш в базе данных: реализуется с помощью плагинов или in-memory индексов некоторых СУБД.
- Кэш в памяти приложения: сохраняется в рамках выполняемого процесса и одного сервера. При перезапуске приложения кэш теряется. На нескольких серверах – на каждом свой кэш.
- Распределённый кэш: общий для нескольких экземпляров приложения. Часто реализуется внешним сервисом (Redis, Memcached) или библиотекой внутри приложения.
Алгоритм вытеснения:
- LRU (Least Recently Used): вытесняются давно неиспользуемые записи.
- LFU (Least Frequently Used): вытесняются редко используемые записи.
- MRU (Most Recently Used): вытесняются недавно использованные записи.
Порядок взаимодействия:
- Кэширование на стороне приложения: приложение обращается к кэшу. Если данные есть – используются, иначе – запрос к основному хранилищу.
- Сквозная запись (Write-through): данные записываются одновременно в кэш и в основное хранилище.
- Запись через кэш (Write-back/Write-behind): сначала обновляется кэш, потом основное хранилище.
- Сквозное чтение (Read-through): похоже на кэширование на стороне приложения, но координация запросов к базе происходит на стороне кэша.
- Упреждающее чтение (Read-ahead): данные помещаются в кэш на основе предположения о будущих запросах.
Пример приложения с кэшем
Рассмотрим проектирование кэша на уровне приложения на примере новостного сайта:
- Кэш в памяти приложения: простейшее решение. При запросе приложение сначала обращается к кэшу, затем – к базе данных и сохраняет данные в кэш.
- Распределённый кэш (например, Redis): новости для всех пользователей одинаковы, поэтому одна нода приложения записывает новости в кэш, другие – считывают.
Хранение данных в кэше
Каждая запись в кэше содержит три поля:
- Ключ: обычно кортеж параметров запроса или базы данных.
- Значение: сами данные (например, содержимое новости).
- Время жизни (Time to Live): срок годности записи.
Оценка эффективности кэширования
Для оценки эффективности используются метрики:
- Hit Rate (процент попадания): количество попаданий в кэш / (количество попаданий + количество промахов).
- Сравнение количества элементов в кэше с количеством данных в исходном хранилище: помогает выявить некорректный ключ кэширования.
- Размер записи кэша: определяет ситуации, когда запись содержит больше данных, чем нужно.
- Количество вытесняемых записей в секунду (Evictions): высокое значение может указывать на слишком маленький размер кэша или неэффективный ключ.
- Задержка получения данных из кэша: должна быть меньше задержки получения данных из оригинального источника.
- Количество ошибок получения данных из кэша: детектирует сбои в системе кэширования.
Проблемы и решения при реализации кэширования
Кэширование, ускоряя работу приложения, может вызывать проблемы и ухудшать производительность.
Дублирование запросов
Проблема возникает, когда с минимальной разницей во времени поступают одинаковые запросы, а нужных данных нет в кэше. Одинаковые запросы нагружают базу данных. Решение: паттерн Single Flight. Дополнительная функция по набору параметров дедуплицирует запросы, ожидая ответа на первый запрос.
Недоступность данных
Проблема возникает, когда свежих данных нет в кэше, а основной источник недоступен. Решение: паттерн Fallback cache. Дополнительный кэш хранит последний успешный ответ от основного источника дольше, чем основной кэш. Данные из него берутся только при недоступности основного кэша и основного источника. Можно реализовать, храня два значения TTL: время жизни данных и время жизни как fallback.
Тайм-ауты
Проблема возникает, когда кэш не дождался ответа от backend-а из-за тайм-аута. Решение: увеличение тайм-аута между кэшем и backend-ом, добавление тайм-аута между приложением и кэшем (маленького, чтобы приложение не ждало долго).
Объединение паттернов
Все рассмотренные паттерны можно объединить в один механизм:
- Приложение обращается к кэшу.
- Если данные есть – отображаются.
- Если запрос в полёте – ждём ответа (Single Flight).
- При получении данных из backend-а – отдаём данные приложению.
- При ошибке – пытаемся получить из fallback-кэша или откатываемся к предыдущей версии (Fallback cache).
- Если данных нет – отправляем запрос в backend.
- При получении данных – сохраняем в кэш и отдаём приложению.
- При ошибке – пытаемся получить из fallback-кэша или откатываемся к предыдущей версии (Fallback cache).
Кэширование – важная практика в проектировании высоконагруженных IT-систем. Оно увеличивает производительность и позволяет выдерживать пиковые нагрузки. Существует множество подходов к кэшированию, выбор зависит от задачи, пользовательского поведения и структуры данных. Оценка эффективности осуществляется через мониторинг метрик. Кэширование может принести не только пользу, но и проблемы, для решения которых применяются различные паттерны и подходы. Каждую систему кэширования необходимо проектировать с нуля, учитывая особенности пользовательского поведения и структуры данных.