Объединяем акторов и SEDA-подход: зачем и как?

    В одной из статей про шишки, которые довелось набить за 15 лет использования акторов в C++, речь зашла о том, что большое количество акторов — это, зачастую, сама по себе проблема, а отнюдь не решение. И что использование идей из SEDA-подхода может существенно упростить жизнь при разработке приложений на базе модели акторов. Однако, как показали затем вопросы в комментариях к предыдущим статьям, совмещение SEDA-подхода и модели акторов отнюдь не очевидно, поэтому есть смысл копнуть данную тему чуть глубже.


    Модель акторов и ее достоинства


    Пара слов о модели акторов


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


    • акторы реагируют на входящие сообщения;
    • актор обладает поведением, которое определяет реакцию на входящие сообщения;
    • получив сообщение актор может:
      • отослать некоторое (конечное) количество сообщений другим акторам;
      • создать некоторое (конечное) количество новых акторов;
      • определить для себя новое поведение для обработки последующих сообщений.

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


    Модель акторов не определяет, что из себя должны представлять акторы. Поэтому реализации модели акторов могут выглядеть сильно по-разному. Так, одной из самых известных реализаций модели акторов считается язык программирования Erlang. Там актором является легковесный процесс, работающий в рамках Erlang VM. В еще одной из самых известных реализаций модели акторов, фреймворке Akka для JVM, акторы представляются в виде объектов, методы которых автоматически вызываются фреймворком при получении сообщения для актора. Тогда как в другой реализации акторов для JVM, Quasar, актор — это сопрограмма. В мире C++ такие фреймворки, как QP/C++ и SObjectizer, используют представление актора как объекта, являющегося конечным автоматом. В фреймворке CAF актор может быть как объектом, так и функцией. А в фреймворке Just::Thread Pro: Actors Edition каждый актор — это отдельная нить ОС.


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


    В чем же удобство акторов?


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


    Во-первых, это то, что каждый актор обладает собственным состоянием и общается с внешним миром только посредством асинхронных сообщений. Т.е. воплощение архитектуры share nothing в чистом виде. Важность этого сложно переоценить как при многопоточном программировании, так и при построении распределенных приложений.


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


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


    Во-вторых, каждый актор может представлять из себя некоторую автономную сущность, работающую в соответствии с собственной логикой. Например, в реляционной СУБД можно создавать акторов для выполнения SQL-запросов. Каждый актор может выполнять запрос от начала до конца: разбирать запрос, проверять его корректность, строить план выполнения, запрашивать данные из системы хранения, выделять удовлетворяющие запросу данные, отсылать данные отправителю запроса. Всю эту логику бывает удобно реализовать в рамках одной сущности, в виде актора. После чего приложение (например, сервер РСУБД) будет создавать N акторов, каждый из которых независимо от других обслуживает различные запросы.



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


    Подход SEDA и его достоинства


    SEDA-подход в нескольких словах


    Суть SEDA подхода состоит в том, что каждая конкретная прикладная операция разбивается на отдельные стадии и под каждую стадию выделяется отдельная вычислительная сущность. Так, обслуживание SQL-запроса в РСУБД может быть разбито на следующие стадии: разбор SQL-запроса, валидация параметров запроса, построение плана запроса, поднятие данных из хранилища, фильтрация удовлетворяющих запросу данных, отсылка результатов отправителю запроса.


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



    Фишка SEDA-подхода в том, что очереди сообщений между сущностями-стадиями могут использоваться для контроля за нагрузкой на приложение. Допустим, очередь между стадиями «разбор» и «валидация» может иметь фиксированный размер в 20 элементов. Это означает, что стадия «разбор» не сможет поставить в очередь очередное сообщение, если очередь к стадии «валидация» полностью заполнена.


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


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

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


    Достоинства SEDA-подхода


    Если в реализации SEDA-подхода используется архитектура share nothing (а, как правило, она и используется, т.к. отдельным сущностям-стадиям нет нужды разделять какие-то мутабельные данные), то в случае с SEDA мы имеем такое же удобство многопоточного программирования и построения распределенных приложений, как и в случае модели акторов.


    Однако, наиболее важное достоинство SEDA-подхода — это врожденная защита от перегрузки. В модели акторов перегрузка акторов является ахилесовой пятой. А вот в SEDA-подходе разработчик сразу должен позаботиться о защите своих сущностей-стадий от перегрузки и выбрать один из штатных механизмов, либо же реализовать и задействовать какой-то собственный. Что самым положительным способом сказывается на устойчивости и отзывчивости приложения под высокой нагрузкой.


    Объединение модели акторов и подхода SEDA


    Зачем объединять модель акторов и SEDA-подход?


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


    Акторы не защищены от перегрузки


    Одна из отличительных черт модели акторов — это асинхронный обмен сообщениями. Именно благодаря этому при использовании акторов можно не беспокоится о дедлоках. Но у этой положительной черты есть и своя цена: очереди сообщений для акторов не ограничены в размерах. Это означает, что если актор A отсылает сообщения актору B быстрее, чем актор B их обрабатывает, то размер очереди актора B будет постоянно расти. Еще хуже, когда актору B сообщения летят не только от актора A, но и от акторов C, D, E и далее по списку.


    Подробнее эта тема рассматривалась в одной из предыдущих статей.


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


    Множество акторов сложнее координировать


    Когда в приложении существует множество акторов, их активность может распределиться во времени таким образом, что приложение перестанет подавать признаки жизни. Например, выше мы говорили о подходе, когда в сервере СУБД акторы используются для выполнения SQL-запросов, при этом каждый запрос полностью обрабатывается одним актором. Вполне может быть так, что операция валидации параметров запроса сильно нагружает CPU, тогда как операция поднятия данных из хранилища не грузит CPU, но зато активно использует I/O.


    Если разместить всех подобных акторов на одном пуле рабочих нитей, то рано или поздно возникнет ситуация, когда почти все акторы пытаются выполнить валидацию параметров SQL-запросов. Тем самым нагружая CPU «под плашку». А потом они все пытаются обратиться в хранилище за данными и мы упираемся в возможности I/O. По хорошему, нам следовало бы делать так, чтобы только часть акторов могла грузить CPU, в то время, как другая часть задействует I/O. Но такая координация требует дополнительной работы и логика поведения акторов становится более сложной, чем нам изначально хотелось.


    Существуют естественные «бутылочные горлышки»


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


    Например, акторам может быть необходимо использовать hardware security module (HSM) для выполнения криптографических операций. HSM один. Интерфейс к нему представлен, скорее всего, какой-то сторонней библиотекой, подразумевающей, что вся работа с HSM (инициализация, использование, деинициализация) будет выполняться синхронно.



    Другой характерный пример: акторам нужно работать с БД, а количество параллельных подключений к БД сильно ограничено. Скажем, у нас всего 100 параллельных подключений к БД, а акторов — 10000 штук. И им всем нужно работать с БД.


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


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


    Что дает объединение модели акторов и SEDA-подхода?


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


    Возьмем еще раз пример с РСУБД и обработкой SQL-запросов посредством акторов. В сервере РСУБД у нас могут быть акторы-стадии: разбора SQL-запроса, валидации параметров, построения плана, подъема данных из хранилища, выборки удовлетворяющих условиям запроса данных, отсылки результатов. Каждый из этих акторов будет работать по обычным для акторов правилам: пока нет входящих сообщений, акторы-стадии спят. Когда входящие сообщения появляются, актор-стадия просыпается и выполняет их обработку.


    В таком случае мы получаем несколько приятных бонусов, хотя это обходится нам не бесплатно. Но прежде чем мы перейдем к перечню бонусов и их стоимости, нужно отметить один важный момент:


    Актор-стадия должен выполнять действие, которое может относиться к разным и независимым прикладным операциям. Например, актор-стадия для построения плана выполнения SQL-запроса должен уметь строить план и для insert-запроса, который пришел от клиента Alice, и для update-запроса, который пришел от клиента Bob, и для select-запроса, который пришел от клиента Eve.


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


    В пределе от актора-стадии может потребоваться умение группировать ожидающие сообщения так, чтобы их обработка была наиболее эффективной. Например, для актора-стадии, отвечающего в сервере РСУБД за планы запросов, построение планов для insert-запросов может быть намного дешевле, чем построение планов для select-запросов. Поэтому такой актор может сперва обработать все insert-запросы, а затем перейти к обработке select-запросов. Но это в пределе, когда приложение сталкивается с очень высокими нагрузками и приходится оптимизирования все и вся.


    Бонус №1: уменьшение общего количества акторов


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


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


    Работу небольшого количества акторов проще координировать. Если мы знаем, что акторы A и B являются CPU-bound, то мы можем выделить каждому из них по отдельному потоку ОС и даже можем привязать каждый из этих потоков к своему ядру. Тогда как для акторов C, D и E, являющихся I/O-bound и выполняющих асинхронный ввод-вывод, мы можем выделить одну общую рабочую нить. Ну и чем меньше акторов, тем проще им договориться о том, кто, когда, как и что из ресурсов будет потреблять.



    Примечание. Не всегда сокращение количества акторов — это хорошо. Могут быть задачи, в которых сотни миллионов одновременно существующих акторов — это более чем нормально. Например, фреймворк Orleans для .NET использовался для многопользовательской игры Halo (Halo 4 и Halo 5), где каждый пользователь представлялся отдельным актором, что выглядит как вполне разумный подход к решению подобной задачи. Тем не менее, в каких-то случаях чем меньше одновременно живущих акторов, тем проще.


    Бонус №2: естественным образом выражаются «бутылочные горлышки»


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


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


    А вот если стадия выполнения криптографических операций (например, шифрование и подпись исходящего документа) представлена соответствующим актором-стадией, то:


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

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


    Бонус №3: возможность использования bulk-операций


    Представьте себе, что вы на акторах реализуете MQ-шный брокер. И каждый актор отвечает за обслуживание своей темы (один topic == один актор). И вам нужно обеспечить персистентность для публикуемых сообщений, посредством записи новых сообщений в БД. Т.е., получил актор-topic новую команду publish — должен сперва сохранить сообщение в БД, а уже затем может отослать publish_ack с подтверждением.


    Если каждый актор-topic будет самостоятельно выполнять операции с БД, то мы можем столкнуться с ситуацией, когда над БД выполняется множество мелких транзакций (множество одиночных insert-ов в одни и те же таблицы, множество одиночных delete из тех же таблиц). Что не есть хорошо.


    А вот если у нас есть отдельный актор для выполнения операции publish, то у него появляется возможность выполнить сразу несколько insert-ов в таблицу БД посредством bulk-операции. С точки зрения увеличения пропускной способности (throughput) MQ-шного брокера это гораздо выгоднее множества одиночных insert-ов.


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


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


    Поэтому нужно самому уделять внимание защите акторов-стадий от перегрузки (overload control). И, возможно, придется озадачиться реализацией какой-то обратной связи (back pressure).


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


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


    Актор-коллектор весьма полезен в ситуации, когда сообщения могут дублироваться. Скажем, актор A отсылает сообщение Req и ждет в ответ Resp в течении 5 секунд, если же Resp не получен, то A перепосылает Req. Когда приложение работает под нагрузкой и обработка Req начинает подтормаживать, то A может отослать несколько Req, прежде чем до него дойдет первый Resp. Если все Req проходят через актор-коллектор, то актор-коллектор может быть способен выявить дубликаты Req и устранить их.


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



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


    Если SEDA-подход так хорош, то почему бы не использовать только его и вообще обойтись без акторов?


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


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


    Именно так дело обстоит с моделью акторов: где-то достаточно только ее. Где-то нам приходится бороться с ее недостатками. Где-то мы ее вообще не используем.


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


    Кроме того, на практике у вас встанет вопрос: а что из себя представляют сущности-стадии на уровне реализации? Будет ли каждая стадия потоком ОС? Или это будет объект с методами-коллбэками? Будет ли логика работы стадии описываться конечным автоматом? Должна ли стадия выполнять какие-то действия, пока для нее нет новых заявок на обработку, или же стадия должна спать в ожидании новых заявок? В итоге может оказаться, что в коде сущность-стадия будет тем же самым актором, только в профиль. Ну а раз так, то модель акторов и готовые ее реализации просто-напросто напрашиваются в качестве готовой базы для реализации сущностей-стадий.


    Обезличенный пример из собственного опыта


    Мы сами не сразу пришли к пониманию выгод комбинирования модели акторов и SEDA-подхода и успели набить себе некоторое количество шишек, часть из которых описывалась в двух написанных ранее статьях (№1 и №2). Но даже когда мы начали комбинировать эти два подхода, то так же не сразу определили для себя модель, которая удовлетворяет именно нашим условиям.


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


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


    Для обработки каждого потока было несколько стадий. Каждую стадию обслуживала пара акторов (коллектор и исполнитель). Актор-исполнитель работал на потактовой основе с типичным тактом в 250ms (хотя шаг таймера настраивался и можно было сделать такт в 50ms или в 2s). На очередном такте актор-исполнитель просыпался, забирал у актора-коллектора все накопленные сообщения, после чего фиксировал новые сообщения в БД, выбирал сообщения, которые были готовы к отправки на следующую стадию и отсылал их, обрабатывал накопленные ответы, фиксировал их в БД (доставкой зафиксированных ответов занимались другие акторы). После чего проверял, осталось ли еще время на текущем такте. Например, вся работа могла занять 200ms из 250ms выделенных под такт. В этом случае актор-исполнитель засыпал на оставшиеся 50ms и просыпался лишь на следующем такте. Если же операции на текущем такте заняли слишком много времени, скажем, потребовалось 300ms вместо 250ms, то следующий такт начинается сразу же.


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


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


    Заключение


    В этой статье мы попробовали немного рассказать о своем опыте комбинирования модели акторов и SEDA-подхода. Надеемся, что наш рассказ окажется для кого-то интересным и полезным. Однако, мы понимаем, что модель акторов сейчас используется много где, кроме того, находящиеся в эксплуатации реализации модели акторов очень сильно отличаются друг от друга (даже если они сделаны для одной платформы, как, например, Akka и Quasar для JVM). Поэтому имеет смысл сделать поправку на ветер и радиус кривизны наших рук при попытке переиспользовать наш опыт у себя.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 38

      0
      Заинтересовавшись когда-то подобной темой, с удовольствием прочитал книгу «Шаблоны интеграции корпоративных приложений» Грегори Хопа, описывающую в подробностях системы обмена сообщениями.
        0
        Акторы можно классифицировать по многим признакам, но самым важным является допустимое количество входов. У акторов — идейных наследников сетей Петри (workflow, dataflow) количество входов не ограничено. У акторов Хьюита (Akka) входа ровно два — для входящих сообщений и для внутреннего состояния.
        Почему-то авторы, пишущие об акторах, как правило, знакомы только с моделью Хьюита. Отсюда утверждения типa «акторы не защищены от перегрузки». Акторы Хьюита — да, не защищены, потому что нельзя увеличить число входов. Dataflow актор защитить от перегрузки легко — стоит лишь добавить обратную связь по переполнению и завести ее на добавочный вход.
          0
          Мне казалось, что «модель акторов» (она же Actor Model на английском) — это именно то, что было озвучено сперва Хьюитом, а потом подхвачено Клингером и Агха. Об этой модели акторов и идет речь.

          Где можно прочитать про «модель акторов» с другими корнями?
            0
            1. A STRUCTURED DESCRIPTION OF DATAFLOW ACTORS AND ITS APPLICATION. Johan Eker J¨orn W. Janneck
            2. Robust Workflows for Science and Engineering. David Abramson, Blair Bethwaite, Colin Enticott, Slavisa Garic, Tom Peachey Anushka Michailova, Saleh Amirriazi, Ramya Chitters

            Собственно проблема в том, что при описании dataflow и workflow сетей (а по большому счету, это одно и то же) узлы этих сетей обычно называют не акторами, а как-нибудь по другому, хотя они именно акторы и есть.
            Так что читайте все подряд по dataflow и workflow.
              0
              Собственно проблема в том, что при описании dataflow и workflow сетей (а по большому счету, это одно и то же) узлы этих сетей обычно называют не акторами, а как-нибудь по другому

              Ну да. Там будет про dataflow/workflow. Но не про «традиционную» модель акторов от Хьюита.
              хотя они именно акторы и есть

              Или это вам лично так кажется.

              Чтобы было понятно: есть уже давно ставшим общеупотребительным термин «модель акторов», под которым более-менее понятно, что понимается. Вы, как приверженец dataflow-модели, можете считать, что модель акторов есть частный случай dataflow-модели. Однако вряд ли ваш взгляд на вещи найдет широкое понимание. У меня, например, не находит.
                0
                Вероятно, вы и арифметические операции с 2 параметрами не рассматриваете как частный случай математических функций со многими параметрами?
                  0
                  Сильно зависит от контекста. Если для обучения арифметическим операциям детей в начальных классах школы — то, конечно же, не рассматриваю. При этом вы уходите на доказательства по аналогии, что суть демагогия.

                  Проблема в том, что вы выдвинули тезис о том, что actor model — это частный случай dataflow. При том, что термин actor model уже является устоявшимся термином. И нужно что-то более существенное, нежели аналогия с арифметическими операциями, для обоснования вашего термина.

                  Видите ли, вы ссылаетесь на работу от 2003-го года, в которой авторы сами пишут:
                  In all dataflow models of computation, the model components (which we call actors,
                  and which might also be called processes in other models of computation) communicate
                  by sending each other packets of data called tokens along unidirectional channels with
                  exactly one reader and one writer.

                  Они английским по белому говорят «которые мы называем _акторами_, но которые могут быть названы процессами в других моделях». Т.е. авторы одной работы, вышедшей через 30 лет после работы Хьюита используют термин _актор_ в контексте dataflow-а. При том, что они пытаются ввести такое понятие, как dataflows actors. Не просто actors, а именно dataflow actors.

                  Больше напоминает, что вы прочли про dataflows actors, вам показалось, что это обобщение, в том числе, и Хьюитовских акторов, и теперь вы носитесь с этой идеей. Ничего не имею против ваших личных взглядов на проблемы dataflows actors.

                  Однако, в статье речь идет про Хьюитовских акторов и их проблемы, в том числе и проблемы отсутствия защиты от перегрузки. А вовсе не о так любимых вами dataflow actors.
                    +1
                    аналогия не всегда демагогия. В данном случае аналогия самая прямая. Акторы — это асинхронные функции. То есть, функции, исполняющиеся, когда есть все необходимое для их исполнения — входные данные и вычислительные ресурсы. И выделять акторы с 2 входами в отдельную категорию так же наивно, как выделять сложение и умножение из всего множества математических функций только на том основании, что у них 2 аргумента. Да, это может иметь педагогический смысл, но вы же не хотите оставаться на уровне начальной школы?
                      0
                      И выделять акторы с 2 входами в отдельную категорию так же наивно, как выделять сложение и умножение из всего множества математических функций только на том основании, что у них 2 аргумента.

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

                      Сюрприз, правда?

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

                      Вот и получается, что с точки зрения теории и досужих рассуждений с доказательствами по аналогии — у вас одно. А на практике у меня, скажем, несколько другое. И какой-то дополнительный «вход» для обратной связи привязывать-то и некуда.
                        0
                        С таким же успехом вы могли бы утверждать, что рекурсивных функций на практике не существует, поскольку вы программируете на Фортране IV.
                          0
                          Опять доказательство по аналогии.

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

                          Тем, кому приходится с этим жить, как-то все равно, есть ли теоритические работы, описывающие общие случаи акторов с N входами и M выходами, или таковых нет. А если они и есть, то как там обстоят дела с защитой от перегрузки (пока что советы теоритиков вызывают смех в зале).

                          Попробуйте дать обычному разработку на Akka формулы из статьи «A STRUCTURED DESCRIPTION OF DATAFLOW ACTORS AND ITS APPLICATION», на которую вы ссылаетесь. И посмотрите, как это поможет при разработке реальной программной системы.
                            0
                            Есть и практические реализации акторов с N входами, например, TPL dataflow, Intel Threading Building Blocks.
                            А в Akka вроде уже добавили защиту от перегрузок, во всяком случае, понятие back-pressure упомянуто в http://doc.akka.io/docs/akka/2.5.3/scala/stream/stream-flows-and-basics.html
                              0
                              Вот только в Intel TBB понятие актора не используется, насколько я помню.
                              Так что исходный тезис о том, можно ли отождествлять dataflow и actor model, все еще нуждается в доказательстве.

                              Если вы посмотрите на back-pressure из Akka, то увидите, что этот back-pressure сделан не для акторов, а для Akka Streams, дополнительной штуке, которая существует сбоку от акторов.
                      0
                      «Проблема в том, что вы выдвинули тезис о том, что actor model — это частный случай dataflow».

                      Не вижу здесь никакой проблемы. Узлы dataflow сети названы акторами уже в 1975 году, в одной из первых работ по dataflow: Jack B. Dennis, “First Version of a Data Flow Procedure Language”. То, что actor model Хьюита — частный случай dataflow, ясно любому, кто пытался реализовать и то, и другое.
                        0
                        ясно любому, кто пытался реализовать и то, и другое.

                        Где можно посмотреть на ваши реализации?
                            0
                            Спасибо, повеселили еще раз. Это что, тестовое задание для трудоустройства в LuxSoft?
                              0
                              первая ссылка — да, тестовое задание, но не в люксофт. Им требовалась именно квалификация в многопоточности. Решение их не устроило, сказали что некорректное. А теперь поделитесь, что именно вас развеселило.
                                0
                                После слов:
                                То, что actor model Хьюита — частный случай dataflow, ясно любому, кто пытался реализовать и то, и другое.
                                хотелось увидеть что-то уровнем повыше студенческой курсовой.
                                  0
                                  И что, это вас развеселило? Не оправдавшиеся ожидания обычно огорчают.
                                  Я же написал: «ясно любому, кто пытался реализовать и то, и другое». Пытался. Не обязательно довел до совершенства. Потому что идейная сущность (то, что actor Хьюита — частный случай dataflow), она становится ясной довольно быстро. После чего становится скучно. И это не бог весть какое открытие, именно на уровне студенческой курсовой. А вы что, вообразили, что у нас спор кандидатов на премию Тьюринга?
                                    0
                                    Не оправдавшиеся ожидания обычно огорчают.
                                    Так я увидел как раз то, что ожидал.

                                    Ну и повторю еще раз: вы, лично вы, можете видеть что угодно и где угодно. Считаете вы, что акторы Хьюита — это частный случай dataflow actors — никто вам этого не запретит. Статья не про это, а про то, что делать в случае акторов Хьюита.

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

                                    Вот так же и с акторами, когда их пытаются применить в задачах посложнее студенческой курсовой.
                                      0
                                      «что делать в случае акторов Хьюита» — да ничего вы не сделаете. Признайтесь себе, наконец, что акторы Хьюита не годятся для задач сложнее студенческой курсовой. Подключите Akka Streams, например.
              0
              Dataflow актор защитить от перегрузки легко — стоит лишь добавить обратную связь по переполнению и завести ее на добавочный вход.

              Попутно вопрос: допустим, есть актор B, на которого сыпятся сообщения от акторов C, D, E и далее по списку. Общий темп отсылки сообщений актору B превышает способности B по обработке входящих сообщений. Этот самый «добавочный вход» кому нужно завести и куда?
                0
                от актора B на акторы C, D, E, а если найдется узел, питающий C, D, E, тогда на него.
                  0
                  Ну и зачем этот вход акторам C, D и E? Более того, откуда этот вход будет появляться во run-time, если, скажем, ссылку на B актор C получает в run-time, отправляет актору B одно-единственное сообщение и выбрасывает данную ссылку как ненужную?
                    0
                    этот вход акторам C, D и E нужен, чтобы вести себя прилично в обществе. И закладывать его надо при описании, а не в рантайме. Если же, как вы сказали, ссылку на B актор C получает в сообщении, то здесь есть разные варианты. Например, обязать посылающего, чтобы он зарезервировал у B место для приема сообщения от С. Или разбить актор С на 2 стадии — первая получает ссылку на B и делает запрос к B на резервирование места, посылая в запросе ссылку на вторую стадию. Когда у B освобождается место, он оповещает вторую стадию. В любом случае, у акторов необходимо размещать дополнительные управляющие входы, которые блокировали бы их работу до получения необходимого набора ресурсов.
                      0
                      Или разбить актор С на 2 стадии — первая получает ссылку на B и делает запрос к B на резервирование места, посылая в запросе ссылку на вторую стадию.

                      Спасибо, повеселили.

                      Вот этот «запрос к B на резервирование места» — он какой будет? Синхронный или асинхронный? Если асинхронный, то чем этот запрос отличается от простой и тупой прямой отсылки нужного сообщения? И кто гарантирует, что сам этот запрос не станет причиной перегрузки B?
                        0
                        Асинхронный, естественно. А отличается тем, что общее число таких запросов ограничено общим числом акторов. Отсюда и решение — пусть актор (точнее, один из его входов) сам выступает в качестве запроса, а очередь запросов сделать в виде списка, а не массива. Тогда постановка такого запроса не потребует дополнительной памяти и не сможет вызывать перегрузку.
                          0
                          А отличается тем, что общее число таких запросов ограничено общим числом акторов.

                          А общее число акторов мы знаем откуда?

                          Вы, походу, слишком долго варитесь в области dataflow-программирования. Это там специфика такая, что количество вычислительных сущностей (узлов, процессов) и их взаимосвязи известны заранее.
                          Отсюда и решение — пусть актор (точнее, один из его входов) сам выступает в качестве запроса, а очередь запросов сделать в виде списка, а не массива.

                          Либо вы предлагаете то, чего я не понимаю, либо ваша идея в том, чтобы самих акторов прописывать в интрузивный список. Что есть отстой. Ибо работать будет только для ограниченного типа сценариев (например, когда понятно, что запрос типа X к актору B любой другой актор не может отсылать более одного раза.
                            0
                            А общее число акторов нам знать и не нужно. Мы его нигде не используем.
                            Это в хардверном dataflow количество вычислительных сущностей известно заранее, в силу естественных причин. А в софтверном нам никто не запрещает генерить акторы динамически.
                            Не акторов, я уточнил — входов. Еще точнее — выходов. И запрос у нас не типа X, а запрос на резервирование места под запись. Такие запросы не нужно отсылать более одного раза — достаточно в запросе указать количество запрашиваемых мест.
                              0
                              Вы говорите так, что я никак не могу понять, в чем ваша идея состоит.
                                0
                                Да и фиг с ней. Идея так себе. Если тормозить акторы C, D, E, чтобы они не перегружали B, то они сам окажутся перегруженными. Надо смотреть на корневой источник нагрузки, например, подключения клиентов по сети, и уже этот источник подтормаживать.
                  0

                  Посмотрите http://www.reactivemanifesto.org и связанные имплементации. Там эта тема фигурирует под названием http://www.reactivemanifesto.org/glossary#Back-Pressure

                    0
                    «Связанные имплементации» — это какие именно? Уже упоминавшиеся выше Akka Streams?
                      0
                      Простите, правильнее было бы давать эту ссылку http://www.reactive-streams.org/announce-1.0.0
                        0
                        Ну так первая имплементация из перечисленных там — это Akka Streams и есть, поскольку сам Reactive Manifesto делался, в том числе, и людьми, стоящими за Akka. В том числе и для продвижения Akka.

                        Тем не менее, посмотрите на спецификацию этих самых reactive streams. Там subscriber должен вызвать Publisher.subscribe для того, чтобы получать сообщения от конкретного publisher-а.

                        Что для моего примера выше означает, что актор B должен сперва подписаться на паблишеров C, D, E и всех прочих. Это совсем не та ситуация, о которой я говорю.
                0

                Немного заблудился в комментариях. О какой ситуации говорите вы?

                  0
                  Вот об этой. Суть в том, что B не должен знать ни про C, ни про D, ни про E. Да и C, D и E могут узнать про B только на короткий момент времени, после чего забудут о нем.

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

                Самое читаемое