REDIS: такой простой и такой сложный

в 6:19, , рубрики: cache, caching, database design, high availability, high performance, highload, redis, архитектура, базы данных

Меня зовут Андрей Комягин, я СТО компании STM Labs. Мы занимаемся разработкой очень больших распределённых высоконагруженных систем для различных отраслей и в своей работе широко используем open-source решения, в том числе СУБД Redis. Недавно я подробно рассказывал об этой системе на конференции Saint Highload++ 2024, а теперь с удовольствием поделюсь основной информацией с читателями Хабра. Итак, поехали.

Что такое Redis

Redis — это in-memory noSQL СУБД с открытым исходным кодом класса key-value. Он был создан итальянским разработчиком Сальваторе Санфилиппо для решения типовой классической задачи — кэширования запросов при обращениях в базу данных.

Первая версия была написана на языке Tcl и содержала всего 319 строк кода. Затем её переписали на языке C и в 2009 году презентовали в The Hacker News. Сейчас система поддерживает различные языки программирования, включая C#, Java Go, Python, Node.js.

Изначально проект был опубликован под лицензией BSD, но в 2024 сменил лицензию на двойную - RSALv2+SSPLv1, потому что ребята тоже хотят заработать)

Кстати, название системы не имеет никакого отношения к сочному хрустящему корнеплоду: в данном случае слово «Redis» — это аббревиатура от REmote DIctionary Server.

Для чего используется

Redis может применяться для решения самых разных задач.

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

REDIS: такой простой и такой сложный - 1

Распределенная блокировка (distributed lock). Применяется, если нужно разграничить доступ из нескольких распределенных сервисов к общему изменяемому ресурсу.

REDIS: такой простой и такой сложный - 2

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

REDIS: такой простой и такой сложный - 3

Ограничение нагрузки на определенный сервис (Rate Limiter). Чаще всего речь идет о количестве API-вызовов в единицу времени.

REDIS: такой простой и такой сложный - 4

Ограничить нагрузку можно разными способами:

  • Самое простое — использовать обычный счётчик, который хранится в Redis и есть в разрезе каждого пользователя. Но у этого способа масса минусов — например, он не может сглаживать пиковые нагрузки.

REDIS: такой простой и такой сложный - 5
  • Реализация на базе скользящего окна — более продвинутый метод. Мы задаём время или максимальное количество сессий и отбрасываем все запросы, которые не уложились в установленные лимиты. Эту задачу можно решить, например, с помощью структуры Sorted Set (алгоритм решения мы подробно разберем чуть ниже).

Еще один способ решения данной задачи — метод «дырявого ведра» или Leaky Bucket: запросы копятся в хранилище, как в ведре, но поскольку ведро дырявое, старые запросы постепенно «утекают» из него, освобождая место для новых. Однако если скорость поступления запросов превысит скорость протекания ведра, новые запросы будут отклоняться.

REDIS: такой простой и такой сложный - 6

Также с помощью Redis можно проводить аналитику и выстраивать рейтинги — при помощи всё того же Sorted Set:

REDIS: такой простой и такой сложный - 7

Конечно, мы коснулись лишь самой вершины айсберга и оставили «под водой» огромный пласт задач, которые с легкостью можно решить с использованием Redis.

Что внутри

По сути, Redis — это большая распределенная hash-таблица, где в качестве ключа используется произвольная строка, а в качестве значения — одна из поддерживаемых структур данных (строки, списки, хеш-таблицы, set, отсортированные set и т.д.). Вкратце пробежимся по характеристикам:

  • Персистентность: есть два режима хранения — RDB и AOF.

  • Поддержка отказоустойчивости в различных топологиях на все случаи жизни и под любые задачи.

  • Информационная безопасность: контроль доступа и шифрование данных.

  • Configuration management и мониторинг.

REDIS: такой простой и такой сложный - 8

Персистентность — важный аспект, который нужно рассмотреть поподробнее. Redis поддерживает два режима хранения — RDB и AOF.

AOF (append only file) реализует журнал операций, где все новые операции просто дописываются в конец файла. Этот файл человекочитаемый, то есть его вполне можно читать и даже редактировать. В отличие от RDB, AOF не блокирует Redis.

У этого режима есть и недостатки: например, постепенный рост журнала. Но для решения этой проблемы предусмотрена его компактификация — AOF Rewrite. Когда файл чересчур разрастается, Redis запускает AOF Rewrite и схлопывает некоторые команды. Например, если мы инкрементировали счётчик пять раз, то вместо пяти команд после работы алгоритма останется всего одна.

REDIS: такой простой и такой сложный - 9

RDB (Redis database) периодически сохраняет снэпшот всего dataset. Для этого выполняется fork основного процесса и запускается процедура записи снэпшота — BGSAVE. По этой причине для сохранения снэпшота при интенсивном изменении данных в самом общем случае потребуется двойной объем оперативной памяти от размера dataset.

Конечно, в современном ядре ОС есть режим Copy-on-Write, но он сработает только при невысокой интенсивности изменений данных в оперативной памяти. А если мы говорим о сотнях, миллионах и миллиардах ключей, то это очень существенные объемы.

RDB, в отличие от AOF, приводит к блокировкам и не рекомендуется к использованию на мастер-узлах.

REDIS: такой простой и такой сложный - 10

Redis поддерживает различные топологии развёртывания:

  1. Один узел или stand-alone. Самая простая топология. Имеет право на существование, но исключительно для сред разработки и тестирования, поскольку не является отказоустойчивой.

REDIS: такой простой и такой сложный - 11
  1. Master-Replica (Secondary). В такой топологии между мастером и репликой происходит постоянная асинхронная репликация. Для принудительной синхронизации, то есть для перехода на синхронный режим, в Redis есть специальная команда WAIT.

REDIS: такой простой и такой сложный - 12
  1. Sentinel. Эта топология развёртывания широко применялась на ранних версиях Redis, до поддержки полноценного кластера. Она состоит из отдельных специальных узлов, которые мониторят работу основных узлов Redis, отслеживают сбои Master и запускают процесс восстановления. Работает поверх топологии Master-Replica.

REDIS: такой простой и такой сложный - 13
  1. Полноценный кластер, который состоит из набора мастер-узлов и набора реплик (secondary-узлы). Для репликации используется протокол Gossip: распространение информации в нем идет способом, похожим на эпидемию — каждый узел передает информацию известным ему «соседям». Клиенты могут работать как с мастером, так и с репликой, но с реплики идет только чтение.

REDIS: такой простой и такой сложный - 14

Данные в кластере шардированы, то есть разбиты на сегменты. Кластер не использует консистентное хеширование, вместо этого используются так называемые хеш-слоты.

Всего кластер имеет 16384 слота. Для вычисления хеш-слота для ключа используется формула crc16(key) % 16384.

Каждый узел Redis отвечает за конкретное подмножество хеш-слотов. Например:

  • Узел A содержит хеш-слоты от 0 до 5500.

  • Узел B содержит хеш-слоты от 5501 до 11001.

  • Узел C содержит хеш-слоты от 11001 до 16383.

Это позволяет легко добавлять и удалять узлы кластера, то есть осуществлять быстрый решардинг.

Поддерживаемые типы данных

Redis поддерживает огромное количество структур данных. Основные — это, конечно же, string, list, hash, table и set. Для работы с каждой из них есть набор операций: со строками мы работаем с помощью get, mget, set, append, со списками используем lpush, lpop, ltrim, llen. Операций и структур данных так много, что описать их все в этой статье просто невозможно. Поэтому я предлагаю рассмотреть только специфичные типы данных: Sorted Set, Bitmap, HyperLogLog, Stream.

Sorted Set. На базе этой структуры можно построить уже знакомый нам алгоритм «скользящего окна» (Rate Limiter):

  • Для скоринга можно использовать timestamp входящего запроса.

  • Удалить устаревшие элементы, которые вышли за пределы окна, можно командой ZREMRANGEBYSCORE.

  • Количество запросов в окне вычисляется с помощью команды ZCARD.

  • Если количество запросов не превышает лимит, то новый запрос в окно можно добавить командой ZADD.

Разрешенный rate: 2 запроса в минуту!

Разрешенный rate: 2 запроса в минуту!

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

По сути, это битовая маска с возможностью записи и чтения по любому смещению. На фиксацию факта посещения нашего ресурса одним уникальным пользователем нам нужен всего один бит информации. Один битмап может сохранить 2^32 бита информации.

Берем идентификатор пользователя: если он имеет тип integer, то ничего дополнительно делать не нужно, просто используем его как offset в битовой маске. Если он относится к другому типу, то используем функцию hash и берем остаток от деления для получения смещения в маске. А затем выполняем команду SETBIT для установки нужного бита в маске.

Статистика посещений на Bitmap

Статистика посещений на Bitmap

Stream. Посмотрите на картинку. Что она вам напоминает?

REDIS: такой простой и такой сложный - 17

Ну конечно: это просто калька компонентов, знакомая по ландшафту брокера Kafka. Сразу видно, чем вдохновлялись создатели Redis.

Здесь тоже идёт работа с потоками, и терминология нам прекрасно знакома: producer, consumer, consumer group. Все команды для работы с потоками в Redis имеют префикс X: например, XADD. Для чтения данных в рамках consumer group используется команда XREADGROUP, доставка подтверждается командой XACK и так далее.

HyperLogLog — это вероятностная структура данных, которая позволяет определить мощность некоторого множества, т. е. количество уникальных элементов в нем. При этом она использует всего 12 Кбайт оперативной памяти и определяет кардинальность множеств вплоть до 2^64 элементов, а стандартная ошибка такой оценки — 0,81%. То есть даже меньше процента! Для работы с данной структурой нужны команды PFADD и PFCOUNT.

REDIS: такой простой и такой сложный - 18

Производительность

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

REDIS: такой простой и такой сложный - 19

Выше, а значит быстрее, чем Redis, работают только кэши первого и второго уровня (L1 и L2) со скоростями 1 нс и 10 нс соответственно. Redis работает со скоростью оперативной памяти (RAM), а это примерно 100 нс. Ну и для сравнения: SSD работает со скоростью примерно 100 мкс, а запрос на вставку в PostgreSQL выполняется в среднем за 10 мс.

Однако производительность Redis можно оптимизировать!

В Redis есть пакетная обработка или pipelining. Протокол RESP (Redis Serialization Protocol) работает поверх TCP-соединения в режиме «запрос-ответ». Чтобы минимизировать сетевой обмен, можно сгруппировать команды одним пакетом — это значительно повысит общую производительность системы.

Pipelining (время в сек)

Pipelining (время в сек)

На графике можно увидеть, что группировка 10 000 операций дает четырехкратный прирост. А при размерах пачки в 100 000 команд получится уже 7-кратный прирост!

Вывод: без использования pipelining в высоконагруженных системах не обойтись.

Нюансы: на что обратить внимание при работе с Redis

Увы, не все так просто в этом мире, как кажется на первый взгляд: наверное, в каждой системе есть «подводные камни», о которых следует знать, погружаясь в работу с ней. И Redis не исключение. Вот несколько нюансов, о которых я должен предупредить вас «на берегу».

  1. В Redis есть две очень похожие по назначению команды: KEYS и SCAN. Они обе нужны для получения набора ключей, которые соответствуют шаблону, но путать их всё-таки не стоит.

KEYS — это блокирующая команда (а мы помним, что Redis — однопоточная штука, которую лучше не блокировать).

SCAN — это stateless-команда, построенная на базе курсора. Выборка, полученная с помощью команды SCAN, может содержать дубли, однако это не так страшно, как использование KEYS на prod-среде, поскольку от дублей можно легко избавиться алгоритмически.

REDIS: такой простой и такой сложный - 21
  1. В Redis реализованы две стратегии удаления просроченных ключей:

  • ленивое удаление: при каждой операции чтения и записи вызывается функция expireIfNeeded();

  • периодическое удаление, при котором Redis отбирает с помощью алгоритма сэмплирования случайный набор ключей в рамках цикла activeExpireCycle(). Так как набор ключей ограниченный и произвольный, требуется несколько таких итераций. Номер итерации записывает в переменную current_db.

При загрузке RDB-снэпшотов все просроченные ключи фильтруются. При использовании AOF, в журнал в явном виде записывается команда DEL для просроченных ключей. В контексте репликации все алгоритмы просрочки работают исключительно на мастере узле, реплика получает команду DEL если ключ просрочился. В последних версиях Redis добавили индекс на базе RadixTree, в который добавляются все ключи, которые были когда-либо проверены в рамках случайного отбора. Таким образом, все последующие проверки идут существенно быстрее.

  1. Говоря про использование Redis в ландшафте высоконагруженной системы, нельзя обойти стороной тему тюнинга. Здесь есть несколько основных моментов:

  • Отключаем transparent huge pages,

  • Включаем vm.overcommit_memory = 1.

  • Ограничиваем maxmemory сверху на уровне 75-85%.

  • В части сетевого стека поднимаем значение параметра tcp-backlog, особенно для высоконагруженных систем.

  • Контролируем максимальное количество клиентов.

  • Я не советую использовать RDB на мастер-узлах «Редиса», потому что это блокирующая операция.

А есть ли альтернативы Redis?

Напоследок предлагаю рассмотреть несколько СУБД, которые позиционируют себя как более производительные аналоги Redis: Dragonfly, KeyDB и Garnet. Причем это не просто аналоги, а drop-in replacement — то есть их использование не потребует внесения изменений в код или конфигурацию. Давайте разбираться, так ли это на самом деле!

DragjonflyDB — одно из самых свежих решений на рынке:

  • in-memory база данных, написанная на C++;

  • полностью совместима с Redis, но не является его форком;

  • многопоточная архитектура;

  • заново написан полноценный LRU на базе алгоритма Dashtable — реализация 2Q (в Redis используется сэмплирование).

На сайте вендора есть графики, сравнивающие Dragonfly и Redis не в пользу последнего:

Тест Dragonfly от вендора (OPS)

Тест Dragonfly от вендора (OPS)

Судя по этим данным, Dragonfly на порядок обходит Redis как на операциях чтения, так и на операциях записи. На деле же ребята просто взяли single-node конфигурацию Redis и сравнивали свою систему с ней. Но мы-то с вами знаем, что такие конфигурации непригодны для production-среды. Для объективного сравнения нужно использовать полноценный отказоустойчивый кластер Redis. То есть данный график не совсем корректен и явно был создан в целях продвижения «стрекозы».

А что на деле?

  1. Если мы попробуем протестировать заявленный drop-in replacement то обнаружим, что Dragonfly не поддерживает key-space-notifications. А они крайне необходимы для решения многих задач.

  2. В Dragonfly до сих пор нет горизонтального масштабирования. Создатели утверждают, что это такое преимущество: мол, вертикальное масштабирование эффективнее. Но весь наш опыт говорит об обратном: современные решения должны де-факто масштабироваться в горизонт.

Собственно, реакция создателей Redis не заставила себя долго ждать: они провели своё сравнение, взяв для этого полноценный кластер Redis и написав тесты с использованием механики pipelining. И — о чудо! — результаты оказались совершенно иными:

Альтернативный тест (OPS)

Альтернативный тест (OPS)

KeyDB активно развивается с 2019 года и по сути является многопоточным форком Redis.

  • «под капотом» всё та же многопоточная архитектура;

  • заявленный прирост производительности — х5 (по сравнению с single-node конфигурацией Redis);

  • совместим с Redis drop-in replacement;

  • собственная реализация репликации (Active Replica, Multi-master).

Выглядит прекрасно, но почему тогда все не убежали на KeyDB и продолжают использовать Redis? Дело в том, что обе реализации отказоустойчивой топологии, мягко говоря, не самые надежные.

Если посмотреть на другие критерии, станет понятно, почему KeyDB так и не стал полноценной заменой Redis:

REDIS: такой простой и такой сложный - 24

Garnet. Альтернатива Redis от компании Microsoft вышла совсем недавно, но уже вызывает интерес:

  • это кэш-хранилище с открытым кодом под лицензией MIT;

  • написан на C#;

  • собственный многопоточный движок Tsavorite (fork хранилища Microsoft FASTER);

  • хранилище разделено на два: основное (для строк) и объектное (для сложных объектов);

  • все данные лежат в C# heap;

  • совместим с Redis drop-in replacement.

Microsoft тоже сравнили свой продукт с аналогами и составили график. Надо отдать им должное — для анализа они применили pipelining. Результаты впечатляют: Garnet действительно обходит конкурентов по многим показателям и в перспективе возможно даже заменит Redis.

REDIS: такой простой и такой сложный - 25

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

Автор: akomiagin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js