В комментариях к последней статье про шишки, которые нам довелось набить за 15 лет использования акторов в C++, вновь всплыла тема отсутствия в SObjectizer-5 распределенности «из коробки». Мы уже отвечали на эти вопросы множество раз, но очевидно, что этого недостаточно.


В SObjectizer-5 нет распределенности потому, что в SObjectizer-4 поддержка распределенности была, но по мере того, как расширялся спектр решаемых на SObjectizer задач и росли нагрузки на SObjectizer-приложения, нам пришлось выучить несколько уроков:


  • Под каждый тип задачи желательно иметь свой специализированный протокол. Потому что обмен большим количеством мелких сообщений, потеря части которых не страшна, сильно отличается от обмена большими бинарными файлами;
  • Реализация back-pressure для асинхронных агентов — это сама по себе непростая штука. А когда сюда еще и примешивается общение по сети, ситуация становится гораздо хуже;
  • Сегодня какие-то куски распределенного приложения обязательно будут написаны на других языках программирования, а не на C++. Поэтому требуется интероперабильность и наш собственный протокол, заточенный под C++ и SObjectizer, мешает разработке распределенных приложений.

Далее в статье попробуем раскрыть тему подробнее.


Преамбула


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


Прозрачная отсылка и получение сообщений


Распределенность «из коробки» хороша тогда, когда она прозрачна. Т.е. когда отсылка сообщения на удаленную сторону ничем не отличается от отсылки сообщения внутри приложения. Т.е. когда агент вызывает send<Msg>(target,...), а уже фреймворк определяет, что target — это какой-то другой процесс или даже другая нода. После чего сериализует, ставит сообщение в очередь на отправку, отсылает в какой-то канал.


Если такой прозрачности нет и API для отсылки сообщения на удаленную сторону заметно отличается от API для отсылки сообщения внутри того же процесса, то в чем выгода от использования встроенного в фреймворк механизма? Чем это лучше использования какого-нибудь готового MQ-брокера, ZeroMQ, RESTful API или, скажем, gRPC? Собственный велосипед на эту тему вряд ли будет лучше уже готового стороннего инструмента, который развивается давным-давно и гораздо большими силами.


То же самое касается и получения сообщений. Если для агента входящее сообщение от соседнего агента ничем не отличается от входящего сообщения от удаленного узла, то как раз это и дает возможность легко и непринужденно разделять одно большое приложение на части, которые могут работать в разных процессах на одном или на нескольких узлах. Если же локальное сообщение получается одним способом, а сообщение от удаленного узла — другим, то пользы от собственных велосипедов уже гораздо меньше. И, опять же, непонятно, зачем отдавать предпочтение собственному велосипеду, а не воспользоваться, например, одним из существующих MQ-брокеров.


Прозрачность топологии приложения


Хорошо, когда все узлы распределенного приложения связаны друг с другом:


В этом случае вообще нет проблемы отослать сообщение от узла A узлу E.


Только вот в реальной жизни топологии будут не такими простыми и придется работать в ситуации, когда между какими-то узлами прямой связи нет:



В этом случае узел A не может отправить сообщение узлу E непосредственно, для этого нужно будет воспользоваться услугами промежуточного узла D. Естественно, хочется, чтобы программист, который использует акторный фреймворк, вообще ничего не знал про выбор маршрутов передачи сообщений. Поскольку если программисту придется явно указывать, что сообщение от A к E должно идти через D, то толку от такой распределенности «из коробки» будет не так много, как хотелось бы.


Минимизация количества каналов и их типов


Если двум узлам нужно обмениваться, например, тремя типами трафика (скажем, обычный поток транзакций с подтверждениями; регулярный обмен сверочной информацией, телеметрия), то можно было бы, в теории, создать между двумя этими узлами три разных канала. По первому бы шел транзакционный трафик, по второму — сверочные файлы, по третьему — телеметрия.


Только на практике это не удобно. Чем меньше каналов, тем лучше. Если достаточно всего одного канала для передачи всех трех типов трафика, то и самому программисту будет проще, а уж DevOps-ы точно скажут спасибо.


Минимизация количества каналов так же важна и для решения проблем топологии распределенного приложения. Так, если взаимодействовать нужно узлам A и E, то вряд ли будет удобно создавать под это взаимодействие три разных канала между A и D, а затем еще три между E и D.


Соответственно, хорошая распределенность «из коробки» должна уметь гонять разные типы трафика через один и тот же коммуникационный канал. Если она не делает это хорошо, то намного проще использовать разные типы транспорта для каждого типа трафика.


Ну и, собственно, сами подводные камни


Итак, встроенная распределенность хороша, когда она не отличается от локального обмена сообщениями, сама определяет топологию и выбирает оптимальные пути доставки сообщений, способна передавать разные типы сообщений по одним и тем же коммуникационным каналам. При реализации такой распределенности придется столкнуться с несколькими проблемами. Вот их перечень в произвольном порядке, без попытки отсортировать их по степени «тяжести» и/или «важности». Ну и да, перечень отнюдь не полный. Это только то, что сразу вспомнилось.


Back-pressure между прикладными и траспортными агентами


Асинхронный обмен сообщениями плохо дружит с back-pressure. Агент, который выполняет send<Msg>(target,...) не может быть просто так приостановлен на вызове send, если в очереди получателя скопилось достаточно много необработанных сообщений. В случае, когда target — это удаленная сторона, у нас получается следующее:


  • Есть очередь сообщений, которые нужно передать на удаленную сторону. Это еще не сериализованные сообщения, которые должны быть должным образом сериализованы и подготовлены для передачи в канал. Эта очередь может расти очень быстро, если агенты-отправители генерируют больше сообщений, чем может обработать агент, отвечающий за преобразование сообщений в какой-то формат;
  • Есть буфер (или набор буферов) в котором лежат уже подготовленные к отправке данные, которые еще в канал не ушли. При возникновении затыков в сети, когда скорость записи в канал падает, объемы этих буферов растут.

Тут можно использовать разные подходы. Например, один и тот же агент принимает сообщения, сериализует их и пишет в канал. В этом случае у него может быть буфер ограниченного размера для сериализованных данных, но мало возможностей влиять на размер очереди еще не сериализованных сообщений. А может быть и два разных агента: один принимает сообщение, сериализует их в буфера с двоичными данными, а второй агент отвечает за запись этих буферов в канал. Тогда могут расти и очереди сообщений, и очереди буферов (а так же размеры этих буферов) при каких-то затыках в сети и/или внутри самого приложения.


Простой выход в том, чтобы «резать» лишнее. Т.е. если канал начинает притормаживать, то мы не даем расти очередям сверх какого-то размера. И просто выбрасываем какие-то данные (например, самые старые или самые новые). Но это ведет к нескольким вопросам, требующим какого-то решения:


  • Размеры этих буферов хорошо бы тюнить для того, чтобы терять как можно меньше и спокойно переживать небольшие затыки. Появляется головная боль по поводу настройки этих параметров. Еще веселее, когда данные параметры должны адаптироваться к текущим условиям в run-time;
  • Прикладная логика приложения может требовать как-то фиксировать факты выбрасывания сообщений. Грубо говоря, если сообщение A с идентификатором Id1 было выброшено из-за затыков в сети, то следы этого должны появиться в логах. Иначе затем будет не понять, почему транзакция X началась, дошла до стадии отсылки A, а потом «подвисла» без движения.

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


Причем понятие «длительный» может варьироваться от приложения к приложению. Где-то «длительный» — это несколько минут или даже десятков минут. А где-то, где сообщения передаются с темпом 10000 в секунду, длительным будет считаться разрыв всего в 5 секунд.


Неприятность проблемы взаимодействия в направлении от прикладных агентам к транспортным в том, что прикладным агентам, как правило, не нужно знать, что они отсылают сообщение на удаленную сторону, а не соседнему агенту в этом же процессе. Но из этого следует, что когда скорость передачи исходящих данных падает или канал совсем рвется, то прикладные агенты не могут так просто приостановить генерацию своих исходящих сообщений. И несоответствие между тем, что генерируют прикладные агенты и тем, что может уйти в канал должен брать на себя транспортный слой, т.е. та самая прозрачная распределенность «из коробки».


Back-pressure между транспортными и прикладными агентами


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


Соответственно, запросто может произойти так, что данные из канала поступают быстрее, чем приложение успевает их десериализовать, доставлять до прикладных агентов и обрабатывать. Если пустить эту ситуацию на самотек, то ничем хорошим это для приложения не кончится: возникнет перегрузка, которая, при плохом раскладе, приведет к полной потере работоспособности из-за деградации производительности.


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


Но возникает вопрос: если транспортный агент просто читает сокет, десериализует бинарные данные в прикладные сообщения и отдает эти сообщения кому-то, то как транспортный агент узнает, что эти прикладные сообщения не успевают обрабатываться?


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


Смешение трафика с разными приоритетами в одном канале


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


Проблема здесь в том, что тривиальные реализации транспортного слоя так не сумеют :(


Fire-and-forget or not-to-forget?


Думаю, что не ошибусь, если скажу, что использование политики fire-and-forget является общепринятой практикой при построении приложений на базе агентов/акторов. Агент-отправитель просто асинхронно отсылает сообщение агенту-получателю и не заботится о том, дошло ли сообщение или потерялось где-то по дороге (например, было выброшено механизмом защиты агентов от перегрузки).


Все это хорошо до тех пор, пока не возникает необходимость передачи больших бинарных блоков (BLOB-ов) через транспортный канал. Особенно, если этот канал имеет свойство часто рваться. Ведь никому не понравится, если мы начали передавать блок в 100MiB, передали 50MiB, канал порвался, затем восстановился, а мы не стали ничего досылать. Или начали перепосылать все заново.


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


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


Итого


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


Ни в коем случае не хочу сказать, что невозможно сделать транспортный слой, который бы успешно закрывал все или большую часть обозначенных проблем. Насколько я знаю, в Ice от ZeroC что-то подобное как раз и реализовано. Или в nanomsg. Так что это возможно.


Проблемы здесь две:


  • Сможете ли вы настолько вложиться в разработку своего продвинутого транспортного слоя? Тот же Ice от ZeroC находится в активной разработке и эксплуатации больше полутора десятков лет. А автор nanomsg перед этим «собаку съел» на реализации ZeroMQ. Будут ли у вас такие ресурсы и такая компетенция?
  • Как при этом обеспечить еще и интероперабильность с другими языками программирования? Замкнутым системам, вроде Erlang-а и Akka, хорошо, их мало волнует то, что происходит вне их экосистем. В случае же с самодельным и малоизвестным акторным фреймворком для C++ ситуация совершенно другая.

В SObjectizer-4 со всеми этими проблемами мы столкнулись. Какие-то решили, какие-то обошли, какие-то остались. А вот при создании SObjectizer-5 мы взглянули на проблему распределенности еще раз и поняли, что наших ресурсов на то, чтобы успешно справиться еще и с ней, у нас не хватит. Мы сосредоточились на том, чтобы SObjectizer-5 стал классным инструментом для упрощения жизни при создании многопоточных приложений. Поэтому SO-5 закрывает проблемы диспетчеризации сообщений внутри одно процесса. Ну а общение между процессами...


Как оказалось, такое общение довольно удобно делать с использованием готовых протоколов и готовых MQ-брокеров. Например, посредством MQTT. Конечно же, при этом нет никакой прозрачной распределенности. Но практика показывает, что когда речь заходит о высоких нагрузках или сложных сценариях работы, от прозрачной распределенности пользы мало.


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

Поделиться с друзьями
-->

Комментарии (23)


  1. OlegZH
    30.03.2017 22:26

    Честно говоря, возникает желание заняться этой темой. Это важно и актуально. Но, кажется, что эта тема хорошо проработана, потому что про «мультиагентный подход» я слышу уже дано, а, значит, разработчиками уже выработаны какие-то оптимальные решения. Или, всё же, не совсем? Но разобраться в основах было бы очень любопытно.


    1. eao197
      30.03.2017 22:38

      Блин, опять промахнулся. Ответил вам здесь.


  1. eao197
    30.03.2017 22:37

    Это важно и актуально.
    Что именно важно и актуально?
    а, значит, разработчиками уже выработаны какие-то оптимальные решения. Или, всё же, не совсем?
    Не знаю, что творится в этой теме с точки зрения computer science. На практике же есть несколько широко известных реализаций: Erlang и Akka. Erlang — это отдельный язык и виртуальная машина (т.е. настоящая платформа). Akka — это просто фреймворк для JVM (для Java и Scala). Кроме них существуют и другие реализации для разных языков и платформ (например, Elixir для Erlang VM, Quasar для Java, Orleans для .NET, Celluloid для Ruby и т.д.) Для C++ полный разброд и шатание (немного подробнее здесь). Впрочем, для C++ это нормально, т.к. C++ применяется для совершенно разных задач.


  1. DaylightIsBurning
    30.03.2017 23:02
    +1

    Вы часто упоминаете сложности организации back-pressure в модели акторов, но не вдавались в подробности, в чём эти сложности заключаются. Было бы интересно почитать про это.
    Наивно, кажется что с этим не должно быть проблем: получатель должен отправлять сообщение «перегружен» всем отправителям, когда его очередь входящих сообщений забита более чем на 50-80% и сообщение «свободен», когда очередь освободилась до уровня 10-20%. Соответственно все отправители, если недавно было получено сообщение «перегружен» от получателя должны остановить отправку до получения сообщения «свободен». Сообщение «перегружен» также может содержать таймаут, когда оно истекает если не обновлено. Если получатель не знает, кто ему отправит сообщение — он его узнает после получения первого же сообщения. Остаётся опасность того, что каждое сообщение приходит от нового актора, но большое число акторов и так несет опасность, как Вы уже писали, да и получатель часто знает своих получателей на этапе компиляции. Теоретически, кажется, что это всё может работать без особо участия пользователя фреймворка так как необходимая информация и так доступна системе… Было бы интересно почитать, где тут возникают сложности.

    P.S. Очень интересные статьи, спасибо!


    1. eao197
      30.03.2017 23:21
      +2

      Один из главных факторов — это то, что доставка сообщения до получателя не является мгновенной операцией. Т.е. перегруженный агент A отсылает агенту B сообщение о том, что A перегружен. Но пройдет какое-то время, в течении которого агент B будет отсылать сообщения агенту A. За это время агент A может быть загружен «по самое не хочу». Опять же, сообщение о том, что агент А освободился и готов принимать данные вновь, придет к B не сразу. В течении этого времени агент B будет простаивать, т.е. мы будет терять время.

      Кроме того, далеко не всегда есть жесткая связь 1:1 между отправителем и получателем. Т.е. может быть получатель A, которому отсылают сообщения агенты B1, B2, B3 и т.д. И все с разным темпом, и все в своем собственном порядке. Так что если перегрузка A возникает при сообщении от B2, то это не значит, что блокировать нужно именно B2.

      Кроме того, агент A может отсылать сообщения самому себе (такое регулярно используется). Не будет же A блокировать самого себя :)

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

      Кроме того, далеко не факт, что очередь, заполненная на 80% будет разгребаться дольше, чем очередь, заполненная на 20%. Тут зависит от того, что за сообщения стоят в очереди и сколько стоит обработка каждого из них. Есть сообщения, обработка которых стоит очень дешево, есть сообщения, на обработке которых агент может тратить минуты в буквальном смысле.

      Кроме того, далеко не факт, что агенту будет принадлежать собственная очередь. Это от фреймворка зависит. У нас в SObjectizer сообщения идут не в очередь агента, а в очередь диспетчера, на котором работают агенты. Для каких-то диспетчеров у каждого агента будет своя очередь, а для каких-то — будет общая очередь, разделяемая с другими агентами. Кроме того, это все unbound-очереди, так что процент их наполнения не подсчитать. AFAIK, в CAF и в Akka у каждого актора своя очередь.

      Так что, по хорошему, для того, чтобы понять, насколько загружен актор, нужно понимать сколько «тяжелых» сообщений уже стоят в его очереди. Но даже при этом, когда агенты взаимодействуют в режиме 1:N или N:1, или M:N, не понятно кого именно приостанавливать.

      PS. Понятно, что в каких-то конкретных случаях можно сделать какое-то частное решение, которое будет прекрасно работать на конкретной задаче. Но если смотреть с точки зрения реализации универсального фреймворка, все не так тривиально, как хотелось бы.


      1. eiennohito
        31.03.2017 15:40

        А почему нельзя реализовать протокол подобный ReactiveStreams в таком случае?
        По-моему весьма удобный инструмент для работы с backpressure, у grpc тоже очень похожее внутреннее API.


        1. eao197
          31.03.2017 15:42

          А почему нельзя реализовать протокол подобный ReactiveStreams в таком случае?
          А где можно посмотреть на описание этого протокола?


          1. eiennohito
            31.03.2017 15:45

            Основная документация с TCK: http://www.reactive-streams.org/
            TCK он для джавы, но одним из вдохновителей лежат авторы akka как раз.
            API даже войдёт в JDK9.
            https://community.oracle.com/docs/DOC-1006738


            1. eao197
              31.03.2017 16:45

              Ну вот как раз про backpressure информация что-то с ходу не обнаруживатся. В описании изменений для JDK9 вообще вот что сказано:

              The Flow API does not provide any APIs to signal or deal with back pressure as such, but there could be various strategies one could implement by oneself to deal with back pressure.
              Т.е. стандартного API для этого нет, нужно делать что-то по месту бедствия.

              Об этом же говорят и авторы reactive manifesto (раздел Basic Semantics):
              How elements are transmitted, their representation during transfer, or how back-pressure is signaled is not part of this specification.
              Из описания API для Reactive Streams следует, что основная идея в периодическом запросе N новых сообщений Subscriber-ом после того, как он разобрался со своей текущей нагрузкой. Для каких-то задач это вполне себе нормально. И может быть без особых усилий реализовано поверх асинхронного общения акторов.

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


              1. eiennohito
                31.03.2017 16:53

                Да, для вашего юзкейса возможно правильнее дропать какие-то сообщения на получателях если они не успевают обрабатывать весь поток.
                ReactiveStreams — это такой минимальный API/протокол для реализации async non-blocking backpressure. Например необязательно посылать следующий сигнал когда обработаны все текущие сообщения, можно сделать это когда есть какое-то место в буфере обработчике например.

                Akka Streams например поверх протокола ReactiveStreams реализует много чего. И отбрасывание лишних сообщений, и группировку, и непосылание новых запросов.


                1. eao197
                  31.03.2017 16:59

                  Ну так ведь речь шла вот о чем:

                  Вы часто упоминаете сложности организации back-pressure в модели акторов, но не вдавались в подробности, в чём эти сложности заключаются.
                  Я попытался объяснить, с чем приходится сталкиваться в общем случае. Понятно, что бывают частные случаи, в которых реализация back-pressure более очевидно (как, например, в ограниченном контексте с Reactive Streams). Вообще, вот здесь я специально подчеркивал, что хороший механизм защиты от перегрузки должен быть заточен под конкретную задачу.
                  Akka Streams например поверх протокола ReactiveStreams реализует много чего. И отбрасывание лишних сообщений, и группировку, и непосылание новых запросов.
                  А можно ссылочку попросить, дабы не перелопачивать всю документацию по Akka Streams?


                  1. eiennohito
                    31.03.2017 17:08

                    Штуки, про которые я рассказывал реализованы вместе с буферизацией на асинхронных границах http://doc.akka.io/docs/akka/2.4.14/scala/stream/stream-rate.html

                    Правда akka-stream штука достаточно сложная для понимания с наскока, и возможно будет лучше именно что познакомиться получше с документацией и может почитать исходники. Мне в akka-stream очень нравятся примитивы для написания собственной логики обработчиков (GraphStage), которую нельзя описать встроенными комбинаторами. Для обработки потоковых данных на мой взгляд akka-stream гораздо проще и удобнее, чем обычные акторы.


                    1. eao197
                      31.03.2017 17:15

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


                      1. eiennohito
                        31.03.2017 17:18

                        Я бы не сказал, что akka-stream это про акторы. Оно просто использует их для реализации процесса обработки однотипных данных и не более того.


                        1. eao197
                          31.03.2017 18:03

                          Я на следил пристально за развитием Akka и ее инфраструктуры, но мне казалось, что Akka Streams появились именно потому, что просто на акторах делать обработку потоков данных неудобно. И для упрощения этой задачи сделали прослойку поверх акторов.


                          1. eiennohito
                            31.03.2017 19:16

                            Частично, пожалуй, то, что вы говорите, частично на мой взгляд — это создание API для работы с типизированными данными.

                            akka-stream — это ведь не только про обработку линейных потоков данных. В akka-stream модель вычислений — граф, с циклами и прочим весельем при желании. Поэтому чисто на потоках например реализован akka-http.

                            Сами акторы в akka-stream используются только для разруливания проблем с concurrency (по сути единственный актор там — это GraphInterpreter, который исполняет действия над пайплайном обработки).


                    1. eao197
                      01.04.2017 18:22

                      Штуки, про которые я рассказывал реализованы вместе с буферизацией на асинхронных границах http://doc.akka.io/docs/akka/2.4.14/scala/stream/stream-rate.html
                      Похоже, что задачи, возлагаемые на Akka Streams, в мире C++ решаются с помощью Data Flow and Dependency Graphs из Threading Building Blocks. Это вообще отдельная ниша со своими особенностями. Решения для нее можно строить на базе Модели Акторов (например, акторами могут быть отдельные вычислительные узлы графа, так же акторами могут быть координаторы, которые решают, какому вычислительному узлу выделить время на рабочей нити в пуле). Можно строить на базе CSP и короутин. Это может быть даже проще и удобнее: связи между узлами представляются в виде CSP-каналов, вычислительные узлы графа — в виде короутин. Когда короутина-поставщик пытается записать очередной элемент данных в заполненный канал, то она просто приостанавливается. Когда короутина-потребитель пытается прочитать очередную порцию данных из пустого канала, она так же просто приостанавливается. Тем самым получается относительно несложное отображение большого количества короутин на небольшое количество рабочих потоков.

                      В любом случае, Akka Streams производит впечатление штуки, которая может быть построена на базе акторов, но не наоборот. Соответственно, механизмы backpressure, которые хорошо подходят к Akka Streams не обязательно должны хорошо работать в случае с просто акторами в задачах, не похожих на задачи из области data flow programming.


                      1. eiennohito
                        02.04.2017 05:04

                        Я могу ошибаться, но TBB умеет представлять вычисления только в виде орграфа, те без циклов. akka-stream умеет в циклы в том числе.

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

                        Корутины и continuations — это хорошо, я не спорю, оно мне самому нравится.

                        Задачи — они разные, решения тоже есть разные, поэтому да, нужно смотреть что лучше подходит в каждом конкретном случае. Но мне кажется, что протокол, когда обработчик говорит, сколько сообщений он может принять и не умереть (плюс возможно что делать с другими сообщениями) вполне имеет право на жизнь почти везде.


                        1. eao197
                          02.04.2017 09:27

                          Нужно иметь какой-то баланс между проведением вычислений и передачей данных.
                          При использовании CSP-каналов и короутин здесь неоткуда ожидать больших накладных расходов, чем при использовании акторов.
                          Но мне кажется, что протокол, когда обработчик говорит, сколько сообщений он может принять и не умереть (плюс возможно что делать с другими сообщениями) вполне имеет право на жизнь почти везде.
                          Имеет. Но, во-первых, как один из набора возможных. Во-вторых, с ним тоже не все так просто, примеры чего можно найти даже по приведенной вами ссылке. Так что изначальный тезис о том, что backpressure для асинхронных агентов — это не так просто, как хотелось бы, на мой взгляд, пока сохраняет свою актуальность.


                  1. eiennohito
                    31.03.2017 17:15
                    +1

                    А по поводу протоколов для доставки можете посмотреть на https://github.com/real-logic/Aeron где есть настраиваемая надёжность доставки с приоритетами поверх UDP.


  1. gorodnev
    31.03.2017 07:03
    +1

    Какая же интересная у Вас работа…


  1. Jef239
    31.03.2017 22:30

    Мы в свое время (15 лет назад) в качестве транспорта использовали named pipe. Преимущество в том, что мы передаем сообщения вместе с их длиной (как в UDP), но имеем надежную передачу (как в TCP).

    Но… по результатам разработки я бы сейчас ограничился просто TCP/IP, а разделение на сообщения написал бы сам. Как минимум, тогда могло бы ходить не только по локальной сети.

    В целом вам есть смысл посмотреть на Skype и другие голосовые мессенджеры. Там тоже несколько типов трафика (чат с обязательной доставкой, основная часть голоса, обертона, которые если не дошли — так и не надо).


  1. eao197
    31.03.2017 22:58

    Преимущество в том, что мы передаем сообщения вместе с их длиной (как в UDP), но имеем надежную передачу (как в TCP)
    Не очень понял вашу мысль. Сообщение при передаче по TCP точно так же должно предваряться длиной. Ну или придется искать в потоке данных специальные разделители (как это сделано в HTTP), что совсем неудобно при передаче BLOB-ов.