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).