Обзор Riak

Riak - распределенная opensource база данных, разработанная на Erlang и спроектированная в расчете на:

  • Высокую доступность и устойчивость к сбоям;
  • Масштабируемость и простоту обслуживания;
  • Универсальность.

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

Высокая доступность и устойчивость к сбоям

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

Масштабируемость и простота обслуживания

  • Добавление нового сервера тривиально путем копирования конфига и одной команды.
  • Перераспределение данных и все остальное прозрачно происходит за сценой.
  • Минимальный рекомендуемый размер Riak кластера - 5 серверов, меньшее количество не дает раскрыть весь потенциал.
  • Одинаково легко обслуживать как маленький, так и большой кластер.
  • Есть коммерческая Enterprise версия с поддержкой от Basho, компании-разработчика Riak (изначально выходцы из Akamai), равноправной зашифрованной репликацией между датацентрами и поддержкой SNMP.
  • Есть встроенный веб-интерфейс для мониторинга и управления кластером, у меня правда так и не дошли руки его освоить:

Универсальность

  • Схема отсутствует, ключи и данные - произвольные бинарные строки. Ключи располагаются в пространствах имен (bucket).
  • Сериализация - на усмотрение разработчика, популярные варианты - Erlang'овский BERT, JSON для других платформ, можно использовать просто как файловую систему.
  • Модульная система хранилищ данных, альтернатив много, основная - GoogleLevelDB; еще интересный вариант с хранением полностью в оперативной памяти - получается продвинутый распределенный кэш с репликацией, поиском и пр.
  • Гибко настраиваемое количество узлов кластера, которые должны подтвердить успешность операции, чтобы она считалась успешной: можно указывать для всего кластера, пространства имен и даже конкретного запроса. Riak в любом случае остается eventually consistent базой данных (AP из CAP теоремы), но с возможностью управлять балансом производительности операций и надежностью выполнения запросов.
  • Три интерфейса доступа (API):
    • Google ProtocolBuffers - для основного использования в боевых условиях.
    • HTTPREST - для использования в языках, где нет готового клиента на ProtocolBuffers и для того, чтобы по-быстрому что-то посмотреть из консоли через curl. Хотя по факту клиенты для большинства языков программирования есть и проще делать запросы через интерпретатор.
    • Еще есть прямой интерфейс Erlang-сообщений, но даже из самого Erlang им пользоваться не рекомендуют, не говоря уже о реализациях Erlang node (BERT) на других платформах.
  • Вместе с данными хранятся метаданные для разных целей, которые используются в соответствующих типах запросов:
    • Векторные часы для разрешения конфликтов версий данных (обязательно, есть автоматическое разрешение);
    • Индекс для полнотекстного поиска (концептуально позаимствован у Lucene/Solr, опционально);
    • Индекс для простых выборок (по бинарным и числовым полям, опционально);
    • Связанные ключи (отдаленный аналог внешних ключей, опционально).
  • Встроенная поддержка MapReduce, фазы можно реализовывать на Erlang или JavaScript; для обоих языков есть библиотека с наиболее популярными случаями, которые можно использовать для образца.
  • Есть поддержка выполнения операций до/после операций записи/чтения (hooks), чаще всего используются для построения полнотекстного индекса, но можно реализовать и свои, специфичные для приложения.

Недокументированные возможности

Пока я их нашел всего две:

  • Счетчики: как такового API в для увеличения/уменьшения числовых значений (increment/decrement) в Riak нет, так как он не лезет внутрь хранящихся данных. Зато есть векторные часы, которые растут с каждой операцией записи по ключу. Чтобы реализовать увеличение (increment) необходимо записать в Riak пустую бинарную строку с опцией return_body, и у вернувшегося значения сложить все поля в векторных часах. Пример на Erlang. Если нужно еще и уменьшение (decrement) этого можно добиться с помощью пары счетчиков "плюс и минус" и вычитать второе значение из первого. Для авто инкремента основных ключей не самый лучший вариант, но для не особо критичных случаев вполне себе работает.
  • Выборка по списку ключей (multiget)такого API тоже нет, но здесь на выручку приходит MapReduce. Это, пожалуй, наиболее популярное его применение. На вход подаем имеющийся список ключей и используем фазы из готовой библиотеки: reduce_set_union и map_identity. Данные возвращаются неотсортированные  и требуют небольшой обертки на выходе, но все равно это намного быстрее, чем последовательно проходить по списку ключей и делать для каждого обычный get. Пример на Erlang.

Буду рад, если Вы поможете мне дополнить этот список, оставив известные Вам подобные трюки в комментариях.

Подводные камни

  • Если в Вашем приложении необходима функциональность постраничного просмотра отсортированных данных(pagination), то будьте готовы реализовать её на клиенте. То есть Riak быстро сделал нужную выборку всех "страниц" и уже на клиенте её придется отсортировать и выкинуть лишнее. Вообще в большинстве случаев результаты запросов к Riak приходят в произвольном порядке из-за его распределенной природы.
  • В продолжение к предыдущему: в REST Solr интерфейсе есть аргументы (в ProtoBuf это тоже добавили в одной из последних версий), которые, казалось бы, достаточны для реализации постраничного просмотра: sort, start, rows - что еще нужно? На практике оно работает не так, как было бы логично. Сортировка по значению (заданная в sort) применяется ПОСЛЕ того, как была отсчитана страница по start и rows. Они отмеряются по ключам или рейтингу значения в полнотекстном поиске и никак иначе. С тем же успехом эти 5-10 значений можно очень быстро отсортировать и на клиенте. Зачем-то это может быть и нужно, но в моем случае оказалось совершенно бесполезно.
  • У Riak есть 4 основных типа запросов: простой get/set, полнотекстовый поиск, вторичные ключи (secondary indices), МapReduce и проход по связанным ключам (link walking).
    • Если Ваши данные являются сериализованным JSON, BERT или XML, то в большинстве случаев Вам нужны лишь первые два из них, исключение - упомянутая выше выборка по списку ключей через MapReduce.
    • Основной сценарий использования вторичных индексов - метаданные к произвольным неструктурированным бинарным данным, например в случае с аналогом файловой системы. Либо совсем примитивные случаи, когда правда нужно сделать простую выборку по одному целочисленному полю, что бывает редко.
    • Если данные сериализованы, то связанные ключи проще хранить внутри данных, а не средствами СУБД. Разницы в производительности нет, в итоге делается тот же MapReduce с теми же фазами.
  • Хоть Riak "из коробки" и правда надежнее многих других СУБД и 1-2 упавших/отключенных сервера в кластере внешне практически не заметны, есть одно но. Если один узел упал - соединения всех подключенных к нему клиентов теряются. Два основных пути преодоления этого момента:
    • Если кластер клиентов и кластер Riak расположены на разных серверах, то между ними можно поставить отказоустойчивый TCP балансировщик нагрузки, в частности HAProxy или IPVS здесь наиболее органично вписываются.
    • Если на одних и тех же, то есть вариант поставить балансировщик нагрузки перед клиентами (для веба возможно и в HTTP/HTTPS режиме), а каждый клиент подключается к своему локальному серверу Riak и если один, другой или оба сразу упали, то отрубать весь физический сервер целиком.

Выводы

Riak отлично подходит для многих вариантов использования, как в Интернет среде, так и в смежных вроде телекома. Обладает отличным набором положительных "черт характера", о которых шла речь в начале статьи. Прекрасно справляется с большим потоком как операций записи, так и операций чтения.

Как уже упоминалось, практически единственный сценарий, где Riak совсем не справляется, это выборки по большим объемам данных с сортировкой и постраничным выводом. Но даже в этом случае никто не мешает использовать отдельный сервис, который будет индексировать нужным образом данные и подготавливать список идентификаторов для последующей multiget выборки из Riak. К слову, проекты по этой части уже появляются, например Yokozuna - интеграция полноценного Solr с Riak (Riak Search - лишь частичный порт Solr+Lucene на Erlang).