вторник, 7 апреля 2015 г.

[prog.c++11] Состоялся релиз SObjectizer-5.5.4

Состоялся релиз версии 5.5.4. Архивы с исходными текстами и бинарниками для Windows доступны для скачивания.

В двух словах версия 5.5.4 добавляет несколько мелких и не очень возможностей:

  • вспомогательные шаблонные методы make_agent и make_agent_with_binder (аналоги make_shared и make_unique из C++11/14), упрощающие создание экземпляров объектов (описание в Wiki проекта);
  • приватные диспетчеры. Приватный диспетчер можно использовать только по прямой ссылке, которую получает только тот, кто создает такой диспетчер. Так же приватные диспетчеры автоматически уничтожаются, когда ими больше никто не пользуется (описание в Wiki проекта);
  • лимиты для сообщений, т.е. возможность ограничить количество сообщений конкретного типа в очереди заявок для агента, что позволяет реализовывать простую защиту агентов от перегрузок штатными средствами SObjectizer-а (описание в Wiki проекта);
  • новый тип context_t и дополнительный конструктор для базового типа agent_t, за счет чего упрощается создание агентов, использующих лимиты для сообщений, особенно в случае наследования агентов (описание в Wiki проекта);
  • простейший вариант сбора и распространения мониторинговой информации о том, что происходит внутри SObjectizer Run-Time. В первую очередь этот вариант предназначен для предоставления возможности сохранения мониторинговой информации посредством таких инструментов, как statsd+graphite, Zabbix, Nagios и т.д. (описание в Wiki проекта);
  • новые примеры, демонстрирующие возможности SObjectizer-5.5 (#1, #2, #3, #4).

Версия 5.5.4 может быть загружена из раздела Files или получена из Subversion-репозитория.

Отдельную благодарность хочется высказать Алексею Сырникову, как за помощь в подготовке этого релиза, так и за работы по созданию зеркала SObjectizer на GitHub-е.

Более подробная информация о релизе, включая информацию о доступных для скачивания дистрибутивах и более развернутое описание нововведений, находится под катом.

В Files для загрузки доступны следующие архивы:

  • so-5.5.4 (7z, zip) -- исходный текст ядра SObjectizer (включая тесты и примеры);
  • so-5.5.4--doc-html (7z, zip) -- сгенерированный посредством Doxygen API Reference Manual;
  • so-5.5.4--bin-msvs2013-x86 (7z, zip) -- исходные тексты и 32-битовые бинарники для Windows (скомпилированы посредством MS Visual Studio 2013 Express);
  • so-5.5.4--bin-msvs2013-x86_amd64 (7z, zip) -- исходные тексты и 64-битовые бинарники для Windows (скомпилированы посредством MS Visual Studio 2013 Express).

Теперь о нововведениях чуть-чуть подробнее.


Вспомогательные методы make_agent/make_agent_with_binder позволяют писать меньше кода при создании агентов. Если раньше нужно было явно вызывать new и передавать в конструктор агента SObjectizer Environment:

void a_parent_t::register_child_coop()
{
   auto coop = so_5::rt::create_child_coop( *this"child" );

   forsize_t i = 0; i != m_child_count; ++i )
   {
      coop->add_agent(
            new a_child_t(
                  so_environment(),
                  "a_child_" + std::to_string(i+1),
                  so_direct_mbox(), m_logger );
   }

   so_environment().register_coop( std::move( coop ) );
}

То сейчас можно использовать make_agent/make_agent_with_binder, которые играют ту же роль, что и функции make_shared/make_unique в C++11/14:

void a_parent_t::register_child_coop()
{
   auto coop = so_5::rt::create_child_coop( *this"child" );

   forsize_t i = 0; i != m_child_count; ++i )
   {
      coop->make_agent< a_child_t >(
            "a_child_" + std::to_string(i+1),
            so_direct_mbox(), m_logger );
   }

   so_environment().register_coop( std::move( coop ) );
}

Так что теперь появляется возможность писать SObjectizer-приложения, в которых динамически созданные объекты используются в полный рост, но в коде которых не будет ни одного явного вызова new или delete :)


До версии 5.5.4 все диспетчеры в рамках одного экземпляра SObjectizer Environment должны были быть именованными. Будучи однажды добавленным к SO Environment диспетчер оставался работать до окончания времени жизни Environment-а, даже если этим диспетчером никто больше не пользовался. Это осложняло работу с кооперациями, которые создаются для решения какой-то временной задачи, создают под себя свой собственный диспетчер (или даже группу диспетчеров), а затем дерегистрируются после завершения работы над задачей. Кооперации-то дерегистрировались, но вот созданные ими диспетчеры оставались в SO Environment, хотя никому эти диспетчеры уже не были нужны.

В версии 5.5.4 добавлена возможность создания диспетчеров, которые автоматически останавливаются и уничтожаются когда ими больше никто не пользуется. Как правило, такие диспетчеры создаются кооперациями для себя самих, про их диспетчеров никто не знает. Поэтому такие диспетчеры названы приватными или частными диспетчерами.

Все пять штатных диспетчеров SObjectizer сейчас предоставляют набор функций с именами create_private_disp. Эти функции возвращают динамически созданный экземпляр приватного диспетчера соответствующего типа. Т.е. функция so_5::disp::one_thread::create_private_disp() возвращает приватный диспетчер типа one_thread. А функция so_5::disp::adv_thread_pool::create_private_disp() -- приватный диспетчер типа adv_thread_pool.

Для привязки агентов к приватному диспетчеру нужно воспользоваться методом binder(), который есть у всех приватных диспетчеров:

void
init( so_5::rt::environment_t & env )
{
   // При создании кооперации дополнительных указаний по связыванию
   // агентов с диспетчерами не дается, поэтому основным диспетчером
   // для кооперации будет диспетчер по умолчанию.
   auto coop = env.create_coop( so_5::autoname );

   // Агент-logger будет привязан к диспетчеру по умолчанию.
   auto logger = coop->make_agent< a_logger_t >();

   // Для агента stats_listener будет задействован отдельный
   // приватный диспетчер с одной рабочей нитью. Только этот
   // агент будет работать на этой нити.
   // Т.к. диспетчер используется всего один раз, результат
   // функции create_private_disp даже не нужно нигде сохранять.
   coop->make_agent_with_binder< a_stats_listener_t >(
         so_5::disp::one_thread::create_private_disp(
               env, "stats_listener" )->binder(),
         logger->so_direct_mbox() );

   // Группа агентов-worker-ов нуждается в собственном thread_pool-диспетчере.
   // Т.к. ссылка на этот диспетчер потребуется чуть позже, она
   // сохраняется в промежуточной переменной.
   auto worker_disp = so_5::disp::thread_pool::create_private_disp(
         env,
         3// Количество рабочих нитей для агентов-worker-ов.
         "workers" ); // Имя диспетчера для упрощения мониторинга.
   // Для привязки агентов потребуются дополнительные параметры, поэтому
   // описание этих параметров так же сохраняется в виде промежуточной
   // переменной.
   const auto worker_binding_params = so_5::disp::thread_pool::params_t{}
         .fifo( so_5::disp::thread_pool::fifo_t::individual );

   std::vector< so_5::rt::mbox_t > workers;
   forint i = 0; i != 5; ++i )
   {
      auto w = coop->make_agent_with_binder< a_worker_t >(
            // Каждый агент-worker привязывается к приватному
            // thread_pool-диспетчеру.
            worker_disp->binder( worker_binding_params ) );
      workers.push_back( w->so_direct_mbox() );
   }

   coop->make_agent_with_binder< a_generator_t >(
         // Агент-generator привязывается к своему собственному
         // диспетчеру типа active_obj.
         so_5::disp::active_obj::create_private_disp( env, "generator" )->binder(),
         logger->so_direct_mbox(),
         workers );

   env.register_coop( std::move( coop ) );
}

Лимиты сообщений позволяют агентам указать, какое максимальное количество сообщений конкретного типа может ожидать обработки в очереди заявок для данного агента. А так же что делать в случае, когда происходит превышение лимита, т.е. когда появляется "лишнее сообщение".

Например, агент-logger может указать, что у него в очереди может быть только 100 сообщений log_message, а все остальное должно просто выбрасываться. Агент-http_request_processor может ограничить количество сообщений new_request, скажем, 50-ью штуками, указав, что все остальные должны сразу же преобразовываться в отрицательный ответ service_busy.

Лимиты для сообщений опциональны. Т.е. агентам вовсе не обязательно их определять. Если лимиты не заданы, то SObjectizer не ведет учет количества сообщений для агента. Но, если агент указал лимиты, то SObjectizer начинает их контролировать и предпринимать соответствующие действия при превышении лимитов.

На данный момент доступно четыре типа реакции на превышение лимита:

  • просто выбросить лишнее сообщение, как будто его вообще не было. Т.к. взаимодействие агентов, в подавляющем большинстве случаев, строится на обмене асинхронными сообщениями, доставка которых не гарантируется, этот тип реакции наиболее простой, понятный и эффективный. Агентные системы должны быть рассчитаны на работу в условиях возможной потери единичных сообщений, не важно по каким причинам. Выбрасывание сообщения из-за перегрузки получателя в этом случае принципиальной роли не играет;
  • переслать лишнее сообщение другому получателю. Эта реакция выгодна, если логика задачи допускает перераспределение нагрузки между несколькими агентами. Например, приложение может работать с двумя HSM-ами -- дорогим, быстрым и мощным, на котором обрабатывается подавляющее число криптографических операций, и старым, медленным, резервным, который можно задействовать при пиковых всплесках нагрузки. В этом случае агент для работы с быстрым HSM-ом может настроить переадресацию лишних сообщений на агента для работы с медленным HSM-ом;
  • преобразовать лишнее сообщение в какое-то другое и отослать новое сообщение на некоторый mbox. Эта реакция выгодна, когда в случае превышения нагрузки на агента можно применять упрощенную обработку новых сообщений. Например, агент для обслуживания HTTP-запросов в случае перегрузки сразу отвечает сообщением о том, что он перегружен и обработать новый запрос не в состоянии;
  • аварийно прервать работу приложения посредством std::abort(). Самый крайний случай, но иногда дела могут идти так, что ничего больше не поделать. Например, при работе с внешними устройствами или даже при работе со сторонними библиотеками иногда возникают ситуации, когда синхронный вызов (обращение к устройству или к сторонней библиотеке) блокируется на слишком долгое время. Например, происходит какой-то сбой в оборудовании или же в чужой библиотеке происходит зависание из-за бага (бесконечный цикл, деадлок и т.д.). В таких ситуациях самым простым выходом может быть быстрый крах через вызов std::abort() с последующим рестартом с помощью какого-то супервизора.

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


После того, как в версии 5.5.3 появилось понятие tuning_options для агентов, а в версии 5.5.4 через tuning_options задаются лимиты для сообщений, встал вопрос, как упростить работу с tuning_options при наследовании агентов?

Выход был найден в новом типе context_t и дополнительном конструкторе для базового класса agent_t. Эти две штуки позволяют писать вот так:

class my_agent_with_many_subscriptions : public so_5::rt::agent_t
{
public :
    // Конструктор для агента, который хочет использовать специальный
    // тип контейнера для хранения большого количества подписок.
    my_agent_with_many_subscriptions(
        context_t ctx,
        some_agent_specific_params params )
        :   // В конструктор базового типа передается уточненный
            // контекст, в котором указан тип хранилища для подписок.
            so_5::rt::agent_t(
                // Агенту нужно хранилище для очень большого количества подписок.
                ctx + so_5::rt::hash_table_based_subscription_factory() )
        ,    ...
        {...}
    ...
};

// Производный агент, который еще и назначает лимиты для сообщений.
class my_agent_with_limits : public my_agent_with_many_subscriptions
{
public :
    my_agent_with_limits(
        context_t ctx,
        some_agent_specific_params params )
        :   my_agent_with_many_subscriptions(
                // В базовый класс пойдет уточненный контекст, в котором
                // заданы лимиты. А базовый класс уточнит этот контекст еще и
                // соответствующим типом хранилища подписок.
                ctx + limit_then_drop< request_one >( 100 )
                    + limit_then_drop< request_two >( 100 )
                    + ...,
                params )
        {...}
    ...
};

Новые шаблонные методы make_agent/make_agent_with_binder могут работать как с агентами, конструкторы которых первым аргументом получают ссылку на so_5::rt::environment_t, так и экземпляр context_t.

Учитывая все вышесказанное, начиная с версии 5.5.4 в конструкторах агентов рекомендуется использовать именно context_t, а не ссылку на environment_t.


До версии 5.5.4 SObjectizer Run-Time был абсолютным "черным ящиком", получение информации о ходе работы которого не было возможным в принципе. Т.е. невозможно было узнать, сколько агентов зарегистрировано в данный момент, сколько отложенных и периодических сообщений ждут на таймерной нити, сколько заявок находится в очередях и т.д.

В принципе, это не представляло большой проблемы, т.к. в моих прошлых проектах на базе SObjectizer-а было много источников данных, снимающих более актуальные для прикладной задачи параметры. Например, количество находящихся в обработке прикладных транзакций, времена выполнения операций с БД, времена отклика сторонних систем и т.д.

Однако, если есть цель собирать для последующего анализа максимум полезной статистики, то показатели из внутренностей SObjectizer Run-Time явно окажутся не лишними. Так, если при анализе следов работы приложения выяснится, что замедление обработки транзакций совпадает с ростом количества коопераций/заявок в очередях/отложенных сообщений, то это даст разработчикам гораздо большее поле для поиска причин возникновения проблем и путей их обнаружения.

Именно для того, чтобы из длительно работающего SObjectizer-приложения можно было снимать мониторинговую информацию и передавать ее в инструменты вроде statsd+graphite/Zabbiz/Nagios и т.д., в версии 5.5.4 была добавлена возможность приема мониторинговой информации из внутренностей SObjectizer-а.

Для этого разработчику нужно подписаться на сообщение quantity<std::size_t>, приходящего из специального mbox-а и включить run-time мониторинг. После чего агенту будут приходить сообщения от всех источников данных, собирающих информацию внутри SObjectizer Run-Time. Разработчик может далее отфильтровывать те сообщения, которые его интересуют, и делать с полученной информацией то, что ему требуется.

Например, вот такой агент получает и логирует информацию о текущих размерах очередей сообщений:

class a_stats_listener_t : public so_5::rt::agent_t
{
public :
   a_stats_listener_t(
      // Контекст, на котором будет работать агент.
      context_t ctx )
      :  so_5::rt::agent_t( ctx )
   {}

   virtual void
   so_define_agent() override
   {
      // Подписываемся на сообщения от run-time мониторинга.
      so_default_state().event(
            // Сообщения с мониторинговой информацей будут отсылаться
            // в этот почтовый ящик.
            so_environment().stats_controller().mbox(),
            &a_stats_listener_t::evt_quantity );
   }

   virtual void
   so_evt_start() override
   {
      // Устанавливаем скорость с которой хотим получать обновления
      // мониторинговой информации. Приблизительно три раза в секунду.
      so_environment().stats_controller().set_distribution_period(
            std::chrono::milliseconds( 330 ) );
      // Запускаем мониторинг. По умолчанию он выключен и SObjectizer
      // почти не тратит ресурсов на сбор статистики. Поэтому мониторинг
      // нужно включать явным образом.
      so_environment().stats_controller().turn_on();
   }

private :
   const so_5::rt::mbox_t m_logger;

   void
   evt_quantity(
      const so_5::rt::stats::messages::quantity< std::size_t > & evt )
   {
      // Берем в обработку только те сообщения, которые содержат информацию
      // о размерах очередей заявок.
      if( so_5::rt::stats::suffixes::work_thread_queue_size() == evt.m_suffix )
      {
         // Делаем текстовое описание полученной информации.
         // prefix+suffix дают полное имя приславшего информацию
         // источника данных, а value содержит его текущие показания.
         std::cout << "stats: '" << evt.m_prefix << evt.m_suffix << "': "
               << evt.m_value << std::endl;
      }
   }
};

В версии 5.5.4 реализован самый простой, базовый уровень предоставления мониторинговой информации. Есть планы развить эту тему в будущих версиях (например, ввести несколько уровней, на старших и самых дорогих из которых будут отслеживаться времена обработки сообщений, времена нахождения сообщений в очередях и т.д.). Так что здесь еще есть огромное поле для работы, а если сюда добавятся еще и конкретные пожелания из конкретных проектов, то можно будет сделать полезный для многих вещей инструментарий наблюдения и контроля за внутренностями SObjectizer Run-Time.

Комментариев нет: