Глава 5. Разработка при помощи librados
{Прим. пер.: рекомендуем сразу обращаться к нашему переводу 2 издания вышедшего в феврале 2019 Полного руководства Ceph Ника Фиска}
Содержание
Ceph предоставляет блочное, файловое и объектное хранение через имеющийся в нём встроенный интерфейс который будет отвечать всем требованиям большей части пользователей. Однако, при некоторых сценариях когда некое приложение разрабатывается изнутри, могут иметься преимущества прямого взаимодействия с Ceph путём применения librados. Librados является собственной библиотекой Ceph, которая даёт возможность приложениям напрямую считывать и записывать объекты на уровне RADOS Ceph.
В данной главе мы рассмотрим следующие вопросы:
-
Что такое librados?
-
Как применять librados и какие языки она поддерживает
-
Как написать некий пример приложения librados
-
Как написать некое приложение librados, которое хранит образы в Ceph при помощи Python
-
Как написать некое приложение librados с использованием атомарных операций при помощи C++
Librados является библиотекой Ceph, которое вы можете включить в своё собственное приложение чтобы позволить себе напрямую общаться с неким кластером Ceph при помощи его естественного протокола. Поскольку взаимодействия librados с Ceph используют его естественные протоколы взаимодействия, это позволяет вашему приложению обуздать всю мощь, скорость и гибкость Ceph вместо необходимости применения протоколов высокого уровня подобного Amazon S3. Огромный массив функций делает возможным для ваших приложений считывать и записывать простые объекты всеми путями, доступными расширенным операциям, причём вы можете пожелать обернуть некие операции в какую- то транзакцию либо работать с ними асинхронно. Librados доступен для различных языков, включая C, C++, Python, PHP и Java.
Чтобы приступить к работе с librados необходима некоторая среда разработки. Для примера данной главы может использоваться один из узлов монитора для действия и в качестве среды разработки, и как сам клиент для исполнения разработанного приложения. Все примеры в данной книге предполагают, что мы применяем некий дистрибутив на основе Debian:
-
Прежде всего установите все основные инструменты построения для своей операционной системы:
$ sudo apt-get install build-essential
Предыдущая команда предоставит вам следующий вывод:
-
Установите необходимую библиотеку разработки librados:
$ sudo apt-get install librados-dev
Предыдущая команда предоставит вам следующий вывод:
-
Теперь, когда ваша среда готова, давайте создадим написанное на C короткое приложение для установления некоторого соединения чтобы проверить кластер Ceph:
$ mkdir test_app $ cd test_app
-
В вашем любимом текстовом редакторе создайте некий файл с названием
test_app.c
и поместите в него следующее:#include <rados/librados.h> #include <stdio.h> #include <stdlib.h> rados_t rados = NULL; int exit_func(); int main(int argc, const char **argv) { int ret = 0; ret = rados_create(&rados, "admin"); // Применяем кольцо ключей client.admin if (ret < 0) { // Проверяем что необходимый объект rados был создан printf("не могу проинициализировать rados! ошибка %d\n", ret); ret = EXIT_FAILURE; exit_func; } else printf("RADOS initialized\n"); ret = rados_conf_read_file(rados, "/etc/ceph/ceph.conf"); if (ret < 0) { //Разбираем файл ceph.conf для получения подробностей о кластере printf("отказано в разборе параметров настройки! ошибка %d\n", ret); ret = EXIT_FAILURE; exit_func(); } else printf("Ceph config parsed\n"); ret = rados_connect(rados); //Инициализируем соединение с данным кластером Ceph if (ret < 0) { printf("couldn't connect to cluster! error %d\n", ret); ret = EXIT_FAILURE; exit_func; } else { printf("Connected to the rados cluster\n"); } exit_func(); //Конец примера, вызов exit_func для очистки и окончания } int exit_func () { rados_shutdown(rados); //Уничтожаем соединение с данным кластером Ceph printf("соединение RADOS удалено\n"); printf("The END\n"); exit(0); }
-
Оттранслируем для проверки соединения, исполнив такую команду:
$ gcc test_app.c -o test_app -lrados
Замечание Важно отметить, что вам необходимо сообщить
gcc
подключаться к самой библиотеке для применения её функций. -
Затем убедитесь что данное приложение работает, выполнив его. Не забудьте исполнять его от имени root или применив
sudo
, в противном случае вы не будете иметь доступа к нужному вам кольцу ключей Ceph:sudo ./test_app
Предыдущая команда предоставит вам следующий вывод:
Чтобы проверить приложение просто прочтите свои настройки ceph.conf
,
воспользуйтесь ими для установления некоего соединения с вашим кластером Ceph, а затем отсоединитесь.
Это вряд ли ли самое захватывающее приложение, однако оно проверяет что базовая инфраструктура работает и
устанавливает основу для всех остальных приложений в данной главе.
Теперь мы пройдём некоторые примеры приложений, использующие librados, чтобы получить лучшее представление о том, что вы получаете от данной библиотеки.
Следующий пример проведёт вас через определённые этапы по созданию некоторого приложения, в котором задаётся в качестве параметра некий файл образа и будет сохранён этот образ в виде объекта в каком- то кластере Ceph и запомнены различные атрибуты этого файла образа в виде атрибутов объекта. Данное приложение также позволит вам выбирать этот объект и экспортировать его как некий файл образа. Данный пример будет написан на Python, который также поддерживается librados. Приводимый далее пример также применяет Python Imaging Library (PIL) для чтения размера некоторого образа и Argument Parser library для чтения параметров командной строки:
-
Вначале нам необходимо установить привязки librados Python и библиотеки манипуляции образом:
$ sudo apt-get install python-rados python-imaging
Предыдущая команда предоставит вам следующий вывод:
-
Создайте некий новый файл для своего приложения Python, оканчивающееся расширением
.py
и введите в него следующее:import rados, sys, argparse from PIL import Image #Синтаксический анализатор аргументов применяется для считывания параметров и выработки --help parser = argparse.ArgumentParser(description='Образ для утилиты объекта RADOS') parser.add_argument('--action', dest='action', action='store', required=True, help='Загружает или выгружает образ в/из Ceph') parser.add_argument('--image-file', dest='imagefile', action='store', required=True, help='Файл образа для загрузки в RADOS') parser.add_argument('--object-name', dest='objectname', action='store', required=True, help='Имя объекта RADOS') parser.add_argument('--pool', dest='pool', action='store', required=True, help='Имя пула RADOS для хранения данного объекта') parser.add_argument('--comment', dest='comment', action= 'store', help='Комментарий для его сохранения вместе с объектом') args = parser.parse_args() try: #Считываем файл настроек ceph.conf для получения мониторов cluster = rados.Rados(conffile='/etc/ceph/ceph.conf') except: print "Ошибка чтения настроек Ceph" sys.exit(1) try: #Соединиться с кластером Ceph cluster.connect() except: print "Ошибка соединения с кластером Ceph" sys.exit(1) try: #Открытие определённого пула RADOS ioctx = cluster.open_ioctx(args.pool) except: print "Ошибка чтения пула: " + args.pool cluster.shutdown() sys.exit(1) if args.action == 'upload': #Если действием является загрузка try: #Открываем файл образа для чтения в двоичном режиме image=open(args.imagefile,'rb') im=Image.open(args.imagefile) except: print "Ошибка открытия образа файла" ioctx.close() cluster.shutdown() sys.exit(1) print "Image size is x=" + str(im.size[0]) + " y=" + str(im.size[1]) try: #Записываем всё содержимое файла образа в объект и добавляем атрибуты ioctx.write_full(args.objectname,image.read()) ioctx.set_xattr(args.objectname,'xres',str(im.size[0]) +"\n") ioctx.set_xattr(args.objectname,'yres',str(im.size[1]) +"\n") im.close() if args.comment: ioctx.set_xattr(args.objectname,'comment',args.comment +"\n") except: print "Ошибка записи объекта или атрибутов" ioctx.close() cluster.shutdown() sys.exit(1) image.close() elif args.action == 'download': try: #Открываем файл образа для записи в двоичном режиме image=open(args.imagefile,'wb') except: print "Онибка открытия файла образа" ioctx.close() cluster.shutdown() sys.exit(1) try: #Записываем объект в файл образа image.write(ioctx.read(args.objectname)) except: print "Ошибка записи объекта в файл образа" ioctx.close() cluster.shutdown() sys.exit(1) image.close() else: print "Определите, пожалуйста --является ли действие загрузкой (upload) или выгрузкой (download)" ioctx.close() #Закрываем соединение с пулом cluster.shutdown() #Закрываем соединение с Ceph #Конец
-
Проверим работоспособность
help
, создаваемую библиотекой синтаксического разбора аргументов:$ sudo python app1.py --help
Предыдущая команда предоставит вам следующий вывод:
-
Выгрузим логотип Ceph для использования в качестве проверки на образе:
wget http://docs.ceph.com/docs/master/_static/logo.png
Предыдущая команда предоставит вам следующий вывод:
-
Выполните наше приложение Python для чтения некоторого файла образа и загрузки его в Ceph в качестве объекта:
$ sudo python app1.py --action=upload --image-file=test1.png --object-name=image_test --pool=rbd --comment="Ceph Logo"
Предыдущая команда предоставит вам следующий вывод:
-
Убедимся что данный объект был создан:
$ sudo rados -p rbd ls
Предыдущая команда предоставит вам следующий вывод:
-
Примените
rados
для проверки что данный атрибут был добавлен к данному объекту:$ sudo rados -p rbd listxattr image_test
Предыдущая команда предоставит вам следующий вывод:
-
Воспользуйтесь
rados
для проверки содежимого данного атрибута, как это показано на следующем снимке экрана:
В предыдущем примере приложения librados был создан некий объект в нашем кластере Ceph и затем были добавлены необходимые атрибуты этого объекта. В большинстве случаев такие два этапа действий могут быть удовлетворительными, однако некоторые приложения могут потребовать чтобы создание самого объекта и его атрибута были атомарными. Иными словами, если возникло некоторое прерывание обслуживания, данный объект должен присутствовать только если установлены его атрибуты, в противном случае наш кластер Ceph должен откатить назад данную транзакцию. Следующий написанный на C++ пример показывает как применять атомарные операции librados чтобы гарантировать непротиворечивость транзакции для множества операций. Данный пример записывает некий объект и затем запрашивает своего пользователя если тот хочет прервать данную транзакцию. Если он выбирает прервать, тогда вся операция записи объекта будет откачена обратно. Если он выбирает продолжить, тогда все атрибуты будут записаны и вся транзакция целиком зафиксирована. Выполните следующие шаги:
-
Создайте какой- то новый файл с расширением
.cc
и разместите в нём следующее:#include <cctype> #include <rados/librados.hpp> #include <iostream> #include <string> void exit_func(int ret); librados::Rados rados; int main(int argc, const char **argv) { int ret = 0; // Определяем переменные const char *pool_name = "rbd"; std::string object_string("Я представляю некий атомарный объект\n"); std::string attribute_string("I am an atomic attribute\n"); std::string object_name("atomic_object"); librados::IoCtx io_ctx; // Создаём необходимый объект Rados и инициализируем его { ret = rados.init("admin"); // По умолчанию применяем client.admin keyring if (ret < 0) { std::cerr << "Failed to initialize rados! error " << ret << std::endl; ret = EXIT_FAILURE; } } // Считываем файл настроек ceph config в определённом для него по умолчанию местоположении ret = rados.conf_read_file("/etc/ceph/ceph.conf"); if (ret < 0) { std::cerr << "Отказ в проведении синтаксического разбора настроек " << "! Ошибка" << ret << std::endl; ret = EXIT_FAILURE; } // Соединяемся с нашим кластером Ceph ret = rados.connect(); if (ret < 0) { std::cerr << "Отказ в соединении со своим кластером! Ошибка " << ret << std::endl; ret = EXIT_FAILURE; } else { std::cout << "Подключён к кластеру Ceph" << std::endl; } // Создаём соединение с необходимым пулом Rados ret = rados.ioctx_create(pool_name, io_ctx); if (ret < 0) { std::cerr << "Отказ в соединении с пулом! Ошибка: " << ret << std::endl; ret = EXIT_FAILURE; } else { std::cout << "Подключён к пулу: " << pool_name << std::endl; } } librados::bufferlist object_bl; // Инициализация списка буферов object_bl.append(object_string); // Добавление строки текста нашего объекта в список буфера librados::ObjectWriteOperation write_op; // Создание транзакции записи write_op.write_full(object_bl); // Запись нашего списка буфера в транзакцию std::cout << "Object: " << object_name << " был записан в транзакции" << std::endl; char c; std::cout << "Вы хотите прервать транзакцию? (Y/N)? "; std::cin >> c; if (toupper( c ) == 'Y') { std::cout << "Транзакция была прервана, поэтому в действительности объект не записан" << std::endl; exit_func(99); } librados::bufferlist attr_bl; // Инициализация другого списка буфера attr_bl.append(attribute_string); // Добавляем наш атрибут в список буфера write_op.setxattr("atomic_attribute", attr_bl); // Записываем на атрибут в своей транзакции std::cout << "Атрибут был записан в транзакцию" << std::endl; ret = io_ctx.operate(object_name, &write_op); // Фиксируем транзакцию if (ret < 0) { std::cerr << "failed to do compound write! error " << ret << std::endl; ret = EXIT_FAILURE; } else { std::cout << "Мы записали транзакцию, содержащую наши объект и атрибут" << object_name << std::endl; } } v oid exit_func(int ret) { // Выполняем очистку и выход rados.shutdown(); exit(ret); }
-
Компилируем данный исходный код при помощи
g++
:g++ atomic.cc -o atomic -lrados -std=c++11
-
Теперь мы можем исполнить своё приложение. Вначале давайте исполним его и прервём имеющуюся транзакцию:
Предыдущий снимок экрана показывает, что даже хотя мы и отправили некую команду записи объекта, поскольку данная транзакция не была зафиксирована, данный объект на самом деле не был записан в наш кластер Ceph.
-
Теперь мы выполним это приложение вновь, но в этот раз давайте продолжим эту транзакцию:
Как вы можете увидеть, на этот раз данный объект был записан вместе со своим атрибутом.
Следующее приложение librados написано на C и отображает как применять функциональность RADOS
watch
или notify
. Ceph
позволяет некому клиенту создавать наблюдателя некоторого объекта и получать уведомления от совершенно
другого клиента, подключённого к тому же самому кластеру.
Функциональность наблюдателя реализуется посредством обратного вызова. Когда вы вызываете определённую функцию librados для создания такого наблюдателя, два имеющихся аргумента используются для функции обратного вызова, причём один служит того что делать при получении уведомления, а другой предназначен для того что выполнять если наблюдатель утрачивает контакт или сталкивается с ошибкой для данного объекта. Эти функции обратного вызова в свою очередь содержат тот код, который вы хотите исполнить при получении некого уведомления или возникновении ошибки.
Данная простая форма обмена сообщениями, который обычно применяется для указания использующему RBD клиенту что требуется сделать некий снимок. Тот клиент, который желает выполнить некий моментальный снимок, отправляет некое уведомление всем клиентам, которые могут наблюдать за этим объектом, что он может сбросить свой кэш и возможно убедиться что данная файловая система находится в согласованном состоянии.
Приводимый ниже пример создаёт наблюдателя некоторого объекта с названием my_object
и затем ожидает. Когда он получает некое уведомление, он отобразит всю полезную нагрузку и затем отправит
принятое сообщение обратно уведомителю.
-
Создайте некий новый файл с расширением
.c
и разместите в нём следующее:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <syslog.h> #include <rados/librados.h> #include <rados/rados_types.h> uint64_t cookie; rados_ioctx_t io; rados_t cluster; char cluster_name[] = "ceph"; char user_name[] = "client.admin"; char object[] = "my_object"; char pool[] = "rbd"; /* Функция обратного вызова наблюдателя - вызывается когда наблюдатель получает некое уведомление */ void watch_notify2_cb(void *arg, uint64_t notify_id, uint64_t cookie, uint64_t notifier_gid, void *data, size_t data_len) { const char *notify_oid = 0; char *temp = (char*)data+4; int ret; printf("Уведомительное сообщение: %s\n",temp); rados_notify_ack(io, object, notify_id, cookie, "Получено", 8); } /* Функция обратного вызова ошибки наблюдателя - вызывается когда наблюдатель стакивается с ошибкой */ void watch_notify2_errcb(void *arg, uint64_t cookie, int err) { printf("Удаляем наблюдение с объекта %s\n",object); err = rados_unwatch2(io,cookie); printf("Создаём наблюдение за объектом %s\n",object); err = rados_watch2(io,object,&cookie,watch_notify2_cb,watch_notify2_errcb,NULL); if (err < 0) { fprintf(stderr, "Не могу создать наблюдение за %s/%s: %s\n", object, pool, strerror(-err)); rados_ioctx_destroy(io); rados_shutdown(cluster); exit(1); } } int main (int argc, char **argv) { int err; uint64_t flags; /* Создаём объект Rados */ err = rados_create2(&cluster, cluster_name, user_name, flags); if (err < 0) { fprintf(stderr, "Не могу создать объект в данном кластере!: %s\n", strerror(-err)); exit(EXIT_FAILURE); } else { printf("Данный объект rados создан.\n"); } /* Считываем файл настроек Ceph для настройки необходимого дескриптора кластера. */ err = rados_conf_read_file(cluster, "/etc/ceph/ceph.conf"); if (err < 0) { fprintf(stderr, "не могу считать файл настроек: %s\n", strerror(-err)); exit(EXIT_FAILURE); } else { printf("Файл настроек считан.\n"); } /* Соединяемся с кластером */ err = rados_connect(cluster); if (err < 0) { fprintf(stderr, "Не могу соединиться с кластером: %s\n", strerror(-err)); exit(EXIT_FAILURE); } else { printf("\n Подключён к кластеру.\n"); } /* Создаём соединение с необходимым пулом Rados */ err = rados_ioctx_create(cluster, pool, &io); if (err < 0) { fprintf(stderr, "Не могу открыть пул rados %s: %s\n", pool, strerror(-err)); rados_shutdown(cluster); exit(1); } /* Создаём Наблюдателя Rados */ printf("Создаём Наблюдателя за объектом %s/%s\n",pool,object); err = rados_watch2(io,object,&cookie,watch_notify2_cb, watch_notify2_errcb,NULL); if (err < 0) { fprintf(stderr, "Не могу создать наблюдателя за объектом %s/%s: %s\n", pool, object, strerror(-err)); rados_ioctx_destroy(io); rados_shutdown(cluster); exit(1); } /* Цикл пока ожидаем уведомление */ while(1){ sleep(1); } /* Очистка */ rados_ioctx_destroy(io); rados_shutdown(cluster); }
-
Компилируем данный код примера
watcher
:$ gcc watcher.c -o watcher -lrados
-
Выполняем данное приложение примера
watcher
:
-
Наш наблюдатель теперь ожидает некого уведомления. В другом терминальном окне, воспользовавшись
rados
, отправим какое- то уведомление нашему наблюдаемому объектуmy_object
:
-
Вы можете видеть, что данное уведомление было отправлено и подтверждающее уведомление было обратно получено. Если мы посмотрим в окно первого терминала вновь, мы можем увидеть такое сообщение от уведомителя:
Это завершает нашу главу по разработке приложений с применением librados. Теперь вам должно быть удобно при наличии основных понятий того,как включать функциональность librados в своё приложение и как читать объекты и записывать их в своём кластере Ceph. Если вы собираетесь разрабатывать некое приложение с применением librados, следует также порекомендовать прочесть всю официальную документацию для получения дополнительных подробностей с тем, чтобы вы могли получить большее представление обо всём диапазоне доступных функций. {Прим. пер.: также см. Приложение A и Приложение B}.