Глава 7. Распределённые вычисления при помощи классов RADOS Ceph

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

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

  • Примеры приложений и преимущества применения классов RADOS

  • Написание некоторого простого класса RADOS в Lua

  • Написание некоторого класса RADOS, который эмулирует распределённое вычисление

Примеры приложений и преимущества использования классов RADOS

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

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

Написание примера класса RADOS в Lua

Одним из имеющихся по умолчанию в Ceph классов Rados, начиная с выпуска Kraken и далее, является класс, который способен исполнять сценарии Lua. Сценарий Lua динамически передаётся в имеющийся класс объекта RADOS Lua, который затем исполняет всё содержимое данного сценария. Такие сценарии обычно передаются в формате строк JSON в имеющийся класс объекта. Хотя это и привносит преимущества над имеющимися обычно классами объектов RADOS, которые должны быть скомпилированы перед их применением, это также ограничивает доступную сложность того, что данные сценарии Lua могут выполнять и, следовательно, стоит обдумать какой метод подходит для той задачи, которую вы желаете выполнять.

Следующий код Python демонстрирует как создать и передать некий подлежащий исполнению в каком- то OSD сценарий Lua. Данный сценарий Lua считывает всё содержимое данного определённого объекта и возвращает данную строку текста обратно заглавными буквами, причём вся обработка выполняется в данном удалённом OSD, который содержит данный объект и никогда не отправляется самому клиенту.

Поместите приведённый ниже код в некий файл с названием rados_lua.py:


import rados, json, sys

try: #Считываем файл настроек ceph.conf для получения мониторов
  cluster = rados.Rados(conffile='/etc/ceph/ceph.conf')
except:
  print "Ошибка чтения настроек Ceph"
  exit(1)

try: #Соединяемся с кластером Ceph 
  cluster.connect()
  except:
  print "Ошибка соединения с кластером Ceph "
  exit(1)

try: #Открываем определённый пул RADOS 
  ioctx = cluster.open_ioctx("rbd")
  except:
  print "Ошибка открытия пула"
  cluster.shutdown()
  exit(1)

cmd = {
  "script": """
      function upper(input, output)
        size = objclass.stat()
        data = objclass.read(0, size)
        upper_str = string.upper(data:str())
        output:append(upper_str)
      end
      objclass.register(upper)
      """,
      "handler": "upper",
} 

ret, data = ioctx.execute(str(sys.argv[1]), 'lua', 'eval_json', json.dumps(cmd))
print data[:ret]

ioctx.close() #Закрываем соединение с пулом
cluster.shutdown() #Закрываем соединение с Ceph
 	   

Теперь давайте создадим некий объект для проверки со всеми строчными символами:


echo this string was in lowercase | sudo rados -p rbd put LowerObject –
 	   

По умолчанию имеющемуся классу объекта Lua не разрешается исполнение в OSD; нам необходимо добавить следующее во все свои OSD в их ceph.conf:


[osd]
osd class load list = *
osd class default list = *
 	   

А теперь выполним наше приложение librados Python:


sudo python rados_lua.py LowerObject
 	   

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

 

Рисунок 1



Вы должны увидеть, что весь текст из нашего объекта был преобразован в прописные буквы. Вы можете увидеть из приведённого ранее кода Python, что мы не делаем никакого преобразования в самом локальном коде Python и что всё выполняется удалённо в OSD.

Написание класса RADOS, который эмулирует распределённые вычисления

Как упоминалось в нашем приведённом ранее примере, хотя применение имеющегося класса объектов Lua снижает сложность использования класса объекта RADOS, имеется некое ограничение на то, что вы можете в настоящее время получить. Чтобы написать некий класс, который способен выполнять более продвинутую обработку, нам необходимо перейти на нижний уровень напсиания необходимого класса на C. Затем нам понадобится встроить этот новый класс в имеющийся исходный код Ceph.

Для демонстрации этой возможности мы напишем некий новый класс объекта RADOS, который будет вычислять хэш MD5 определённого имеющегося объекта и затем сохранять его в некотором атрибуте этого объекта. Данный процесс будет повторён 1000 раз для эмуляции некоторого занятого окружения, а также для более простого измерения времени исполнения. Затем мы сравним общую скорость обработки выполнения этого через данный класс объектов в сопоставлении с вычислением значения хэшей MD5 на самом клиенте. Хотя это всё ещё достаточно базовая задача, она также позволит нам воспроизвести некий управляемый повторяемый сценарий и позволит нам сопоставить общую скорость выполнения некоторой задачи клиентской стороны и прямого выполнения этого на имеющихся OSD при помощи класса RADOS. Это также послужит хорошей основой чтобы позволить понять как строить более продвинутые решения.

Подготовка к построению среды

Для клонирования имеющегося репозитория git Ceph воспользуйтесь следующей командой:


git clone https://github.com/ceph/ceph.git
 	   

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

 

Рисунок 2



Когда у нас имеется клонированный репозиторий git Ceph, нам понадобится изменить его файл CMakeLists.txt и добавить раздел для нашего нового класса, который мы собираемся написать.

Измените следующий файл в дереве исходного кода:


~/ceph/src/cls/CMakeLists.txt
 	   

Также поместите в этот файл следующее:


# cls_md5
set(cls_md5_srcs md5/cls_md5.cc)
add_library(cls_md5 SHARED ${cls_md5_srcs})
set_target_properties(cls_md5 PROPERTIES
  VERSION "1.0.0"
  SOVERSION "1"
  INSTALL_RPATH "")
install(TARGETS cls_md5 DESTINATION ${cls_dir})
target_link_libraries(cls_md5 crypto)
list(APPEND cls_embedded_srcs ${cls_md5_srcs})
 	   

Когда файл CMakeLists.txt обновлён, мы можем взять cmake чтобы собрать необходимое построение среды выполнив приведённую ниже команду:


do_cmake.sh
 	   

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

 

Рисунок 3



Это создаст некий каталог build в имеющемся дереве исходного кода.

Для того чтобы нам построить класс RADOS, нам необходимо установить все требующиеся пакеты, которые содержит данная команда make:


sudo apt-get install build-essentials
 	   

Также в нашем дереве исходного кода Ceph имеется некий файл install-deps.sh, который установит все остальные необходимые пакеты при своём исполнении.

Класс RADOS

приводимый ниже пример кода является неким классом RADOS, который при исполнении чтений определённого объекта вычисляет его хэш MD5 и затем записывает его в виде некоторого атрибута в сам объект без какого бы то ни было вовлечения клиента. Всякий раз, когда вызывается этот класс, он повторяет данную операцию 1000 раз локально для OSD и уведомляет своего клиента только по окончанию данной обработки. Нам предстоит выполнить следующие этапы:

  1. Создадим для своего нового класса определённый каталог:

    
    mkdir ~/ceph/src/cls/md5
     	   
  2. Теперь создадим необходимый исходный файл C++:

    
    ~/ceph/src/cls/md5/cls_md5.cc
     	   
  3. Поместим в него следующий код:

    
    #include "objclass/objclass.h"
    #include <openssl/md5.h>
    
    CLS_VER(1,0)
    CLS_NAME(md5)
    
    cls_handle_t h_class;
    cls_method_handle_t h_calc_md5;
    
    static int calc_md5(cls_method_context_t hctx, bufferlist *in,
    bufferlist *out)
    {
      char md5string[33];
      for(int i = 0; i < 1000; ++i)
      {
        size_t size;
        int ret = cls_cxx_stat(hctx, &size, NULL);
        if (ret < 0)
          return ret;
    
        bufferlist data;
        ret = cls_cxx_read(hctx, 0, size, &data);
        if (ret < 0)
          return ret;
        unsigned char md5out[16];
        MD5((unsigned char*)data.c_str(), data.length(), md5out);
        for(int i = 0; i < 16; ++i)
          sprintf(&md5string[i*2], "%02x", (unsigned int)md5out[i]);
        CLS_LOG(0,"Loop:%d - %s",i,md5string);
        bufferlist attrbl;
        attrbl.append(md5string);
        ret = cls_cxx_setxattr(hctx, "MD5", &attrbl);
        if (ret < 0)
        {
          CLS_LOG(0, "Error setting attribute");
          return ret;
        }
      }
      out->append((const char*)md5string, sizeof(md5string));
      return 0;
    }
    
    v
    oid __cls_init()
    {
      CLS_LOG(0, "loading cls_md5");
      cls_register("md5", &h_class);
      cls_register_cxx_method(h_class, "calc_md5", CLS_METHOD_RD |
      CLS_METHOD_WR, calc_md5, &h_calc_md5)
    }
     	   
  4. Перейдите в свой ранее созданный каталог build и создайте наш новый класс RADOS при помощи make:

    
    cd ~/ceph/build
    make cls_md5
     	   

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

     

    Рисунок 4



  5. Теперь нам нужно скопировать наш новый класс в OSD в своём кластере:

    
    udo scp vagrant@ansible:/home/vagrant/ceph/build/lib/libcls_md5.so* /usr/lib/rados-classes/
     	   

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

     

    Рисунок 5



    Также перезапустите все OSD чтобы они загрузили данный класс.

    Вы теперь увидите в имеющемся журнале OSD что наш новый класс загружен:

     

    Рисунок 6



    Это необходимо повторить для всех узлов в данном кластере.

Клиентские приложения librados

Как уже упоминалось ранее, мы будем применять дав приложения librados, одно будет вычислять все хэши MD5 напрямую в нашем клиенте, а другое вызывать наш класс RADOS и заставлять его вычислять необходимый хэш MD5. Оба эти приложения необходимо исполнить из узла монитора в тестовом кластере, однако могут быть скомпилированы в любом узле и скопированы оттуда, если хотите. Для целей данного примера мы скомпилируем все приложения прямо в имеющихся узлах монитора.

Прежде чем мы приступим, давайте убедимся что построенная среда представлена в данном узле монитора:


apt-get install build-essential librados-dev
 	   

Вычисление MD5 в самом клиенте

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

Создайте некий новый файл rados_md5.cc и поместите в него следующее:


#include <cctype>
#include <rados/librados.hpp>
#include <iostream>
#include <string>
#include <openssl/md5.h>

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_name("LowerObject");
  librados::IoCtx io_ctx;
  // Создаём необходимый нам объект Rados и инициализируем его
  {
  ret = rados.init("admin"); // Применяем определённое по умолчанию кольцо ключей client.admin 
  if (ret < 0) {
    std::cerr << "Отказ в инициализации rados! ошибка " << ret 
	          << std::endl;
    ret = EXIT_FAILURE;
  }
} 

// Считываем файл настроек ceph в его определённом по умолчании местоположении
ret = rados.conf_read_file("/etc/ceph/ceph.conf");
if (ret < 0) {
  std::cerr << "Отказ в синтаксическом разборе файла config "
            << "! Ошибка" << 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;
}
for(int i = 0; i < 1000; ++i)
  {
    size_t size;
    int ret = io_ctx.stat(object_name, &size, NULL);
    if (ret < 0)
      return ret;

    librados::bufferlist data;
    ret = io_ctx.read(object_name, data, size, 0);
    if (ret < 0)
      return ret;
    unsigned char md5out[16];
    MD5((unsigned char*)data.c_str(), data.length(), md5out);
    char md5string[33];
    for(int i = 0; i < 16; ++i)
      sprintf(&md5string[i*2], "%02x", (unsigned int)md5out[i]);
    librados::bufferlist attrbl;
    attrbl.append(md5string);
    ret = io_ctx.setxattr(object_name, "MD5", attrbl);
    if (ret < 0)
    {
      exit_func(1);
    }
  }
  exit_func(0);
}

void exit_func(int ret)
{
  // Очистка и выход
  rados.shutdown();
  exit(ret);
}
 	   

Вычисление MD5 в OSD через класс RADOS

Наконец, самый последний пример кода является тем приложением librados, которое инструктирует OSD вычислять необходимый хэш MD5 локально без передачи каких- либо данных к- или от- данного клиента. Вы отметите, что данный представленный ниже код не имеет операторов чтения или записи librados и полностью полагается на имеющуюся функцию exec для включения создания необходимого хэша MD5.

Создайте некий новый файл rados_class_md5.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_name("LowerObject");
  librados::IoCtx io_ctx;
  // Создаём необходимый объект Rados и инициализируем его
  {
    ret = rados.init("admin"); // Применяем определённое по умолчанию кольцо ключей client.admin
    if (ret < 0) {
      std::cerr << "Отказ в инициализации rados! ошибка " << ret 
                << std::endl;
      ret = EXIT_FAILURE;
    }
  }

  // Считываем файл настроек ceph config в его определённом по умолчанию местоположении
  ret = rados.conf_read_file("/etc/ceph/ceph.conf");
  if (ret < 0) {
    std::cerr << "Отказ в синтаксическом разборе файла config "
              << "! Ошибка" << 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 in, out;
  io_ctx.exec(object_name, "md5", "calc_md5", in, out);
  exit_func(0);
}

void exit_func(int ret)
{
  // Очистка и выход
  rados.shutdown();
  exit(ret);
}
 	   

Теперь мы можем оттранслировать оба приложения:

 

Рисунок 7



В случае успешной трансляции вывод будет отсутствовать.

Проверка

Мы выполним два имеющихся приложения librados с применением стандартной утилиты Linux time для измерения того, как долго будет исполняться каждое:


time sudo ./rados_md5
 	   

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

 

Рисунок 8



Давайте убедимся что данный атрибут был в действительности создан:


time sudo ./rados_md5
 	   

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

 

Рисунок 9



Давайте удалим этот атрибут с тем, чтобы мы могли иметь уверенность что данный класс RADOS правильно создаёт его при своём исполнении:


sudo rados -p rbd rmxattr LowerObject MD5
 	   

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


time sudo ./rados_class_md5
 	   

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

 

Рисунок 10



Как вы можете отметить, применение метода класса RADOS намного быстрее, фактически на два порядка в своём абсолютном значении.

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


sudo rados -p rbd getxattr LowerObject MD5
 	   

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

 

Рисунок 11



Благодаря вставленному в наш класс RADOS ведению протоколирования, мы также можем проверить журналы OSD чтобы убедиться что данный класс RADOS на самом деле выполнился тысячу раз:

 

Рисунок 12



При повторе малых задач на самом деле добавляется существенное переполнение взаимодействием между самим клиентом и OSD. Удаляя процесс напрямую в OSD мы можем исключить его.

Возражения против класса RADOS

Хотя мы и увидели всю мощность которая может быть покорена с применением классов RADOS, важно отметить, что она достигается именно благодаря вашему персональному коду из внутренних глубин OSD. Вследствие этого следует проявлять максимальную осторожность в отношении отсутствия ошибок в вашем классе RADOS. Класс RADOS имеет возможность изменять любые данные в вашем кластере Ceph, и поэтому возможно случайное повреждение данных. Также есть вероятность что такой класс RADOS может привести к крушению самого процесса OSD. Если данный класс применяется в операциях кластера большого масштаба, он имеет возможность воздействия на все OSD в данном кластере и, следовательно, необходимо следить за тщательной обработкой ошибок во избежание сбоев.

Выводы

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

В своей следующей главе мы воспользуемся некоторыми примерами изучения важности мониторинга в Ceph.

Вопросы

  1. В каких компонентах Ceph вычисляются классы RADOS?

  2. В каких языках программирования можно писать классы RADOS?

  3. Какие преимущества привносят классы RADOS?

  4. Какими недостатками обладают классы RADOS?