Redis: подробный обзор

На сегодняшний день ассортимент решений для хранения данных очень широк: от встраиваемых СУБД до кластерных распределенных систем. SQL перестал быть стандартом де-факто для доступа к данным, а альтернативные решения давно переросли примитивные хранилища пар ключ-значение. Сегодня я хочу вкратце рассказать об одном из таких решений, продукте, который нашел свое место во многих моих проектах за последние годы, Redis.

 О чем пойдет речь?

 

Прежде чем вдаваться в детали, давайте разберемся чем по сути является главный герой этого поста?

 

  • Redis предлагает пять фиксированных примитивных структур данных, но не дает возможности создавать свои (по принципу таблиц или букетов).
  • Redis держит все данные в памяти, но имеет гибко настраиваемый механизм сохранения их копии на диск.
  • Redis реализован в виде однопоточного демона на C, общающегося по собственному текстово-бинарному TCP-протоколу, но без намеков на многопоточность и кластеризацию.

 

Этих трех тезисов еще не достаточно, чтобы хорошо прочувствовать особенности и основные варианты использования Redis, но общая картинка уже начинает складываться. Интересно? Двигаемся дальше!

 

Модель данных

 

Модель данных у Redis основана на принципе пар ключ-значение, с той лишь разницей, что значение может быть сложной структурой с дополнительными операциями. Пройдемся по каждой:

 

  • Строка: в самом случае значением может быть просто последовательность каких-то байт, о содержании которых Redis ничего не знает и знать не должен. По тому же принципу работает, скажем, memcached и набор основных операций тот же: get/set/del и их групповые аналоги mget/mset. Здесь же стоит упомянуть про возможность атомарного инкремента и получения списка имеющихся в системе ключей.
  • Словарь/хэш: аналогичная структура данных есть во многих языках программирования, так что принцип наверняка знаком — за идентификатором (в языках программирования — переменной, а в Redis — ключом) стоит не одно значение, а подмножество пар ключ-значение с механизмом доступа, аналогичным основному множеству ключей в Redis. По сути команды, работающие со словарями отличаются от работающих со строками наличием первого параметра с идентификатором словаря и буквой h в начале, например hget, hset. Если воспринимать словарь как подмножество пар ключ-значений тяжело, то можно рассматривать их как строки с полями, то есть если в словарях использовать одинаковый набор внутренних ключей, то можно добиться абстракции на подобии таблиц в РСУБД.
  • Набор: используется для хранения множества неотсортированных уникальных значений, предоставляя механизмы для работы с этими данными (команды на букву s), в частности стоит обратить внимание пересечение и объединение множеств. На практике же неотсортированные наборы удобно использовать для проверки уникальности каких-то данных, скажем e-mail'ов зарегистрированных пользователей, время операции проверки наличия значения в наборе не зависит от общего количества значений в наборе, аналогичным свойством обладает и операция подсчета количества значений в наборе…
  • Отсортированный набор: сортировка в наборах осуществляется путем присваивания каждому значению условного ранга (score) в виде целого числа; значения в наборе будут автоматически храниться в порядке от минимального к максимального ранга. Доступные операции (на букву z) позволяют делать выборки значений с определеным диапазоном рангов или по индексу (если представить отсортированный набор в виде массива или списка), узнавать ранг и индекс по значению. Выглядит замысловато, но на практике довольно удобно. Их можно использовать в роли индекса в терминах РСУБД: например, в отсортированном наборе можно хранить идентификаторы пользователей, назначая в качестве ранга их возраст — в результате можно очень быстро делать выборки пользователей по возрасту, после чего по идентификаторам получать более подробную информацию о каждом, скажем командой mget или как-то еще.
  • Список: в отличии от отсортированных наборов в списках значения хранятся просто по порядку. Операции (на букву l) позволяют эффективно добавлять и получать значения в начало, конец и по индексу (порядковому номеру), а также работать с последовательными участками списка. В результате на программном уровне можно реализовать любые абстракции работы с последовательностями, в том числе стек и любые очереди.

 

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

 

В кажом сервере Redis может содержаться до 16 независимых пространств ключей, которые принято называть базами данных.

 

Сохранность данных

 

В Redis доступны четыре основных стратегии для сохраненния полученных:

 

  • Хранить только в памяти: по сути Redis из персистентной базы данных превращается в кэширующий сервер. Производительность в таком режиме максимальна и сравнима с часто используемым в этой роли memcached, в некоторых ситуациях его даже превосходит; плюс не забываем про более продвинутую структуру данных. Минус с высоким риском потери данных — очевиден, за производительность надо осознанно платить.
  • Периодически сохранять на диск: (по-умолчанию) момент, когда Redis решает сделать очередную копию (snapshot) данных на диске, зависит от времени создания предыдущей и количества измененных ключей. При настройках по-умолчанию это случается раз в несколько минут (1-15).
  • Лог транзакций: если потеря нескольких минут изменений данных неприемлема для приложения, есть опция синхронной записи каждого изменения в специальный append-only файл (лог).
  • Репликация: в Redis поддержка репликации достаточно примитивна — каждому серверу можно указать мастера (командой slaveof или одноименной строкой в конфигурации), все изменения на мастере будут воспроизводиться и на подчиненном. Это можно использовать для построения более сложных схем сохранности данных, например: мастер работает в режиме «только в памяти», а подчиненный — в режиме «лог транзакций». У каждого сервера может быть только один мастер, но неограниченное количество подчиненных.

 

Здесь же стоит упомянуть, что копия данных Redis на диске представляет собой бинарный файл (по-умолчанию dump.rdb), резервную копию которого можно сделать любыми средствами операционной системы, закинуть «в облако» наподобии Amazon S3 и.т.п. Восстановление происходит аналогичным образом скармливанием серверу файла в таком формате.

 

Масштабируемость

 

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

 

Но все не так плохо как кажется, этот проект предоставляет достаточно возможностей, чтобы масштабировать систему своими руками именно так, как представляется нужным конкретному проекту. Сразу хочется сказать, что не стоит бросаться воплощать все нижеизложенное в жизнь, если даже по самым оптимистичным прогнозам суммарный объем данных в Redis в вашем проекте не будет превышать 50-100 гигабайт. Какой-либо универсальной реализации нижеизложенного не существует, так как большая часть манипуляций осуществляется на стороне клиента, что потребовало бы реализации на всех возможных языках программирования.

 

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

 

  • Много процессов Redis на одном физическом сервере: процессы Redis однопоточны и если ваш проект работает на физическом не антикварном оборудовании, то для использования всех ресурсов процессора потребуется запускать несколько процессов Redis на разных портах. Еще одна особенность, подталкивающая в пользу такого подхода — существенно большее потребление оперативной памяти 64-битной сборкой Redis, так как удваивается длина указателей, которых используется очень большое количество. 32-битная же сборка позволяет держать в памяти лишь примерно 3  гигабайта данных. Альтернатива: много виртуальных машин с 1 ядром и 3 гигабайтами оперативной памяти.
  • Распределение данных по ключам: так или иначе, у всех данных в Redis есть уникальный идентификатор, от которого можно взять хэш, а от него взять остаток от деления на константу — получим номер сервера в статичном списке ip:port, на котором нужную структуру данных можно найти. На клиенте держится по соединению на каждый используемый сервер Redis, выбирается нужный и отправляется запрос.
  • Создание виртуальных баз данных (ВБД): у предыдущего подхода есть масса недостатков, связанных с перераспределением данных. Эту ситуацию можно решить введя понятие виртуальных БД, то есть брать в качестве константы для остатка не реальное число серверов, а достаточно большую степень двойки, скажем 65536 (будет удобно удваивать количество реальных серверов) — что далее будет считаться номером ВБД (который иногда удобно приписывать к ключу через разделитель). Виртуальные же базы необходимо по произвольному алгоритму распределить по запущенным процессам Redis и в простейшем случае указать это соотношение в конфигурационных файлах приложений-клиентов.
  • Маршрутизатор по виртуальным базам данных: в предыдущем приеме подразумевалось, что распределение виртуальных БД по серверам фиксированное, но очень скоро захочется иметь механизм для перераспределения данных в кластере. Это может потребоваться при неравномерности нагрузки (к какой-то ВБД существенно больше запросов, чем к остальным) или, опять же, при расширении парка используемых серверов. Для реализации потребуется некое общее хранилище для информации о физическом расположении ВБД, это может быть как отдельный «особый» процесс Redis, так и какая-то встраиваемая СУБД, расположенная в общедоступном месте, есть и другие варианты. Добавление такой прослойки для определения расположения данных немного увеличит время отклика, но позволит более гибко перераспределять данные.
  • Перераспределение данных: есть два подхода к переносу ВБД между двумя процессами Redis.
    • В лоб: некий процесс последовательно читает данные с сервера-источника и записывает в сервер-назначение, после окончания процесса отмечает в маршрутизаторе ВБД что данные теперь на новом сервере, удаляет данные на  сервере-источнике. Просто в реализации, не особо быстро при больших объемах.
    • Через репликацию: существенно более хитрый вариант, о котором я услышал на одной из конференций от представителей команды Skype. Заключается в создании процесса, реализующего протокол репликации Redis с обоих сторон. То есть для сервера-источника он представляется подчиненным, а для сервера-назначения — мастером, при этом пропуская через себя только те ключи, которые относятся к нужной ВБД (требуется, чтобы её номер содержался в ключе). По окончании первичной репликации также отмечает в маршрутизаторе, что ВБД в новом месте. В протоколе репликация есть опция передачи начальной части данных бинарным файлом — этот вариант будет максимально быстрым, но потребуется разобраться с его форматом. Ребята из Skype обещали опубликовать в opensource свою реализацию этой схемы, но я её пока не видел…
  • Использование репликации: помимо увеличения максимального объема данных, стоит задумываться и о надежности системы, а также сохранности данных. Как работает репликация мы уже обсуждали, как добавить её к одной из вышеизложенных схем — дело техники. Самый простой вариант — хранить в маршрутизаторе или конфигурационном файле два физических адреса для каждой ВБД, основной и запасной. Запасной же настраивать как подчиненного с логом транзакций, а основной как мастера в режиме кэша или с очень редким созданием копии на диске. Сложные варианты репликации можно «позаимствовать» у изначально-распределенных СУБД вроде Cassandra или Riak, например можно хранить первую копию по указанному в маршрутизаторе адресу, вторую по адресе, который указан для следующей ВБД, а третью — для через-следующей, но в такой схеме нельзя будет использовать репликацию напрямую, потребуется «прослойка», аналогичная описывавшейся в перераспределении данных через репликацию.

 

Бонусы

 

В дополнение хотелось бы упомянуть три интересные особенности Redis, не совсем относящиеся к затрагивавшимся ранее вопросам, но определенно заслуживающих внимания:

 

  • Транзакции: клиент может отправить команду multi, что означает начало транзакции. Все последующие команды не изменят данные, пока сервер не получит команду exec, либо можно отменить транзакцию командой discard.
  • Публикация и подписка на сообщения: помимо стандартного использование в роли хранилища данных, Redis может использоваться и как очередь сообщений. Клиент может включить режим ожидания сообщений по определенному ключу/каналу (команды доступа к данным в этом режиме недоступны), а также отправлять произвольные сообщения всем, кто подписан на определенный канал.
  • Мониторинг: к каждому процессу Redis можно подключиться для просмотра всех поступающих команд или только тех, которые выполнялись больше N микросекунд.

 

Заключение

 

Redis — определенно заслуживающий внимания opensource проект. Благодаря использованию нестандартных решений, он с одной стороны стал очень гибкой системой хранения данных, а с другой — ограничил круг проектов, где ему найдется место в стеке технологий.

Вверх