Агенты-близнецы
Author: |
Евгений Охотников |
Contact: |
eao197 at intervale dot ru; eao197 at yahoo dot com |
Version: |
0.1 |
Date: |
2007.11.25 |
Данный текст появился в результате пересечения двух идей:
- о том, что диспетчеров, которые обеспечивают работу всех пассивных агентов
на контексте всего одной рабочей нити уже недостатчно. Например, такими
диспетчерами являются шатные диспетчеры с одной рабочей нитью и диспетчер с
активными агентами. На многоядерных/многопроцессорных
машинах можно было бы выделить пассивным агентами N нитей (например, по
числу ядер/процессоров) и распределять пассивных агентов по этим нитям (но
гарантировать, что один агент не сможет одновременно работать сразу на более
чем одной нити);
- соображений Дмитрия Вьюкова, высказанных в списке рассылки SObjectizer о
том, что в приложении может существовать несколько ключевых агентов,
которые обрабатывают значительную часть трафика сообщений в приложении.
Например, агент, который владеет списком активных транзакции и к которому
нужно обратиться при начале каждой новой транзакции. При плотном потоке
транзакций этот агент может стать узким местом, поскольку транзакции не
смогут обрабатываться до получения ответа от этого агента. В условиях
многоядерности/многопроцессорности можно было бы попытаться избавиться от
данного узкого места, если бы удалось разрешить подобным агентам работать
сразу на нескольких нитях.
В текущей версии SObjectizer 4.4.0 многопоточную работу агента можно
обеспечить только за счет преобразования его событий в insend-события. Такие
события гарантированно вызываются внутри so_4::api::send_msg() на
контексте той нити, которая обращается к send_msg(). Это приведет к тому,
что если несколько агентов на своих нитях отсылают сообщения ключевому агенту,
то события ключевого агента стартуют параллельно на контексте каждого
инициатора сообщения.
У решения с insend-событиями есть свои минусы:
- оно будет замечательно работать, если ключевому агенту в своих событиях не
придется модифицировать свои внутренние данные. Т.е. если все запросы к
ключевому агенту будут только на чтение. В этом случае действительно
возможно параллельное исполнение события агента на нескольких нитях. Однако,
ситуация серьезно ухудшается, если данные ключевого агента могут
модифицироваться. В этом случае ключевому агенту придется защищать их при
помощь mutex-ов. Но тогда, во-первых, в ключевом агенте появляется ручное
управление синхронизацией, а SObjectizer ценен тем, что он в значительной
степени освобождает программиста от этой работы. В частности, синхронизация
внутри insend-событий не видна прикладному программисту, поэтому она,
потенциально, может стать причиной тупиков и взаимных блокировок, обнаружить
и устранить которые будет очень сложно. И, во-вторых, синхронизация внутри
insend-события сводит на нет идею запуска события на разных нитях -- ведь
большинство из них будут заблокированны операцией синхронизации;
- если обработчики событий ключевых агентов окажутся достаточно ресурсоемкими,
то время их работы может негативно сказаться на работе агентов-инициаторов.
Действительно, агенты-инициаторы расчитаны на то, что они отсылают
сообщение, которое где-то и когда-то обработается, а инициаторы могут сразу
же продолжать свою работу. Но тут вмешивается ключевой агент со своим
insend-событием, который прерывает работу агент-инициаторов на значительное
время, тем самым приостанавливая всех агентов, разделяющих рабочие нити с
агентоми-инциаторами.
Примечание. Данное предложение является всего лишь первым шагом в решении
описанной выше задачи. Здесь обозначенны лишь общие наметки предполагаемого
подхода. Поэтому с течением времени возможен серьезный пересмотр и переработка
изложенных ниже идей.
Суть предлагаемого подхода в том, чтобы позволить регистрировать в SObjectizer
нескольких одинаковых агентов под одним именем. При этом агенты должны
быть полностью одинаковыми -- принадлежать одному типу и выполнять
одинаковую подписку.
Каждому из агентов-близнецов SObjectizer выделяет собственный контекст. При
этом предполагается, что контекст для близнецов может предоставлять
специальный диспетчер, обеспечивающий пассивным агентам не одну, а N рабочих
нитей.
Когда агентам-близнецам по их общему имени отсылается сообщение, то возможны
следующие результаты:
- сообщение доставляется на обработку только одному из близнецов. Остальные
агенты-близнецы даже не знают про существование этого сообщения;
- сообщение доставляется сразу всем близнецам.
Управлять способами доставки сообщений агентам-близнецам должен специальный
объект-балансировщик. Этот объект реализуется разработчиком агентов-близнецов
в соответствии с логикой работы близнецов.
Библиотека MBAPI содержит
специального агента-маршрутизатора mbapi-сообщений. Этот агент отвечает за:
- контроль коммуникационных каналов. Как только создается новый
коммуникационный канал агент-маршрутизатор обязан отослать в него информацию
о локальных mbox-ах приложения. В ответ из канала приходит информация
об mbox-ах удаленной стороны. На основании ответа из канала
агент-маршрутизатор модифицирует свою таблицу маршрутизации, добавляя в нее
имена удаленных mbox-ов и идентификатор канала. При закрытии какого-либо
канала агент-маршрутизатор вычеркивает из таблицы имена ставших недоступными
mbox-ов;
- сериализацию и отправку в коммуникационные каналы mbapi-сообщений.
Агент-маршрутизатор получает mbapi-сообщение, определяет, на какой mbox оно
адресовано, выбирает канал, через который будет идти доставка, сериализует
mbapi-сообщение в обычное сообщение специального глобального агента и
отсылает полученное результирующее сообщение в нужный канал.
Как раз работу по сериализации и отсылке результирующих сообщений в
коммуникационные каналы могут выполнять агенты-близнецы. Для этого нужно,
чтобы объект-балансировщик доставлял им исходные mbapi-сообщение поочереди.
Аналогичная ситуация и с сообщением msg_client_connected с информацией о
появлении очередного коммуникационного канала. Его достаточно доставить любому
из близнецов, чтобы он отослал в канал список локальных mbox-ов.
Однако, такие сообщения, как msg_client_disconnected (о закрытии
коммуникационного канала), список удаленных mbox-ов или сообщение о
появлении/исчезновении локального mbox-а, должны доставляться сразу всем
агентам-близнецам. Тогда все они смогут поддерживать одинаковые копии таблиц
маршрутизации, хотя это и будут собственные копии каждого агента-близнеца.
Если приложение серьезно использует криптографические механизмы, особенно на
основе ассиметричной криптографии, то для выполнения ряда криптографических
операций может быть выделен специальный агент, в задачи которого может
входить:
- проверка криптографической подписи;
- формирование криптографической подписи;
- шифрование и дешифрация данных.
При большом потоке запросов к этому агенту он легко может стать узким местом.
Поэтому такой криптографический агент может быть заменен несколькими
агентами-близнецами, нагрузка на которых будет балансироваться.
В случае использования специализированных криптоустройств, подключаемых тем
или иным образом к компьютеру (SmartCard, HSM, крипто-платы), количество
криптографических агентов-близнецов может выбираться исходя из количества
доступных криптоустройств, а не из количества ядер/процессоров.
Ниже приводятся некоторые предварительные соображения о том, как работа с
агентами-близнецами может выглядеть на уровне программного кода.
При регистрации агентов-близнецов SObjectizer нужно будет передать объект,
реализующий следующий интерфейс:
class twin_agents_controller_t
{
public :
//! Создание нужного количества агентов-близнецов.
/*!
* Может быть создано любое их количество.
* Аргумент desired_count задает только оптимальное с
* точки зрения диспетчера число.
*/
virtual std::vector< so_4::rt::agent_t * >
create(
//! Общее имя для создаваемых агентов.
const std::string & common_name,
//! Оптимальное с точки зрения диспетчера число агентов-близнецов.
unsigned int desired_count ) = 0;
//! Выбор получателя очередного сообщения.
/*!
* Этот метод вызывается при получении сообщения, которое
* адресуется кому-то из агентов-близнецов.
*
* Данный метод отвечает за выбор конкретного агента, который будет
* обрабатывать данное сообщение. Метод может выбрать любого агента
* или всех агентов сразу.
*/
virtual std::vector< so_4::rt::agent_t * >
select_message_target(
//! Описание сообщения.
const message_routing_info_t & message,
//! Список агентов-близнецов, из которых нужно делать выбор.
const std::vector< so_4::rt::agent_t * > & twins_list ) = 0;
};
При этом SObjectizer будет поступать с агентами-близнецами следующим образом.
- При регистрации агентов-близнецов SObjectizer использует экземпляр
twin_agents_controller_t в качестве фабрики. Метод create() создает
экземпляры агентов, которые SObjectizer регистрирует под одним общим
именем. Для каждого агента запускается метод so_on_subscription(),
отрабатывает evt_start() и т.д. При этом каждый агент-близнец думает,
что он единственный агент в системе, имеющий данное общее имя.
- При отсылке сообщения на общее имя агентов-близнецов SObjectizer использует
экземпляр twin_agents_controller_t для выбора агента-получателя. Для
этого вызывается метод select_message_target и сообщение доставляется
тем агентам, указатели на которых оказались в возвращенном
select_message_target векторе.
Подобный подход обеспечивает прозрачность агентов-близнецов для:
- самих агентов-близнецов. Они должны быть написаны так, чтобы каждый из них
думал, что он единственный агент в системе. Что позволит легко изменять
их количество в зависимости от конкретных условий (например, от числа
доступных ядер/процессоров);
- прикладного кода, который отсылает агентам-близнецам сообщения. Снаружи
видно только одно общее имя агента и используются штатные функции
so_4::api::send_msg(). Поэтому изменение числа агентов-близнецов
или же полный отказ от агентов-близнецов не сказывается на остальных
частях прикладной системы.
Единственное место, которое знает детали работы агентов-близнецов -- это место
создания twin_agents_controller_t и регистрация агентов-близнецов в
SObjectizer.