Это вторая статья в нашей новой серии Технологии сердцем, в которой мы рассматриваем темы, связанные с нашими технологиями. Ознакомьтесь с нашим первым постом, в котором рассказывается о 5 уроках, которые мы извлекли при масштабировании технической архитектуры Glispa.

В Glispa Global Group наши очень сложные запатентованные технологии требуют постоянного и надежного доступа к данным. Наши глобально распределенные серверы DSP, например, должны обрабатывать тысячи запросов в секунду, 24 часа в сутки, отвечая на каждый из них менее чем за 10 мс (стандарт, который мы установили для себя).

Наши серверы DSP используют протокол HTTP и, помимо других данных, должны знать о кампаниях и объявлениях для обработки запросов. Для каждого запроса объявления система должна просматривать тысячи кампаний. Фактический объем памяти этого набора данных довольно мал, всего пара гигабайт, и меняется не очень часто. Это означает, что при рассмотрении наших требований к производительности в Glispa мы быстро пришли к выводу, что значительно выиграем от подхода к кэшированию, при котором данные хранятся временно, чтобы запросы на эти данные могли обслуживаться быстрее. Нам нужно было решить, как лучше настроить такую ​​систему, исходя из наших требований, поскольку существует несколько различных подходов к этому, в зависимости от вариантов использования. Ниже приведены три подхода к кэшированию, которые мы рассмотрели в Glispa.

Ленивая загрузка

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

Это решение легко реализовать, но оно имеет несколько недостатков для определенных случаев использования. Во-первых, время доступа к данным недетерминировано, оно будет сильно различаться. Запросы данных, которые еще не кэшированы, будут выполняться медленно. Очистка старых данных также может быть сложной задачей. Подход «время жизни» (TTL), в котором учитывается количество времени, в течение которого кэшированный элемент данных, как ожидается, будет действительным, может быть трудно настроить. Если выбранные TTL слишком короткие, это повлияет на производительность, поскольку они будут удалены из кэша слишком рано. Установите их слишком высоко, и вы можете показывать устаревшие кампании, поскольку кеш не обновлялся достаточно часто. Подходы с инвалидацией, при которых данные, которые были изменены, становятся недействительными в кэше, что означает, что их необходимо снова извлечь из хранилища, также влияют на производительность. Инвалидация части ваших данных может вызвать пик запроса в хранилище данных и увеличить время ответа. Конечно, кеш также должен поддерживать параллелизм, сохраняя и извлекая данные из кеша одновременно.

Стремительная загрузка с одним буфером

Другой вариант — использовать нетерпеливую загрузку с одним буфером, особенно если цель состоит в том, чтобы серверы имели детерминированное время доступа. Одним из подходов к этому является фоновая активная синхронизация.

При таком подходе все данные загружаются в кеш. Впоследствии процесс фоновой синхронизации поддерживает его в актуальном состоянии. Данные всегда находятся в памяти, поэтому время доступа постоянно и единственная задержка — это период синхронизации. При инкрементальной синхронизации этот период может быть небольшим, всего несколько секунд. Тем не менее, можно столкнуться с проблемами, связанными с параллелизмом. Каждый класс данных должен обрабатывать тот факт, что он может быть записан (потоком фоновой синхронизации) в то же время, когда он читается (потоком обработки запросов). Это означает, что необходимо использовать адаптированные коллекции и механизм блокировки.

Нетерпеливая загрузка с двойными буферами

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

Рассмотрим вариант использования

Кэширование возможно в различных формах. Чтобы выбрать правильную стратегию, важно учитывать характеристики ваших данных (общий размер, требуемая частота обновления и использование среды выполнения). Экспериментальным путем мы узнали, какую стратегию выбрать: не бойтесь пробовать инкрементальные подходы и следите за временем отклика (в среднем и в процентилях), чтобы найти решение, которое наилучшим образом соответствует вашим требованиям.

Также нет необходимости ограничиваться одной стратегией, приложение может использовать разные подходы для разных данных. В Glispa мы широко используем как ленивую загрузку, так и активную загрузку с двойной буферизацией. У нас есть реализация для двойного буфера, который мы используем в нескольких проектах, таких как серверы DSP, а также мы используем Guava для ленивой загрузки другого набора данных.

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

Об авторе:

Первоначально опубликовано на www.glispa.com.