REDIS: такой простой и такой сложный
Меня зовут Андрей Комягин, я СТО компании STM Labs. Мы занимаемся разработкой очень больших распределённых высоконагруженных систем для различных отраслей и в своей работе широко используем open-source решения, в том числе СУБД Redis. Недавно я подробно рассказывал об этой системе на конференции Saint HighLoad++, а теперь с удовольствием поделюсь основной информацией с читателями Хабра. Итак, поехали.
Что такое Redis
Redis — это in-memory noSQL СУБД с открытым исходным кодом класса key-value. Он был создан итальянским разработчиком Сальваторе Санфилиппо для решения типовой классической задачи — кэширования запросов в базу данных.
Первая версия была написана на языке Tcl и содержала всего 319 строк кода. Затем её переписали на языке C и в 2009 году презентовали в The Hacker News. Сейчас система поддерживает различные языки программирования, включая C#, Java Go, Python, Node.js.
Кстати, название системы не имеет никакого отношения к сочному хрустящему корнеплоду: в данном случае слово «Redis» — это аббревиатура от REmote DIctionary Server.
Для чего используется
Redis может пригодиться вам при выполнении самых разных задач.
Кэширование — самая частая задача, которую решают при помощи этой СУБД. Обычно кэширование рассматривается в контексте обращений в БД: чтобы не было чрезмерной нагрузки, необходимую запись сперва ищут в кэше. Если данных в кэше нет, мы запрашиваем их в базе данных, и результат выполнения запроса сохраняем в кэше, чтобы в следующий раз вернуть эти данные значительно быстрее. Содержимое кэша всегда должно быть актуальным — для этого мы задаём длительность хранения с помощью TTL.
Распределенная блокировка (distributed lock). Применяется, если нужно разграничить доступ из нескольких распределенных сервисов к общему изменяемому ресурсу.
Управление сессиями. Redis можно использовать как централизованное хранилище сессий. С его помощью можно задавать время жизни сессии и вообще очень гибко управлять другими метаданными в рамках сессии.
Ограничение нагрузки на определенный сервис (Rate Limiter). Чаще всего речь идет о количестве API-вызовов в единицу времени.
Ограничить нагрузку можно разными способами:
Самое простое — использовать обычный счётчик, который хранится в Redis и есть в разрезе каждого пользователя. Но у этого способа масса минусов — например, он не может сглаживать пиковые нагрузки.
Реализация на базе скользящего окна — более продвинутый метод. Мы задаём время или максимальное количество сессий и отбрасываем все запросы, которые не уложились в установленные лимиты. Эту задачу можно решить, например, с помощью структуры Sorted Set (алгоритм решения мы подробно разберем чуть ниже).
Еще один способ решения данной задачи — метод «дырявого ведра» или Leaky Bucket: запросы копятся в хранилище, как в ведре, но поскольку ведро дырявое, старые запросы постепенно «утекают» из него, освобождая место для новых. Однако если скорость поступления запросов превысит скорость протекания ведра, новые запросы будут отклоняться.
Также с помощью Redis можно проводить аналитику и выстраивать рейтинги — при помощи всё того же Sorted Set:
Конечно, мы коснулись лишь самой вершины айсберга и оставили «под водой» огромный пласт задач, которые с легкостью можно решить с использованием Redis.
Что внутри
По сути, Redis — это большая распределенная hash-таблица, где в качестве ключа используется произвольная строка, а в качестве значения — одна из поддерживаемых структур данных (строки, списки, хеш-таблицы, set, отсортированные set и т.д.). Вкратце пробежимся по характеристикам:
Полноценная персистентность: есть два режима хранения — RDB и AOF.
Полноценная поддержка отказоустойчивости в различных топологиях на все случаи жизни и под любые задачи.
Информационная безопасность: контроль доступа и шифрование данных.
Полноценный configuration management и мониторинг.
Персистентность — важный аспект, который нужно рассмотреть поподробнее. Redis поддерживает два режима хранения — RDB и AOF.
AOF (append only file) реализует журнал операций, где все новые операции просто дописываются в конец файла. Этот файл человекочитаемый, то есть его вполне можно читать и даже редактировать. В отличие от RDB, AOF не блокирует Redis.
У этого режима есть и недостатки: например, постепенный рост журнала. Но для решения этой проблемы предусмотрена его компактификация — AOF Rewrite. Когда файл чересчур разрастается, Redis запускает AOF Rewrite и схлопывает некоторые команды. Например, если мы инкрементировали счётчик пять раз, то вместо пяти команд после работы алгоритма останется всего одна.
RDB (Redis database) периодически сохраняет снэпшот всего dataset. Для этого выполняется fork основного процесса и запускается процедура записи снэпшота — BGSAVE. По этой причине для сохранения снэпшота при интенсивном изменении данных в самом общем случае потребуется двойной объем оперативной памяти от размера dataset.
Конечно, в современном ядре ОС есть режим Copy-on-Write, но он сработает только при невысокой интенсивности изменений данных в оперативной памяти. А если мы говорим о сотнях, миллионах и миллиардах ключей, то это очень существенные объемы.
RDB, в отличие от AOF, приводит к блокировкам и не рекомендуется к использованию на мастер-узлах.
Redis поддерживает различные топологии развёртывания:
Один узел или stand-alone. Самая простая топология. Имеет право на существование, но исключительно для сред разработки и тестирования, поскольку не является отказоустойчивой.
Master-Replica (Secondary). В такой топологии между мастером и репликой происходит постоянная асинхронная репликация. Для принудительной синхронизации, то есть для перехода на синхронный режим, в Redis есть специальная команда WAIT.
Sentinel. Эта топология развёртывания широко применялась на ранних версиях Redis, до поддержки полноценного кластера. Она состоит из отдельных специальных узлов, которые мониторят работу основных узлов Redis, отслеживают сбои Master и запускают процесс восстановления. Работает поверх топологии Master-Replica.
Полноценный кластер, который состоит из набора мастер-узлов и набора реплик (secondary-узлы). Для репликации используется протокол Gossip: распространение информации в нем идет способом, похожим на эпидемию — каждый узел передает информацию известным ему «соседям». Клиенты могут работать как с мастером, так и с репликой, но с реплики идет только чтение.
Данные в кластере шардированы, то есть разбиты на сегменты. Кластер не использует консистентное хеширование, вместо этого используются так называемые хеш-слоты.
Всего кластер имеет 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 запроса в минуту!
Bitmap. Эта структура данных идеально подходит для высоконагруженных сервисов — она позволяет провести максимально быструю и при этом компактную с точки зрения утилизации памяти аналитику. Например, составить отчет по уникальным пользователям в разрезе суток.
По сути, это битовая маска с возможностью записи и чтения по любому смещению. На фиксацию факта посещения нашего ресурса одним уникальным пользователем нам нужен всего один бит информации. Один битмап может сохранить 232 бита информации.
Берем идентификатор пользователя: если он имеет тип integer, то ничего дополнительно делать не нужно, просто используем его как offset в битовой маске. Если он относится к другому типу, то используем функцию hash и берем остаток от деления для получения смещения в маске. А затем выполняем команду SETBIT для простановки нужного бита в маске.
Статистика посещений на Bitmap
Stream. Посмотрите на картинку. Что она вам напоминает?
Ну конечно: это просто калька компонентов, знакомая по ландшафту брокера Kafka. Сразу видно, чем вдохновлялись создатели Redis.
Здесь тоже идёт работа с потоками, и терминология нам прекрасно знакома: producer, consumer, consumer group. Все команды для работы с потоками в Redis имеют префикс X: например, XADD. Для чтения данных в рамках consumer group используется команда XREADGROUP, доставка подтверждается командой XACK и так далее.
HyperLogLog — это вероятностная структура данных, которая позволяет определить мощность некоторого множества, т. е. количество уникальных элементов в нем. При этом она использует всего 12 Кбайт оперативной памяти и определяет кардинальность множеств вплоть до 264 элементов, а стандартная ошибка такой оценки — 0,81%. То есть даже меньше процента! Для работы с данной структурой нужны команды PFADD и PFCOUNT.
Производительность
Давайте оценим производительность Redis в цифрах и сравним ее с производительностью других компонентов. Для наглядности я изобразил пирамиду, в которой Redis расположен по центру. Чем выше элемент в пирамиде, тем он производительнее:
Выше, а значит быстрее, чем Redis, работают только кэши первого и второго уровня (L1 и L2) со скоростями 1 нс и 10 нс соответственно. Redis работает со скоростью оперативной памяти (RAM), а это примерно 100 нс. Ну и для сравнения: SSD работает со скоростью примерно 100 мкс, а запрос на вставку в PostgreSQL выполняется в среднем за 10 мс.
Однако производительность Redis можно оптимизировать!
В Redis есть пакетная обработка или pipelining. Протокол RESP (Redis Serialization Protocol) работает поверх TCP-соединения в режиме «запрос-ответ». Чтобы минимизировать сетевой обмен, можно сгруппировать команды одним пакетом — это значительно повысит общую производительность системы.
Pipelining (время в сек)
На графике можно увидеть, что группировка 10 000 операций дает четырехкратный прирост. А при размерах пачки в 100 000 команд получится уже 7-кратный прирост!
Вывод: без использования pipelining в высоконагруженных системах не обойтись.
Нюансы: на что обратить внимание при работе с Redis
Увы, никто в этом мире не совершенен: наверное, в каждой системе есть «подводные камни», о которых следует знать, погружаясь в работу с ней. И Redis не исключение. Вот несколько нюансов, о которых я должен предупредить вас «на берегу».
В Redis есть две очень похожие по назначению команды: KEYS и SCAN. Они обе нужны для получения набора ключей, которые соответствуют шаблону, но путать их всё-таки не стоит.
KEYS — это блокирующая команда (а мы помним, что Redis — однопоточная штука, которую лучше не блокировать).
SCAN — это stateless-команда, построенная на базе курсора. Выборка, полученная с помощью команды SCAN, может содержать дубли, однако это не так страшно, как использование KEYS на prod-среде, поскольку от дублей можно легко избавиться алгоритмически.
В Redis реализованы две стратегии удаления просроченных ключей:
ленивое удаление: при каждой операции чтения и записи вызывается функция expireIfNeeded ();
периодическое удаление, при котором Redis отбирает с помощью алгоритма сэмплирования случайный набор ключей в рамках цикла activeExpireCycle (). Так как набор ключей ограниченный и произвольный, требуется несколько таких итераций. Номер итерации записывает в переменную current_db.
Говоря про использование 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 на порядок обходит Redis как на операциях чтения, так и на операциях записи. На деле же ребята просто взяли single-node конфигурацию Redis и сравнивали свою систему с ней. Но мы-то с вами знаем, что такие конфигурации непригодны для production-среды. Для сравнения нужен полноценный отказоустойчивый кластер. То есть данный график не совсем корректен и явно был создан в целях продвижения «стрекозы».
А что на деле?
Если мы попробуем протестировать заявленный drop-in replacement то обнаружим, что Dragonfly не поддерживает key-space-notifications. А они крайне необходимы для решения многих задач.
В Dragonfly до сих пор нет горизонтального масштабирования. Создатели утверждают, что это такое преимущество: мол, вертикальное масштабирование эффективнее. Но весь наш опыт говорит об обратном: современные решения должны де-факто масштабироваться в горизонт.
Собственно, реакция создателей Redis не заставила себя долго ждать: они провели своё сравнение, взяв для этого полноценный кластер Redis и написав тесты с использованием механики pipelining. И — о чудо! — результаты оказались совершенно иными:
Альтернативный тест (OPS)
KeyDB активно развивается с 2019 года и по сути является многопоточным форком Redis.
«под капотом» всё та же многопоточная архитектура;
заявленный прирост производительности — х5 (по сравнению с single-node конфигурацией Redis);
совместим с Redis drop-in replacement;
собственная реализация репликации (Active Replica, Multi-master).
Выглядит прекрасно, но почему тогда все не убежали на KeyDB и продолжают использовать Redis? Дело в том, что обе реализации отказоустойчивой топологии, мягко говоря, не самые надежные.
Если посмотреть на другие критерии, станет понятно, почему KeyDB так и не стал полноценной заменой Redis:
Garnet. Альтернатива Redis от компании Microsoft вышла совсем недавно, но уже вызывает интерес:
это кэш-хранилище с открытым кодом под лицензией MIT;
написан на C#;
собственный многопоточный движок Tsavorite (fork хранилища Microsoft FASTER);
хранилище разделено на два: основное (для строк) и объектное (для сложных объектов);
все данные лежат в C# heap;
совместим с Redis drop-in replacement.
Microsoft тоже сравнили свой продукт с аналогами и составили график. Надо отдать им должное — для анализа они применили pipelining. Результаты впечатляют: Garnet действительно обходит конкурентов по многим показателям и в перспективе возможно даже заменит Redis.
Рассказывать о возможностях использования Redis можно очень и очень долго. В этой статье я постарался дать основную информацию, которая поможет вам познакомиться с этой СУБД и начать использовать её для своих целей. Как показывает опыт применения и эксплуатации во многих информационных системах, в том числе высоконагруженных, Redis — это надёжный, масштабируемый, эффективный и, самое главное, очень производительный инструмент для решения многих типовых задач — начиная от кэширования и заканчивая аналитикой. И на сегодняшний день он выгодно выделяется на фоне аналогичных систем.