четверг, 16 июля 2015 г.

[prog.thoughts] Про message-passing в схемах 1-to-1 и 1-to-many

Когда акторы обмениваются сообщениями, ключевыми моментами являются схема идентификации получателей/отправителей сообщений и политики доставки. Поскольку схема 1-to-1, т.е. когда сообщение отправляется единственному конкретному получателю -- это одно. А схема 1-to-many, т.е. когда сообщение отсылается, а получателей может быть много (или может не быть вообще) -- это совсем другое. Причем дело здесь даже не низкоуровневых механизмах доставки сообщений, сколько в том, как акторы будут реализовывать свою прикладную логику.

В SCADA Objectizer и затем в SObjectizer-4 использовалась достаточно простая схема. Каждый агент имел свою уникальное имя. Например, "Floor1.Sensor4", "weighing-machine::sourcer-10" или "smsc_map::router". Каждое сообщение, которое принадлежало агенту, так же имело собственное имя. Например, "current_value", "open_gate" или "reconfigure". Соответственно, отсылка сообщений производилась с указанием имен агента и его сообщения. Что-то вроде send("Floor1.Sensor4", "current_value", msg_data).

Подписка на сообщения так же осуществлялась посредством привязки обработчика к имени агента и его сообщения. Т.е. что-то вроде subscribe("Floor1.Sensor4", "current_value", event_handler). На любое сообщение мог подписаться кто угодно. Так что у сообщения могло быть любое количество подписчиков (включая и ноль подписчиков).

Политика 1-to-1 или 1-to-many определялась на этапе отсылки сообщения. Если нужно было отослать некоторое сообщение конкретному получателю, что в send() добавлялось имя агента-получателя. В этом случае сообщение доставлялось только указанному подписчику, а все остальные подписчики игнорировались. Это называлось целенаправленной доставкой. Если же в send() имя получателя не указывалось, то доставка сообщения выполнялась всем подписчикам. Это называлось широковещательной рассылкой.

При разработке SO-5 от схемы с именами агентов и сообщений было решено отказаться.

Одна из причин, возможно, главная, -- это тормоза, связанные с именами агентов и сообщений. Должен быть единый системный словарь, в котором зарегистрированы все эти имена. Там же должны храниться подписки. Все это должно быть защищено, откуда сразу же появляется глобальный mutex, на котором подвисает любая операция send/subscribe. Плюс постоянные манипуляции со строками.

Еще одна причина -- это количество ошибок, которые могут быть обнаружены только в run-time. Ошибся кто-нибудь с именем агента/сообщения, скажем, написал "weighing_machine" вместо "weighing-machine" и все, ищи проблему в отладчике.

Но была и еще одна причина. Объяснить ее сложнее, т.к. она определяется опытом разработки с помощью построенной над SO-4 библиотеки MBAPI-3, которая так толком в свет и не вышла (для истории осталось разве что совсем краткое описание этого инструмента).

Смысл MBAPI был в том, чтобы скрыть реальных получателей за дополнительным уровнем адресации. Вводилось понятие mbox-а (почтового ящика). Сообщение отсылалось в mbox и его дальнейшая судьба была неизвестна отправителю. Соответственно, отправитель уже не знал, происходит ли целенаправленная или широковещательная рассылка.

За mbox-ом могла скрываться довольно сложная механика. На mbox можно было повесить фильтры (они назывались "анализаторы"), что позволяло получать только те сообщения из mbox-а, содержимое которых интересовало получателя. Можно было перехватывать сообщения. Т.е. какой-то получатель мог перехватить сообщение, модифицировать его и отослать дальше уже обновленное сообщение. Или же вообще "проглотить" перехваченное сообщение и никому его не отдать.

Но главной фишкой MBAPI была сетевая прозрачность. При отправке сообщения в mbox отправитель не имел понятия, где находится получатель: в том же процессе или в другом, возможно, на другой ноде. Дабы обеспечить эту сетевую прозрачность MBAPI использовало специальные процедуры handshaking-а при подключении MBAPI-шных процессов друг к другу. В процессе handshaking-а стороны обменивались информацией о своих mbox-ах. Что и позволяло при отсылке сообщения в mbox определить куда именно сообщение должно уйти.

В общем, те дополнительные бонусы, которые давал еще один уровень косвенности, представленный mbox-ами, подтолкнули нас к внедрению понятия mbox-а непосредственно в сам SObjectizer-5. Поэтому все взаимодействие агентов в SO-5 построено вокруг mbox-ов.

Однако, с mbox-ами в SO-5 случилось то, что часто бывает при разработке софта: в планах было одно, на практике же получилось несколько другое :) Так, была мысль о сокрытии за интерфейсом mbox-а двух разных их типов: local_mbox, который предназначался для эффективного обмена сообщениями между агентами в одном процессе, и remote_mbox, который бы обеспечивал сетевую прозрачность. К сожалению, по прошествии пяти лет я уже не смогу вспомнить, что же именно не срослось с remote_mbox, но в итоге остались только local_mbox-ы.

Так же первоначально все mbox-ы были multi-producer/multi-consumer mbox-ами. Т.е. имея ссылку на mbox кто угодно мог отослать сообщение в mbox и кто угодно мог подписаться на сообщения из этого mbox-а. В принципе, все это нормально работало...

Но до тех пор, пока не возникла необходимость посмотреть на производительность обмена сообщениями. И пока не дошли руки до организации синхронного взаимодействия.

Вопрос производительности, вообще-то говоря, довольно неоднозначный. На эту тему можно отдельно поговорить. В принципе, по практическому опыту, если интенсивность обмена сообщениями превышает несколько сотен тысяч штук в секунду, то, вероятно, в приложении что-то не то. Поэтому, цифры в миллионы или десятки миллионы msg/sec -- это в большей степени маркетинговая фишка. Тем не менее, с точки зрения маркетинга было бы полезно иметь возможность показать какой-нибудь синтетический бенчмарк и сказать: "Ну вот же, X миллионов сообщений в секунду. Как с куста!" :)

И вот при получении максимальной производительности от message-passing-а оказывается, что в MPMC mbox-ах некоторые накладные расходы заложены изначально. Неизбежно будут списки получателей и проход по этим спискам. Можно как-то оптимизировать эту кухню и обработать частные случаи, вроде единственного подписчика. Но все равно, некоторая плата за выбор получателя в MPMC mbox-е будет всегда.

При этом выяснилось, что зачастую агенты работают именно по схеме 1-to-1. Грубо говоря, где-то в 70-80% случаев агент-оправитель целенаправленно отсылает сообщения одному и тому же агенту-получателю. Показалось разумным свести накладные расходы для такого распространенного сценария к минимуму. Так появились multi-producer/single-consumer mbox-ы. Т.е. имея ссылку на mbox кто угодно может отослать сообщение в mbox. Но только владелец MPSC mbox-а сможет получить сообщение. Сейчас MPSC mbox-ы представляют собой простую обертку над очередью событий. Т.е. отправка сообщения в MPSC mbox -- это сохранение сообщения в очереди получателя напрямую. В этом плане работа через MPSC mbox-ы напоминает взаимодействие акторов в C++ Actor Framework, в just::thread pro и в Erlang.

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

И вот при синхронном общении агентов опять выяснилось, что MPMC mbox не есть здорово. Т.к. агент-отправитель запроса должен получить ответ от одного агента-приемника. Но в MPMC mbox-е подписчиков на сообщения-запрос может оказаться больше одного или не оказаться вообще! Сейчас в этом случае возникнет ошибка в run-time, но очень бы хотелось отлавливать такие вещи еще в compile-time. Т.е. чтобы нельзя было сделать синхронный запрос через MPMC mbox, а только через MPSC mbox.

Расширение встроенных механизмов overload control так же сталкивается с некоторыми неприятными последствиям наличия MPMC mbox-ов. Например, для overload control было бы полезно иметь возможность "усыпить" отправителя сообщения на send-е, если очередь получателя переполнена. Но это хорошо, если отсылка идет в MPSC mbox, где получатель гарантированно один-единственный. А если в MPMC mbox, где таких получателей 100500?

В связи с этим регулярно посещает мысль о том, что более правильно было бы иметь несколько разных сущностей. Например, сущность "канал" с операциями send/sync_get для организации взаимодействия 1-to-1. И сущность "тема" (или "доска объявлений") с операцией publish для организации взаимодействия 1-to-many.

Соответственно, эти сущности представлялись бы разными типами. Канал был бы объектом типа channel. Доска объявлений -- объектом типа topic или message_board.

Нужно двум агентам играть в ping-pong? Пусть они обмениваются ссылками на свои channel-ы и заталкивают сообщения ping и pong в чужой канал. При этом либо заталкивают асинхронно (метод send), либо синхронно (метод sync_get). При этом на send-е можно притормозить, если channel переполнен.

С другой стороны, нужно агенту широковещательно распространять информацию о чем-то -- он создает topic и публикует свои сообщения в этом topic-е (метод publish). А агенты, которые хотят получать сообщения из topic-а, подписывают на topic свои channel-ы.

Т.е. различия между channel-ами и topic-ами проводятся на уровне программного интерфейса, что есть хорошо. Т.к. еще в compile-time отсекается возможность сделать синхронный запрос на MPMC mbox-е.

Но, с другой стороны, в том, что сейчас у MPMC и MPSC mbox-ов одинаковый интерфейс, есть и свои положительные стороны. Зачастую агенту просто нужно знать, куда писать свою информацию. Агенту дают mbox и он отправляет в mbox сообщения не думая о том, сколько получателей спрятано за mbox-ом: ноль, один или много. Сегодня нужно отдавать информацию одному-единственному получателю? Ok, просто делаем send для mbox-а. Завтра нужно будет отдавать информацию группе получателей? Нет проблем, вообще ничего не меняется, тот же самый send остается.

Ну и самое главное: выбор между channel+topic или mbox нужно было бы делать в самом начале. Сейчас, когда используются только mbox-ы, перейти к разделению на channel-ы и topic-и уже гораздо сложнее. Да и не понятно, будет ли стоить овчинка выделки. Тем не менее, если бы сейчас встал вопрос о разработке SO-6 без сохранения совместимости с SO-5, я бы рассматривал вариант с channel+topic очень и очень пристально. Да и вообще бы уделил механизму publish-subscribe гораздо больше внимания. Возможно, если бы SObjectizer представлял из себя эдакий embedded MQ с поддержкой топиков, фильтров сообщений и прочих фишек publish-subscribe, то и позиционировать, и продвигать SObjectizer было бы проще. Т.к. actor model сейчас очень плотно ассоциируется с тем, что есть в Erlang. Посему, как мне кажется, людям проще осваивать C++ Actor Framework или Akka, которые очень многое взяли из Erlang-а один-в-один, нежели SObjectizer. Ведь SObjectizer, хоть и базируется на actor model, но имеет слишком много отличий от, скажем так, Erlang-inspired реализаций.

Отправить комментарий