Введение в Riak: Часть 1. HTTP-интерфейс, независящий от языка программирования

Сохранение и извлечение данных с помощью HTTP-интерфейса Riak

Это первая часть серии из двух статей о Riak, хорошо масштабируемой распределенной системе хранения данных, написанной на языке Erlang и основанной на Dynamo, хранилище пар ключ-значение высокой готовности компании Amazon. Познакомьтесь с основами Riak и способами сохранения и извлечения данных с помощью ее программного HTTP-интерфейса. Узнайте, как использовать ее инфраструктуру Map/Reduce для выполнения распределенных запросов, как ссылки позволяют определять взаимосвязи между объектами и как обнаруживать эти взаимосвязи с использованием запросов на обход ссылок.

14 мая 2012 года – добавлены ссылки на все статьи серии в разделах Введение и Заключение, а также ссылка на Часть 2 в разделе Ресурсы.

03 апреля 2012 года – в ответ на замечание читателя в параграфе, следующем сразу за листингом 9 примера распределенного поиска, автор изменил третье предложение на "Сохраните код, приведенный в листинге 10, где-нибудь в каталоге".

29 марта 2012 года – в ответ на замечание читателя был изменен оригинальный текст второго параграфа в разделе Введение.

Саймон Бакл, независимый консультант, независимый специалист

Саймон Бакл (Simon Buckle) – фотографияСаймон Бакл (Simon Buckle) - независимый консультант. В сферу его интересов входят распределенные системы, алгоритмы и параллелизм. Получил степень магистра по вычислительной технике в Имперском колледже в Лондоне. Адрес его Web-сайта – simonbuckle.com.



25.09.2012

Введение

Типичные современные реляционные базы данных плохо работают с определенными типами приложений и с трудом справляются с требованиями к производительности и масштабируемости современных интернет-приложений. Необходим другой подход. В последние годы стал популярен новый тип систем хранения данных, т.н. NoSQL-системы, лишенные некоторых недостатков реляционных баз данных. Riak –один из примеров систем хранения данных такого типа.

Riak не единственная NoSQL-система хранения данных. Двумя другими популярными системами являются MongoDB и Cassandra. Хотя они во многом похожи, имеются и существенные отличия. Например, Riak – это распределенная система, в то время как MongoDB – это автономная система; в Riak нет концепции ведущего узла (master node), что делает ее более устойчивой к сбоям. В Cassandra, также основанной на описании Dynamo от Amazon, отсутствуют некоторые функциональные возможности, например алгоритм векторных часов (vector clocks). Вместо этого для разрешения конфликтов Cassandra использует метки времени, поэтому важна синхронизация времени на клиентах.

Еще одной сильной стороной системы Riak является то, что она написана на Erlang. MongoDB и Cassandra написаны на языках программирования "общего назначения" (C++ и Java соответственно), в то время как Erlang, изначально разработанный для поддержки распределенных отказоустойчивых приложений, лучше подходит для написания NoSQL-систем хранения данных, которые имеют ряд общих черт с приложениями, для которых создавался Erlang.

Задания Map/Reduce можно писать либо на Erlang, либо на JavaScript. В данной статье для написания функций map и reduce мы выбрали JavaScript, но можно это сделать и на Erlang. Хотя Erlang-код может выполняться немного быстрее, JavaScript-код понятен более широкой аудитории читателей. Ссылки на дополнительную информацию по Erlang приведены в разделе Ресурсы.


Начало работы

Для работы с примерами, приведенными в данной статье, необходимо установить Riak (см. раздел Ресурсы) и Erlang.

Также нужно создать кластер из трех узлов, работающих на локальной машине. Все данные, хранящиеся в Riak, реплицируются на несколько узлов в кластере. Свойство (n_val) сегмента (bucket), хранящего данные, определяет количество узлов для репликации. Значением этого свойства по умолчанию является 3, следовательно, нам нужно создать кластер минимум с тремя узлами (впоследствии их можно будет создать сколько угодно).

После загрузки исходный код необходимо скомпилировать. Основные действия:

  1. Распакуйте исходный код: $ tar xzvf riak-1.0.1.tar.gz.
  2. Измените каталог: $ cd riak-1.0.1.
  3. Выполните компиляцию: $ make all rel.

После этого Riak будет скомпилирована (./rel/riak). Для локального запуска нескольких узлов необходимо сделать копии ./rel/riak – по одной копии для каждого дополнительного узла. Скопируйте ./rel/riak в ./rel/riak2, ./rel/riak3 и т.д., а затем выполните в каждой копии следующие изменения:

  • В riakN/etc/app.config измените следующие значения: порт, указанный в разделе http{}, handoff_port и pb_port, на что-нибудь уникальное.
  • Откройте riakN/etc/vm.args и измените имя (опять же на что-нибудь уникальное), например на -name riak2@127.0.0.1.

Теперь запустите все узлы по очереди, как показано в листинге 1.

Листинг 1. Запуск каждого узла
$ cd rel
$ ./riak/bin/riak start
$ ./riak2/bin/riak start
$ ./riak3/bin/riak start

Наконец, соедините узлы вместе, чтобы создать кластер, как показано в листинге 2.

Листинг 2. Создание кластера
$ ./riak2/bin/riak-admin join riak@127.0.0.1
$ ./riak3/bin/riak-admin join riak@127.0.0.1

Теперь у вас есть трехузловой кластер, работающий локально. Для его тестирования выполните команду $ ./riak/bin/riak-admin status | grep ring_members.

Вы должны увидеть каждый узел, являющийся частью только что созданного кластера, например ring_members : ['riak2@127.0.0.1','riak3@127.0.0.1','riak@127.0.0.1'].


Программный интерфейс Riak

В настоящее время существует три способа доступа к Riak: HTTP-интерфейс (RESTful-интерфейс), интерфейс Protocol Buffers и родной интерфейс Erlang. Наличие нескольких интерфейсов дает то преимущество, что позволяет выбирать разные способы интеграции приложений. Если приложение написано на Erlang, для более тесной интеграции имеет смысл использовать родной интерфейс Erlang. Существуют и другие факторы (например, производительность), которые могут повлиять на выбор интерфейса. Например, клиент будет работать эффективнее при использовании интерфейса Protocol Buffers, чем при использовании HTTP-интерфейса, т.к. при этом передается меньший объем данных и синтаксический анализ HTTP-заголовков не снижает производительность. С другой стороны, преимущество HTTP-интерфейса заключается в том, что в настоящее время RESTful-интерфейсы знакомы большинству разработчиков (особенно Web-разработчиков) и в большинство языков программирования встроены примитивы для запроса ресурсов по HTTP (например, для открытия URL), т.е. не требуется дополнительное ПО. В данной статье мы сконцентрируемся на HTTP-интерфейсе.

Во всех примерах для взаимодействия с Riak посредством HTTP-интерфейса будет использоваться curl. Это сделано для лучшего понимания используемого программного интерфейса. Есть несколько клиентских библиотек на различных языках, одну из которых следует выбрать при разработке приложения, использующего Riak в качестве хранилища данных. Клиентские библиотеки предоставляют программный интерфейс для Riak, облегчающий ее интеграцию в приложение; вам не придется самостоятельно писать код обработки ответов, которые вы увидите при использовании curl.

Этот интерфейс поддерживает обычные HTTP-методы (GET, PUT, POST, DELETE), которые будут использоваться для извлечения, обновления, создания и удаления объектов соответственно. Мы поочередно рассмотрим каждый из них.

Сохранение объектов

Riak представляет собой распределенную реализацию отображения ключей (строк) на значения (объекты). Riak хранит значения в сегментах. Перед сохранением сегмент не нужно создавать явно; если объект сохраняется в несуществующий сегмент, сегмент создается автоматически.

Сегменты в Riak – это виртуальная концепция; они существуют главным образом как способ группирования связанных объектов. Сегменты имеют свойства, значения которых определяют, что делает Riak с хранящимися в ней объектами. Вот некоторые примеры свойств сегмента:

  • n_val – количество репликаций объекта по кластеру.
  • allow_mult – разрешение параллельных обновлений.

Свойства сегмента (и их текущие значения) можно увидеть, выполнив запрос GET к сегменту.

Для сохранения объекта выполняется HTTP-запрос POST по одному из URL-адресов, приведенных в листинге 3.

Листинг 3. Сохранение объекта
POST -> /riak/<bucket> (1)
POST -> /riak/<bucket>/<key> (2)

Ключи могут назначаться автоматически самой Riak (1) либо определяться пользователем (2).

При сохранении объекта с определенным пользователем ключом можно также выполнить HTTP-запрос PUT к (2), чтобы создать объект.

В последней версии Riak также поддерживается URL-формат /buckets/<bucket>/keys/<key>, но в данной статье мы будем использовать старый формат для обеспечения обратной совместимости с предыдущими версиями.

Если ключ не указан, Riak автоматически назначит его объекту. Давайте сохраним обычный текстовый объект в сегменте foo без явного указания ключа (см. листинг 4).

Листинг 4. Сохранение обычного текстового объекта без указания ключа
$ curl -i -H "Content-Type: plain/text" -d "Some text" \
http://localhost:8098/riak/foo/

HTTP/1.1 201 Created
Vary: Accept-Encoding
Location: /riak/foo/3vbskqUuCdtLZjX5hx2JHKD2FTK
Content-Type: plain/text
Content-Length: ...

Анализируя заголовок Location, можно увидеть ключ, который Riak назначила объекту. Его нелегко запомнить, поэтому альтернативным способом является определение ключа пользователем. Давайте создадим сегмент artists и добавим в него исполнителя по имени Bruce (см. листинг 5).

Листинг 5. Создание сегмента artists и добавление исполнителя
$ curl -i -d '{"name":"Bruce"}' -H "Content-Type: application/json" \
http://localhost:8098/riak/artists/Bruce

HTTP/1.1 204 No Content
Vary: Accept-Encoding
Content-Type: application/json
Content-Length: ...

Если объект с указанным нами ключом создан правильно, мы получим ответ сервера 204 No Content.

В данном примере мы сохраняем значение объекта в JSON-формате, но это можно легко сделать в обычном текстовом или каком-либо другом формате. При сохранении объекта важно правильно указывать заголовок Content-Type (тип содержимого). Например, при сохранении JPEG-изображения следует указать тип содержимого image/jpeg.

Извлечение объекта

Для извлечения сохраненного объекта выполните запрос GET к сегменту, используя ключ извлекаемого объекта. Если объект существует, он будет возвращен в теле ответа, в противном случае сервер возвратит ответ 404 Object Not Found (см. листинг 6).

Листинг 6. Выполнение запроса GET к сегменту
$ curl http://localhost:8098/riak/artists/Bruce

HTTP/1.1 200 OK
...
{ "name" : "Bruce" }

Обновление объекта

При обновлении объекта, также как и при его сохранении, необходим заголовок Content-Type. Например, добавим псевдоним для Bruce (см. листинг 7).

Листинг 7. Добавление псевдонима для Bruce
$ curl -i -X PUT -d '{"name":"Bruce", "nickname":"The Boss"}' \
-H "Content-Type: application/json" http://localhost:8098/riak/artists/Bruce

Как уже говорилось, Riak создает сегменты автоматически. Сегменты имеют свойства. Одно из этих свойств, allow_mult, определяет, разрешены ли параллельные записи. По умолчанию это свойство установлено в значение false, однако при разрешенных параллельных обновлениях для каждого обновления следует отправлять также заголовок X-Riak-Vclock. Этот заголовок должен быть установлен в значение, которое было получено при последнем считывании объекта клиентом.

Riak использует для определения обусловленности модификаций объектов алгоритм векторных часов. Рассмотрение алгоритма векторных часов выходит за рамки данной статьи, но достаточно сказать, что при параллельных обновлениях существует вероятность возникновения конфликтных ситуаций, разрешать которые должно приложение (см. раздел Ресурсы).

Удаление объекта

Удаление объекта происходит по аналогичной схеме: просто выполняется HTTP-запрос DELETE с URL-адресом удаляемого объекта $ curl -i -X DELETE http://localhost:8098/riak/artists/Bruce.

При успешном удалении объекта сервер отправляет ответ 204 No Content, а если удаляемый объект не существует, сервер отправляет ответ 404 Object Not Found.


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

Так что же такое ссылки? Ссылки позволяют пользователю создавать взаимосвязи между объектами. Если вы знакомы с UML-диаграммами классов, можете представлять ссылку как ассоциацию между объектами посредством метки, описывающей взаимосвязь; в реляционной базе данных взаимосвязь устанавливается с помощью внешнего ключа (foreign key).

Ссылки "прикрепляются" к объекту при помощи заголовка Link. Пример заголовка Link приведен ниже. Цель взаимосвязи (например, объект, на который мы ссылаемся) заключается в угловые скобки. Тип взаимосвязи (в данном случае performer) устанавливается в свойстве riaktag: Link: </riak/artists/Bruce>; riaktag="performer".

Добавим несколько альбомов и свяжем их с исполнителем Bruce, являющимся автором данных альбомов (см. листинг 8).

Листинг 8. Добавление альбомов
$ curl -H "Content-Type: text/plain" \
-H 'Link: </riak/artists/Bruce> riaktag="performer"' \
-d "The River" http://localhost:8098/riak/albums/TheRiver

$ curl -H "Content-Type: text/plain" \
-H 'Link: </riak/artists/Bruce> riaktag="performer"' \
-d "Born To Run" http://localhost:8098/riak/albums/BornToRun

Теперь, когда взаимосвязи настроены, можно выполнить к ним запрос link walking (обход ссылок) – так называется процесс запроса взаимосвязей между объектами. Например, для поиска исполнителя альбома The River можно выполнить команду $ curl -i http://localhost:8098/riak/albums/TheRiver/artists,performer,1.

Последний фрагмент команды – это спецификация ссылки. Он указывает, как выглядит запрос ссылки. Первая часть (artists) указывает сегмент, которым ограничивается запрос. Вторая часть (performer) указывает тег, который используется для ограничения результатов, и, наконец, 1 означает, что мы хотим включить результаты этого конкретного этапа запроса.

Можно также выполнять транзитивные запросы. Предположим, у нас есть настроенные взаимосвязи между альбомами и исполнителями, показанные на рисунке 1.

Рисунок 1. Пример взаимосвязей между альбомами и исполнителями
Рисунок 1. Пример взаимосвязей между альбомами и исполнителями

Запрос исполнителей, сотрудничавших с исполнителем альбома The River, можно выполнить посредством команды $ curl -i http://localhost:8098/riak/albums/TheRiver/artists,_,0/artists,collaborator,1. Подчеркивание в спецификации ссылки – это групповой символ, указывающий, что нам не важно, какая это взаимосвязь.


Выполнение запросов Map/Reduce

Map/Reduce – это инфраструктура, распространяемая Google для параллельного выполнения распределенных вычислений в очень больших наборах данных. Riak также поддерживает Map/Reduce, позволяя выполнять более производительные запросы к данным, хранящимся в кластере.

Функция Map/Reduce состоит из фазы отображения (map) и фазы сокращения (reduce). Фаза отображения применяется к некоторым данным и возвращает ноль или более результатов; в терминах функционального программирования это эквивалентно отображению функции на каждый элемент в списке. Фазы отображения выполняются параллельно. Затем фаза сокращения принимает все результаты фаз отображения и объединяет их вместе.

Рассмотрим, например, задачу подсчета частоты встречаемости слов в большом наборе документов. Каждая фаза отображения вычисляет частоту встречаемости каждого слова в конкретном документе. Затем промежуточные результаты отправляются в функцию reduce, которая подсчитывает итоговое значение и выдает результат для всего набора документов. Ссылка на документацию по Google по Map/Reduce приведена в разделе Ресурсы.


Пример: распределенный вариант утилиты grep

В данной статье мы собираемся разработать функцию Map/Reduce, которая будет выполнять распределенный поиск по всему набору документов, хранящихся в Riak. Аналогично утилите grep, окончательным результатом будет набор строк, совпадающих с заданным шаблоном. Кроме того, в каждом результате будет указан номер строки документа, где встретилось совпадение.

Для выполнения запроса Map/Reduce мы выполним запрос POST к ресурсу /mapred. Тело запроса является JSON-представлением запроса; как и в предыдущих случаях, должен присутствовать заголовок Content-Type, который всегда должен иметь значение application/json. В листинге 9 приведен запрос, который мы выполним для распределенного поиска. Каждую часть запроса мы рассмотрим отдельно.

Листинг 9. Пример запроса Map/Reduce
{
  "inputs": [["documents","s1"],["documents","s2"]],
  "query": [
    { "map": { 
        "language": "javascript", 
        "name": "GrepUtils.map", 
        "keep": true, 
        "arg": "[s|S]herlock" } 
    },
    { "reduce": { "language": "javascript", "name": "GrepUtils.reduce" } }
  ]
}

Каждый запрос состоит из входных данных (например, набора документов, над которыми будут выполняться вычисления), и имени функции, выполняемой на фазах отображения и сокращения. Можно также указать источник функций map и reduce непосредственно в самом запросе, используя свойство source вместо name, но здесь я этого не сделал; для использования именованных функций необходимо несколько изменить конфигурацию Riak по умолчанию. Сохраните исходный код, приведенный в листинге 10, в каком-нибудь каталоге. Для каждого узла кластера найдите файл etc/app.config, откройте его и укажите в свойстве js_source_dir название каталога, в котором сохранили код. Для активизации изменений необходимо перезапустить все узлы кластера.

Исходный код в листинге 10 содержит функции, которые будут выполняться на фазах отображения и сокращения. Функция map просматривает каждую строку документа и ищет совпадение с указанным шаблоном (параметр arg). Функция reduce в данном примере почти ничего не делает; она действует как тождественная функция и просто возвращает входные данные.

Листинг 10. GrepUtils.js
var GrepUtils = {       
    map: function (v, k, arg) {
        var i, len, lines, r = [], re = new RegExp(arg);
        lines = v.values[0].data.split(/\r?\n/);  
        for (i = 0, len = lines.length; i < len; i += 1) {
            var match = re.exec(lines[i]);
            if (match) {
                r.push((i+1) + “. “ + lines[i]);
            }
        }
        return r;
    }, 
    reduce: function (v) {
        return [v];
    }    
};

Для выполнения запроса необходимы какие-нибудь данные. Я загрузил пару электронных книг о Шерлоке Холмсе с Web-сайта Project Gutenberg (см. раздел Ресурсы). Первый текст хранится в сегменте documents под ключом s1; второй – в том же сегменте под ключом s2.

В листинге 11 приведен пример загрузки документа в Riak.

Листинг 11. Загрузка документа в Riak
$ curl -i -X POST http://localhost:8098/riak/documents/s1 \
-H “Content-Type: text/plain” --data-binary @s1.txt

После загрузки документа можно выполнять поиск данных. В данном случае мы хотим получить все строки, совпадающие с регулярным выражением "[s|S]herlock" (см. листинг 12).

Листинг 12. Поиск в документах
$ curl -X POST -H "Content-Type: application/json" \
http://localhost:8098/mapred --data @-<<\EOF
{
  "inputs": [["documents","s1"],["documents","s2"]],
  "query": [
    { "map": { 
        "language":"javascript", 
        "name":"GrepUtils.map",  
        "keep":true, 
        "arg": "[s|S]herlock" } 
    },
    { "reduce": { "language": "javascript", "name": "GrepUtils.reduce" } }
  ]
}
EOF

Свойство arg в запросе содержит шаблон, по которому осуществляется поиск в документах; это значение передается в функцию map в качестве параметра arg.

Результаты выполнения задания Map/Reduce с данными примера приведены в листинге 13.

Листинг 13. Данные, полученные после выполнения задания Map/Reduce
[["1. Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan 
Doyle","9. Title: The Adventures of Sherlock Holmes","62. To Sherlock Holmes 
she is always THE woman. I have seldom heard","819. as I had pictured it from  
Sherlock Holmes' succinct description,","1017. \"Good-night, Mister Sherlock 
Holmes.\"","1034. \"You have really got it!\" he cried, grasping Sherlock 
Holmes by" …]]

Функциональность Streaming Map/Reduce

В завершение данного раздела по Map/Reduce мы кратко рассмотрим Riak-функциональность Streaming Map/Reduce (потоковая реализация Map/Reduce). Данная функциональность полезна для заданий, завершение фазы отображения которых занимает определенное время, поскольку потоковая реализация Map/Reduce позволяет получить результаты каждой фазы отображения сразу после того, как они станут доступны, до выполнения фазы сокращения.

Ее можно использовать для эффективного запроса распределенного поиска. Фаза сокращения в нашем примере практически ничего не делает. Фактически мы можем вовсе отказаться от нее и просто выдавать результаты каждой фазы отображения непосредственно клиенту. Для этого необходимо изменить запрос, удалив фазу отображения и добавив ?chunked=true в конец URL-адреса для потоковой передачи результатов (см. листинг 14).

Листинг 14. Изменение запроса для потоковой реализации Map/Reduce
$ curl -X POST -H "Content-Type: application/json" \
http://localhost:8098/mapred?chunked=true --data @-<<\EOF
{ 
  "inputs": [["documents","s1"],["documents","s2"]],
  "query": [
        { "map": {
            "language": "javascript", 
            "name": "GrepUtils.map",
            "keep": true, "arg": "[s|S]herlock" } }
  ]
}
EOF

Результаты каждой фазы отображения (в нашем примере это строки, совпадающие со строкой запроса) будут возвращаться клиенту после завершения этой фазы. Такой подход полезен для приложений, обрабатывающих промежуточные результаты запроса по мере их поступления.


Заключение

Riak – это масштабируемая система хранения пар ключ-значение с открытыми исходными кодами, основанная на принципах, описанных в документации по системе Dynamo компании Amazon. Ее легко разворачивать и масштабировать. К кластеру в любое время можно добавлять дополнительные узлы. Функциональные возможности, такие как link walking и поддержка Map/Reduce, позволяют выполнять более сложные запросы. Кроме HTTP-интерфейса поддерживается родной интерфейс Erlang и Protocol Buffers. Во второй части серии я рассмотрю несколько клиентских библиотек на различных языках и продемонстрирую, как можно использовать Riak в качестве хорошо масштабируемой кэш-памяти.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=837358
ArticleTitle=Введение в Riak: Часть 1. HTTP-интерфейс, независящий от языка программирования
publish-date=09252012