Skip to content

Хранилища данных в Node.js: Riak

02/06/2010

Если Вы следите за записями о Node.js в Твиттере или блогах, Вам наверняка случалось слышать о связке Node.js + Riak. Я тоже недавно узнал об этом хранилище, и мне стало интересно, что оно из себя представляет и чем выделяется в ряду остальных.

Если коротко, Riak — распределённое key-value хранилище с eventual consistency (изменения рано или поздно распространяются по всему кластеру) и запросами на основе map/reduce.

Распределённость

Riak изначально нацелен на распределённое окружение. Всё пространство возможных ключей представлено в виде кольца, равномерно разделённого между множеством виртуальных узлов. Виртуальные узлы размещены на физических компьютерах кластера таким образом, чтобы лежащие рядом на кольце виртуальные узлы оказывались на разных физических машинах. Серверы добавляются и удаляются из кольца (вручную либо по роковому стечению обстоятельств :)), и кольцо автоматически перераспределяет виртуальные ноды с данными. Т.е. даже если уборщица случайно опрокинет на один из серверов ведро грязной воды, приложения использующие riak-кластер этого просто не заметят. Ну, кроме задержки на перераспределение данных.

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

Конфликты и eventual consistency

Riak довольно интересно справляется с конфликтами правок. Для отслуживания порядка изменений используются vector clocks. Если Riak замечает что два клиента одновременно изменили одну и ту же запись, есть два варианта развития событий, определяемых конфигурацией кластера. В первом варианте приоритет имеет последняя правка. Во втором Riak сохранит оба варианта документа, и как только следующий клиент попытается прочитать данные по тому же ключу, Riak отдаст обе версии — таким образом, разрешение конфликтов полностью ложится на использующее Riak приложение. В Riak нет атомарных операций, как например в Redis. Таким образом его должно быть удобно использовать в системах, где объекты редактируются многими клиентами одновременно (всякие collaboration tools). Я сам пока не пробовал такое писать, может с Riak как раз попробую🙂.

В остальном всё довольно обычно для key-value store. Данные хранятся в виде документов, одни документы могут содержать ссылки на другие. У Riak есть REST-интерфейс, через который и работают коннекторы, и есть интерфейс на основе Protocol Buffers, который пока для Ноды никто не реализовал. Он должен обеспечивать большую пропускную способность за счёт лучшего сжатия и более быстрого создания/парсинга.

Установка

Я взял версию 0.8.1 с оф. сайта:

wget http://hg.basho.com/riak/downloads/riak-0.8.1.tar.gz

Судя по распаковываемым файлам, Riak требует для работы Erlang. Как оказалось, простой Erlang из Debian Lenny ему не подходит, пришлось ставить по этой инструкции.

Если мы собираем riak самостоятельно (Erlang уже есть), всё тоже довольно просто:

make all rel

Здесь rel это место куда мы ставим Riak.

Отдельно от Riak поставляется модуль хранения Innostore (проблемы лицензирования, кажется), который и рекомендуется для production-серверов. Поставим и его до кучи (придётся ставить из исходников):

wget http://downloads.basho.com/innostore/innostore-10/innostore-10.tar.gz

Распаковываем. Чтобы Innostore нормально собралось на однопроцессорной машине, надо сделать export ERL_FLAGS="-smp enable". Делаем make. Когда хранилище соберётся, ставим его в папку Riak’а:

./rebar install target=$RIAK/lib

Вместо $RIAK надо подставить директорию, куда мы собирали Riak (я просто заранее сделал export RIAK=/root/riak/riak-0.8.1/rel/riak).

Лeзем в папку где установлен Riak, редактируем файл etc/app.config. Там в самом начале находим такую строчку:

{storage_backend, riak_dets_backend},

Меняем её на:

{storage_backend, innostore_riak},

Если надо поменять IP, на котором висит Riak (например, повесить на внешний ip вместо 127.0.0.1), это делается здесь же, в такой строке:

{riak_web_ip, "127.0.0.1"},

Здесь же лучше исправить префикс веб-интерфейса с «raw» на «riak» — в официальной документации и всех модулях употребляется именно riak, но по умолчанию там raw:

{raw_name, "riak"},

Если забудете это сделать, Riak-клиент будет спамить вас сообщениями о 404 ошибке, вот такими:

<HTML><HEAD><TITLE>404 Not Found</TITLE></HEAD><BODY><H1>Not Found</H1>The requested document was not found on this server.<P><HR><ADDRESS>mochiweb+webmachine web server</ADDRESS></BODY></HTML>

В конец того же файла (app.config) дописываем конфиг для хранилища. Найдите внизу секцию sasl и посмотрите как она описана. Нам надо после неё вписать вот это:

{innostore, [
    {data_home_dir, "/var/lib/riak/innodb"}, %% Where data files go
    {log_group_home_dir, "/var/lib/riak/innodb"}, %% Where log files go
    {buffer_pool_size, 268435456} %% 256MB in-memory buffer in bytes
]}

При этом после секции sasl надо не забыть добавить запятую (прямо после ]}). Кроме этого, если innostore собирался с ERL_FLAGS="-smp enable", в etc/vm.args надо добавить следующую строку:

-smp enable

Сохраняем, запускаем riak в режиме консоли, чтобы видеть что происходит:

bin/riak console

При этом мы, во-первых, должны увидеть сообщение о том что Innostore подключился нормально:

(riak@127.0.0.1)1> InnoDB: Mutexes and rw_locks use GCC atomic builtins
100531 11:20:03  InnoDB: highest supported file format is Barracuda.
100531 11:20:04 Embedded InnoDB 1.0.6.6750 started; log sequence number 44253
    

…и, во вторых, увидеть замечательную консоль, в которую надо писать на Эрланге. Команда чтобы выйти: q(). (не пропустите точку). После того как выйдете, можно запустить демон уже в нормальном режиме:

bin/riak start

Можно проверить что всё в порядке, запросив информацию о дефолтном bucket’е:

curl http://127.0.0.1:8098/riak/default

Здесь riak — префикс интерфейса (raw_name), default — название bucket’а. Если riak работает нормально, нам в ответ придёт json с параметрами bucket’а.

Теперь надо поставить клиент. Есть два на выбор — один не развивается с февраля, у второго нестандартный интерфейс (а значит, библиотеки типа Do пролетают). Я взял второй.

git clone git://github.com/frank06/riak-js.git

Всё, мы готовы.

Использование

Для начала можно запустить тесты, которые прилагаются к коннектору. Имейте в виду, там подразумеваются ip сервера 127.0.0.1 и префикс riak. Если всё отработало хорошо, можно попробовать писать свои скрипты:

require.paths.unshift("../lib");

var Riak = require('riak-node')
  assert = require('assert'),
  sys = require('sys');

var db = new Riak.Client({host: '127.0.0.1', port: 8098, debug: false});

db.save('astronauts', 'neil', {
    'name':'Neil Armstrong',
    'retired': true,
    'daysinspace':8,
    'missions':['Apollo 11', 'Gemini 8']
    })(function(response, meta) {
      sys.puts('Status code: ' + meta.statusCode);
});

«Шапку» я взял из стандартного тестового скрипта. Если у Вас нестандартный префикс веб-интерфейса, он идёт в db.save параметром interface.

Если мы запустим скрипт два раза, в первый раз он вернёт Status code: 200, а во второй: Status code: 204. Это значит что обновление записи не требуется (мы ничего не изменили).

Теперь достанем данные:

db.get('astronauts', 'neil')(function(response2) {
        sys.puts('Astronaut landed: ' + JSON.stringify(response2));
      });

Более сложные запросы реализуются с помощью map/reduce. Добавим ещё парочку астронавтов:

db.save('astronauts', 'buzz', {
    'name':'Buzz Aldrin',
    'retired': true,
    'daysinspace':12,
    'missions':['Apollo 11', 'Gemini 12']
    })(function(response, meta) {
      sys.puts('Status code [buzz]: ' + meta.statusCode);
});

db.save('astronauts', 'jim', {
    'name':'Jim Lovell',
    'retired': true,
    'daysinspace':29,
    'missions':['Gemini 7', 'Gemini 12', 'Apollo 8', 'Apollo 13']
    })(function(response, meta) {
      sys.puts('Status code [jim]: ' + meta.statusCode);
});

db.save('astronauts', 'david', {
    'name':'David Scott',
    'retired': true,
    'daysinspace':22,
    'missions':['Gemini 8', 'Apollo 9', 'Apollo 15']
    })(function(response, meta) {
      sys.puts('Status code [david]: ' + meta.statusCode);
});

У Riak есть три вида запросов: по ключу, link walking и map/reduce. При запросе link walking мы получим объект со всеми объектами, на которые он ссылается. Map/reduce делается почти как в CouchDB, но с некоторыми исключениями: map-функции можно передавать аргументы, link walking можно использовать в качестве дополнительного шага, шагов в одном запросе может быть несколько (map/map/walk/reduce, например). Простого способа делать запрос по свойствам как в MongoDB тут нет.

Сделаем пробный mapReduce запрос, выводящий все миссии в алфавитном порядке:

var map = function(v, keydata, arg) {
    if (v.values) {        // Если нам достался не пустой ключ...
        var a = Riak.mapValuesJson(v)[0];
        return a.missions; // Возвращаем все миссии астронавта,
    } else {
        return [];         // Иначе - пустой массив
    }
}
var reduce = function(valuelist, arg) {
    var out = [];
    var l = valuelist.length;
    // Делаем массив уникальным...
    for(var i=0; i<l; i++) {
        for(var j=i+1; j<l; j++) {
            // If this[i] is found later in the array
            if (valuelist[i] === valuelist[j])
                j = ++i;
        }
        out.push(valuelist[i]);
    }
    // ...и сортируем
    return out.sort();
}

db.mapReduce({
    'inputs': 'astronauts',
    'query': [{
        'map': {
            'source': map
        }
    },{
        'reduce': {
            'source': reduce
        }
    }]
})(function (response) {
    sys.puts('Missions' + JSON.stringify(response));
});

Функции пишутся прямо так (преобразуются в исходный код они позже, в коннекторе), что довольно удобно — есть подсветка синтаксиса🙂. В map-функцию можно передавать аргументы. Map-шаг происходит на всех нодах одновременно, reduce — только на одной.

Заключение

Riak — довольно интересное хранилище. Высокая надёжность, лёгкая расширяемость, устойчивость к потерям элементов кластера делают Riak неплохим хранилищем для приложений с большим объемом записи. Поддержка версий документов и разрешение конфликтов тоже смотрятся интересно. Если бы был нормальный интерфейс для простых запросов ((age > 20) && (city == 'New York'), Riak мог бы стать отличным решением для облачного хостинга. Но и так он найдёт своё применение — например, в сервисах совместного редактирования. Гибкий механизм map/reduce с переменным числом фаз и ссылками на объекты тоже может кому нибудь пригодиться.

Ссылки по теме

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: