Памятка по Riak — часть вторая, практическая

28 октября 2013

В прошлой части мы познакомились с теорией, касающейся устройства Riak. Сегодня же придется запачкать руки — установить и настроить Riak под Ubuntu Linux, а также познакомиться с REST API этой СУБД.

Установка и настройка Riak под Ubuntu Linux

Riak устанавливается очень просто. Качаем самый свежий deb-пакет с официального сайта, а затем устанавливаем его командой dpkg -i riak.deb. В боевом окружении рекомендуется использовать кластер, содержащий не менее пяти нод. Однако в рамках данной заметки мы будем работать с «кластером» из одной ноды. Новые узлы в Riak добавляются очень просто и прозрачно с точки зрения приложения. Подробности вы можете найти в официальной документации от Basho.

Некоторые настройки Riak нужно изменить перед его первым запуском, так как их будет непросто изменить в будущем. Настройки хранятся в /etc/riak/app.config. Формат этого файла может показаться немного непривычным, если вы не знакомы с синтаксисом Erlang. Если в двух словах, то квадратные скобки означают списки, фигурные — кортежи, числа — числа, строки в кавычках — строки, строки без кавычек — именованные константы (атомы), комментарии начинаются со знака процента. Существует две типичные ошибки — (1) поставить запятую там, где она не нужна, например, после последнего элемента списка, и (2) забыть кавычки, в результате чего вместо строки получается атом. Будьте внимательны.

Первое, что нам предстоит изменить в конфиге — это используемый бэкенд для хранения данных. По умолчанию Riak использует бэкенд Bitcask. Bitcask устроен очень просто. Данные храняться в файле на диске. При создании и изменении пар ключ-значение данные дописываются в конец файла. В памяти Riak хранит хэш-таблицу ключ-смещение. Когда пользователь запрашивает данные, при помощи хэш-таблицы определяется смещение данных в файле, делается lseek, затем read. Главная проблема Bitcask состоит в том, что периодически приходится производить сборку мусора, то есть, уплотнять данные в файле. Если верить рассылке erlang-russian, во время такой сборки мусора ответы на некоторые запросы могут формироваться до 15 секунд вместо типичных 5-10 мс.

Бэкенд eLevelDB (Erlang-биндинг к LevelDB) лишен этого недостатка. Кроме того, eLevelDB поддерживает вторичные индексы (2i), кэширование данных в памяти, а также сжатие данных на диске. Чтобы Riak по умолчанию использовал eLevelDB, найдите в конфиге примерно такой участок:

{riak_kv, [
  %% Storage_backend specifies the Erlang module defining the storage
  %% mechanism that will be used on this node.
  {storage_backend, riak_kv_bitcask_backend},

… и замените значение storage_backend на riak_kv_eleveldb_backend:

{riak_kv, [
  %% Storage_backend specifies the Erlang module defining the storage
  %% mechanism that will be used on this node.
  {storage_backend, riak_kv_eleveldb_backend},

Дополнение: Как выяснилось, при использовании eLevelDB есть свои проблемы. Их обещают исправить в Riak 2.0.

Помимо eLevelDB и Bitcask есть еще два бэкенда — Memory и Multi. Первый пригодиться, если вы хотите построить при помощи Riak in-memory кэш с репликацией и авторешардингом данных. Или если вам действительно очень важна скорость. Бэкенд Multi позволяет делать две вещи. Во-первых, с его помощью вы можете использовать разные бэкенды для хранения разных данных. Для большинства данных хорошо подходит eLevelDB, но в некоторых случаях, возможно, будет эффективнее использовать Bitcask, а всякие там сессии и коды капч можно спокойно держать в памяти. Во-вторых, бэкенд Multi позволяет использовать один и тот же бэкенд с разными настройками. Подробности о том, как настраивается бэкенд Multi, вы найдете здесь.

Еще один важный параметр, который следует изменить перед запуском Riak — это количество партиций, оно же количество vnode’ов. Как вы можете помнить, при добавлении и удалении нод в кластер, vnode’ы перераспределяются между нодами так, чтобы (1) каждая физическая нода содержала примерно равное количество vnode’ов и (2) любые N подряд идующих vnode’ов хранились на N разных физических нодах (см иллюстрацию и объяснение того, как Riak делает репликацию, в предыдущей заметке). Если указать слишком малое количество партиций, нагрузка между физическими нодами будет распределена не совсем равномерно, например, где-то она будет на 25% больше. Кроме того, могут возникнуть проблемы с репликацией. Если же значение слишком велико, при добавлении нового физического узла будет происходить передача слишком большого количества данных между нодами.

По умолчанию количество партиций равно 64, что позволяет построить кластер, содержащий 5-6 нод. По понятным причинам, это значение лучше увеличить. Найдите в конфиге:

{riak_core, [
  %% Default location of ringstate
  {ring_state_dir, "/var/lib/riak/ring"},

  %% Default ring creation size.  Make sure it is a power of 2,
  %% e.g. 16, 32, 64, 128, 256, 512 etc
  %{ring_creation_size, 64},

… а затем раскомментируйте ring_creation_size и увеличьте его значение, скажем, до 512:

{riak_core, [
  %% Default location of ringstate
  {ring_state_dir, "/var/lib/riak/ring"},

  %% Default ring creation size.  Make sure it is a power of 2,
  %% e.g. 16, 32, 64, 128, 256, 512 etc
  {ring_creation_size, 512},

Количество партиций должно быть степенью двойки. Согласно книге «Riak Handbook», если в вашем кластере будет 6-12 нод, то подойдет значение ring_creation_size 128 или 256. Значение 1024 позволит построить кластер, содержащий до 100 нод. Однако в документации по 2i говорится, что вторичные индексы не рекомендуется использовать, если партиций больше, чем 512. Общая рекомендация такова — ring_creation_size должен быть на порядок, то есть, в десять раз, больше планируемого максимального количества нод в кластере, но не более 1024.

Продолжаем настройку. В секции lager найдите строку:

{error_logger_hwm, 100}

… и замените ее на:

{error_logger_hwm, 1000}

Параметр задает максимальное количество сообщений в секунду, которое Riak пишет в логи. Если не увеличить это значение, Riak может упасть, а в логах будет написано просто «lager_error_logger_h dropped 35 messages in the last second that exceeded the limit of 100 messages/sec», в связи с чем вы так и не сможете понять причину ошибки.

Затем откройте /etc/default/riak и пропишите там:

ulimit -n 65536

Riak работает с большим количеством файловых дескрипторов. Здесь мы увеличиваем максимальное количество файловых дескрипторов, которое может открыть Riak, до 65536. Если этого не сделать, Riak может упасть после выполнения нескольких запросов.

Вам также может захотеться активировать Riak Control и Riak Search. Эти два компонента отключены по умолчанию.

Проверяем, что в конфиге нет синтаксических ошибок:

$ /usr/lib/riak/erts-5.9.1/bin/erl

1> {ok, _} = file:consult("/etc/riak/app.config"), ok.
ok
2> q().

Наконец, запускаем Riak:

sudo service riak start

В случае возникновения проблем смотрите в /var/log/riak/console.log.

Простые примеры работы с REST API

Riak умеет дофига всего. Здесь мы рассмотрим лишь малую часть его возможностей, чтобы хотя бы почувствовать, как примерно Riak выглядит в работе. Взаимодействовать с Riak мы будем через REST API при помощи curl.

Для начала проверим, что Riak вообще запущен:

$ curl localhost:8098/ping

OK

Кое-какую дополнительную информацию можно получить так:

$ curl -H 'Accept: text/plain' localhost:8098/stats

Тут мы в основном видим всякие метрики. Они могут пригодится для мониторинга Riak-кластера. Также обратите внимание на значения ring_num_partitions и storage_backend.

Данные в Riak хранятся в так называемых корзинах (buckets). Корзины могут вызывать ассоциации с таблицами из мира РСУБД, но на самом же деле корзины мало чем отличаются от обычных префиксов в ключах. Получим список существующих корзин:

$ curl 'http://localhost:8098/riak?buckets=true'

{"buckets":[]}

Поскольку мы только что подняли Riak, никаких корзин пока что нет. Создадим новую запись:

$ curl -XPOST -H 'Content-type: application/json' \
    -d '{"name":"Alex","phone":79161238888}' \
    localhost:8098/riak/users/Alex8888

Указывать какой-либо id не обязательно, Riak умеет генерировать их сам:

$ curl -XPOST -H 'Content-type: application/json' \
    -d '{"name":"Bob","phone":791612345679}' \
    localhost:8098/riak/users -D -

HTTP/1.1 201 Created
Vary: Accept-Encoding
Server: MochiWeb/1.1 WebMachine/1.10.0 (never breaks eye contact)
Location: /riak/users/RHYVXxidxD8ljYKh3r8mXgKHEYT
Date: Sat, 19 Oct 2013 12:21:59 GMT
Content-Type: application/json
Content-Length: 0

Чтение:

$ curl localhost:8098/riak/users/Alex8888

{"name":"Alex","phone":79161238888}

Обновление данных:

$ curl -XPOST -H 'Content-type: application/json' \
    -d '{"name":"Bob","phone":79161237777}' \
    localhost:8098/riak/users/RHYVXxidxD8ljYKh3r8mXgKHEYT -D -

Удаление:

$ curl -XDELETE localhost:8098/riak/users/Alex8888

Обратите внимание, если теперь запросить список корзин, будет получен ответ {"buckets":["users"]}.

Вроде с CRUD разобрались, давайте теперь попробуем 2i. Напоминаю, что вторичные индексы поддерживаются только в бэкенде eLeveDB. Вторичные индексы бывают двух видов — числа и строки. Они различаются при помощи окончаний в именах индексов, числовые заканчиваются на «_int», а строковые — на «_bin». Допустим, мы решили индексировать пользователей по номерам телефонов:

$ curl -XPOST -H 'Content-type: application/json' \
    -H 'X-Riak-Index-phone_int: 79163334455' \
    -d '{"name":"Alice","phone":79163334455}' \
    localhost:8098/riak/users/alice

Поиск по точному значению:

$ curl localhost:8098/buckets/users/index/phone_int/79163334455

{"keys":["alice"]}

Поиск по диапозону значений:

$ curl localhost:8098/buckets/users/index/phone_int/79163334400/79163335500

{"keys":["alice"]}

Если хочется знать, какое именно значение ключа попало в диапозон:

$ curl 'localhost:8098/buckets/users/index/phone_int/79163334400/79163335500?return_terms=true'

{"results":[{"79163334455":"alice"}]}

Также поддерживается пагинация и сортировка, подробности можно найти тут. Обратите внимание, что вторичные индексы не так дешевы, как вы можете думать. Чтобы сформировать ответ, нода вынуждена каждый раз обращаться ко всем остальным нодам кластера.

Теперь попробуем линки. Линки — это однонаправленные связи между данными, похожие на внешние ключи из мира РСУБД. Давайте добавим в корзину с юзерами некого Боба, который считает Алису своим другом:

$ curl -XPOST -H 'Link: </riak/users/alice>; riaktag="friend"' \
    -H 'Content-type: application/json' \
    -d '{"name":"Bob","phone":79161237777}' \
    localhost:8098/riak/users/bob -D -

Линки используются через механизм, называемый link walking. Допустим, мы хотим получить всех друзей Боба:

$ curl localhost:8098/riak/users/bob/users,friend,1

--IfWX80xSsehv7z3io4aISh640xD
Content-Type: multipart/mixed; boundary=UxwFOfEjC0FmYNUaPk3zwwsAMfa

--UxwFOfEjC0FmYNUaPk3zwwsAMfa
X-Riak-Vclock: a85hYGBgzGDKBVIcypz/fgYllS/JYEpkzGNlSP557TRfFgA=
Location: /riak/users/alice
Content-Type: application/json
Link: </riak/users>; rel="up"
Etag: 8oqM64OJZKzZmMXFi6LLj
Last-Modified: Sat, 19 Oct 2013 12:38:59 GMT
x-riak-index-phone_int: 79163334455

{"name":"Alice","phone":79163334455}
--UxwFOfEjC0FmYNUaPk3zwwsAMfa--

--IfWX80xSsehv7z3io4aISh640xD--

Как видите, мы успешно сходили по ссылке. Обратите внимание на последнюю часть URL, users,friend,1. Здесь «users» задает ограничение на корзины, в которых нужно искать связанные записи, а «friend» — тип линков, по которым нужно произвести link walking. Что означает цифра «1» будет показано чуть ниже. Для снятия ограничения можно писать «_». Например, все записи, на которые неважно каким образом ссылается Боб, можно получить так:

$ curl localhost:8098/riak/users/bob/_,_,_

Теперь создадим нового пользователя, Кэрол:

$ curl -XPOST -H 'Content-type: application/json' \
    -d '{"name":"Carol","phone":79161239876}' \
    localhost:8098/riak/users/carol -D -

… и скажем, что Алиса считает Кэрол своей подругой:

$ curl -XPOST -H 'Link: </riak/users/carol>; riaktag="friend"' \
    -H 'Content-type: application/json' \
    -d '{"name":"Alice","phone":79163334455}' \
    localhost:8098/riak/users/alice

Теперь найдем людей, которых друзья Боба считает своими друзьями:

$ curl localhost:8098/riak/users/bob/_,friend,0/_,friend,1

--C4yEcUqH4NpfCbdO47Bbg1O73un
Content-Type: multipart/mixed; boundary=D8xysSs2sRHwyyCvxRipPPpPpYr

--D8xysSs2sRHwyyCvxRipPPpPpYr
X-Riak-Vclock: a85hYGBgzGDKBVIcypz/fgYlla/MYEpkzGNl6GW8fpovCwA=
Location: /riak/users/carol
Content-Type: application/json
Link: </riak/users>; rel="up"
Etag: 7KcXfwyTpPSAMfaUP9kt1S
Last-Modified: Sat, 19 Oct 2013 13:13:49 GMT

{"name":"Carol","phone":79161239876}
--D8xysSs2sRHwyyCvxRipPPpPpYr--

--C4yEcUqH4NpfCbdO47Bbg1O73un--

Так вот, для чего нужны эти единички и нолики! Это же флаги, говорящие о том, нужно ли включать соответствующую запись в результат. Например, в приведенном выше результате не фигурирует Алиса. Чтобы она туда попала, нужно сказать:

$ curl localhost:8098/riak/users/bob/_,friend,1/_,friend,1

Отметим, что знак подчеркивания на месте единички или нолика считается ноликом. Само собой разумеется, ничто не мешает Бобу иметь больше одного друга. Проверьте сами!

Заметьте, что в Riak-кластере все данные автоматически реплицируются на три ноды. Считается, что запись прошла успешно, если большинство нод (то есть, две) подтвердили выполнение операции. Аналогично при чтении актуальное значение выбирается путем голосования. Эти и другие параметры могут быть изменены как на уровне корзины, так и при выполнении каждого отдельного запроса. Но, к сожалению, это уже выходит за рамки данной заметки. Также за кадром остались MapReduce, полнотекстовый поиск, хождение в Riak через Protobuf, разрешение конфликтов, precommit/postcommit хуки, автоматическая экспирация данных и многие другие интересные вещи.

Следует отметить, что такие возможности, как 2i, Riak Search и MapReduce нужно использовать осторожно. Как уже отмечалось, на практике они могут оказаться совсем не так эффективны, как вы представляете.

Дополнительные материалы

Подробности вы найдете в следующих источниках:

А вам доводилось использовать Riak?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.