Глава 5. Разработка при помощи librados

{Прим. пер.: рекомендуем сразу обращаться к нашему переводу 2 издания вышедшего в феврале 2019 Полного руководства Ceph Ника Фиска}

Ceph предоставляет блочное, файловое и объектное хранение через имеющийся в нём встроенный интерфейс который будет отвечать всем требованиям большей части пользователей. Однако, при некоторых сценариях когда некое приложение разрабатывается изнутри, могут иметься преимущества прямого взаимодействия с Ceph путём применения librados. Librados является собственной библиотекой Ceph, которая даёт возможность приложениям напрямую считывать и записывать объекты на уровне RADOS Ceph.

В данной главе мы рассмотрим следующие вопросы:

  • Что такое librados?

  • Как применять librados и какие языки она поддерживает

  • Как написать некий пример приложения librados

  • Как написать некое приложение librados, которое хранит образы в Ceph при помощи Python

  • Как написать некое приложение librados с использованием атомарных операций при помощи C++

Что такое librados?

Librados является библиотекой Ceph, которое вы можете включить в своё собственное приложение чтобы позволить себе напрямую общаться с неким кластером Ceph при помощи его естественного протокола. Поскольку взаимодействия librados с Ceph используют его естественные протоколы взаимодействия, это позволяет вашему приложению обуздать всю мощь, скорость и гибкость Ceph вместо необходимости применения протоколов высокого уровня подобного Amazon S3. Огромный массив функций делает возможным для ваших приложений считывать и записывать простые объекты всеми путями, доступными расширенным операциям, причём вы можете пожелать обернуть некие операции в какую- то транзакцию либо работать с ними асинхронно. Librados доступен для различных языков, включая C, C++, Python, PHP и Java.

Как применять librados?

Чтобы приступить к работе с librados необходима некоторая среда разработки. Для примера данной главы может использоваться один из узлов монитора для действия и в качестве среды разработки, и как сам клиент для исполнения разработанного приложения. Все примеры в данной книге предполагают, что мы применяем некий дистрибутив на основе Debian:

  1. Прежде всего установите все основные инструменты построения для своей операционной системы:

    
    $ sudo apt-get install build-essential
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 1



  2. Установите необходимую библиотеку разработки librados:

    
    $ sudo apt-get install librados-dev
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 2



  3. Теперь, когда ваша среда готова, давайте создадим написанное на C короткое приложение для установления некоторого соединения чтобы проверить кластер Ceph:

    
    $ mkdir test_app
    $ cd test_app
     	   
  4. В вашем любимом текстовом редакторе создайте некий файл с названием 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);
    }
     	   
  5. Оттранслируем для проверки соединения, исполнив такую команду:

    
    $ gcc test_app.c -o test_app -lrados
     	   
    [Замечание]Замечание

    Важно отметить, что вам необходимо сообщить gcc подключаться к самой библиотеке для применения её функций.

  6. Затем убедитесь что данное приложение работает, выполнив его. Не забудьте исполнять его от имени root или применив sudo, в противном случае вы не будете иметь доступа к нужному вам кольцу ключей Ceph:

    
    sudo ./test_app
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 3



Чтобы проверить приложение просто прочтите свои настройки ceph.conf, воспользуйтесь ими для установления некоего соединения с вашим кластером Ceph, а затем отсоединитесь. Это вряд ли ли самое захватывающее приложение, однако оно проверяет что базовая инфраструктура работает и устанавливает основу для всех остальных приложений в данной главе.

Пример приложения librados

Теперь мы пройдём некоторые примеры приложений, использующие librados, чтобы получить лучшее представление о том, что вы получаете от данной библиотеки.

Следующий пример проведёт вас через определённые этапы по созданию некоторого приложения, в котором задаётся в качестве параметра некий файл образа и будет сохранён этот образ в виде объекта в каком- то кластере Ceph и запомнены различные атрибуты этого файла образа в виде атрибутов объекта. Данное приложение также позволит вам выбирать этот объект и экспортировать его как некий файл образа. Данный пример будет написан на Python, который также поддерживается librados. Приводимый далее пример также применяет Python Imaging Library (PIL) для чтения размера некоторого образа и Argument Parser library для чтения параметров командной строки:

  1. Вначале нам необходимо установить привязки librados Python и библиотеки манипуляции образом:

    
    $ sudo apt-get install python-rados python-imaging
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 4



  2. Создайте некий новый файл для своего приложения 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
    #Конец
     	   
  3. Проверим работоспособность help, создаваемую библиотекой синтаксического разбора аргументов:

    
    $ sudo python app1.py --help
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 5



  4. Выгрузим логотип Ceph для использования в качестве проверки на образе:

    
    wget http://docs.ceph.com/docs/master/_static/logo.png
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 6



  5. Выполните наше приложение Python для чтения некоторого файла образа и загрузки его в Ceph в качестве объекта:

    
    $ sudo python app1.py --action=upload --image-file=test1.png --object-name=image_test --pool=rbd --comment="Ceph Logo"
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 7



  6. Убедимся что данный объект был создан:

    
    $ sudo rados -p rbd ls
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 8



  7. Примените rados для проверки что данный атрибут был добавлен к данному объекту:

    
    $ sudo rados -p rbd listxattr image_test
     	   

    Предыдущая команда предоставит вам следующий вывод:

     

    Рисунок 9



  8. Воспользуйтесь rados для проверки содежимого данного атрибута, как это показано на следующем снимке экрана:

     

    Рисунок 10



Пример приложения librados с атомарными операциями

В предыдущем примере приложения librados был создан некий объект в нашем кластере Ceph и затем были добавлены необходимые атрибуты этого объекта. В большинстве случаев такие два этапа действий могут быть удовлетворительными, однако некоторые приложения могут потребовать чтобы создание самого объекта и его атрибута были атомарными. Иными словами, если возникло некоторое прерывание обслуживания, данный объект должен присутствовать только если установлены его атрибуты, в противном случае наш кластер Ceph должен откатить назад данную транзакцию. Следующий написанный на C++ пример показывает как применять атомарные операции librados чтобы гарантировать непротиворечивость транзакции для множества операций. Данный пример записывает некий объект и затем запрашивает своего пользователя если тот хочет прервать данную транзакцию. Если он выбирает прервать, тогда вся операция записи объекта будет откачена обратно. Если он выбирает продолжить, тогда все атрибуты будут записаны и вся транзакция целиком зафиксирована. Выполните следующие шаги:

  1. Создайте какой- то новый файл с расширением .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);
    }
     	   
  2. Компилируем данный исходный код при помощи g++:

    
    g++ atomic.cc -o atomic -lrados -std=c++11
     	   
  3. Теперь мы можем исполнить своё приложение. Вначале давайте исполним его и прервём имеющуюся транзакцию:

     

    Рисунок 11



    Предыдущий снимок экрана показывает, что даже хотя мы и отправили некую команду записи объекта, поскольку данная транзакция не была зафиксирована, данный объект на самом деле не был записан в наш кластер Ceph.

  4. Теперь мы выполним это приложение вновь, но в этот раз давайте продолжим эту транзакцию:

     

    Рисунок 12



    Как вы можете увидеть, на этот раз данный объект был записан вместе со своим атрибутом.

Пример приложения librados, которое использует наблюдателей и уведомителей

Следующее приложение librados написано на C и отображает как применять функциональность RADOS watch или notify. Ceph позволяет некому клиенту создавать наблюдателя некоторого объекта и получать уведомления от совершенно другого клиента, подключённого к тому же самому кластеру.

Функциональность наблюдателя реализуется посредством обратного вызова. Когда вы вызываете определённую функцию librados для создания такого наблюдателя, два имеющихся аргумента используются для функции обратного вызова, причём один служит того что делать при получении уведомления, а другой предназначен для того что выполнять если наблюдатель утрачивает контакт или сталкивается с ошибкой для данного объекта. Эти функции обратного вызова в свою очередь содержат тот код, который вы хотите исполнить при получении некого уведомления или возникновении ошибки.

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

Приводимый ниже пример создаёт наблюдателя некоторого объекта с названием my_object и затем ожидает. Когда он получает некое уведомление, он отобразит всю полезную нагрузку и затем отправит принятое сообщение обратно уведомителю.

  1. Создайте некий новый файл с расширением .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);
    }
     	   
  2. Компилируем данный код примера watcher:

    
    $ gcc watcher.c -o watcher -lrados
     	   
  3. Выполняем данное приложение примера watcher:

     

    Рисунок 13



  4. Наш наблюдатель теперь ожидает некого уведомления. В другом терминальном окне, воспользовавшись rados, отправим какое- то уведомление нашему наблюдаемому объекту my_object:

     

    Рисунок 14



  5. Вы можете видеть, что данное уведомление было отправлено и подтверждающее уведомление было обратно получено. Если мы посмотрим в окно первого терминала вновь, мы можем увидеть такое сообщение от уведомителя:

     

    Рисунок 15



Выводы

Это завершает нашу главу по разработке приложений с применением librados. Теперь вам должно быть удобно при наличии основных понятий того,как включать функциональность librados в своё приложение и как читать объекты и записывать их в своём кластере Ceph. Если вы собираетесь разрабатывать некое приложение с применением librados, следует также порекомендовать прочесть всю официальную документацию для получения дополнительных подробностей с тем, чтобы вы могли получить большее представление обо всём диапазоне доступных функций. {Прим. пер.: также см. Приложение A и Приложение B}.