Архитектуры на акторах: вступление
Архитектуры на акторах:
Вступление (Actors / Objects; Complexity; When to Distribute; Kinds of Actors)
Монолиты (Control/Data Flow; Reactor / Proactor / Half-Sync/Half-Async; Request State)
Простые системы (Notation for Diagrams; Sharding; Layers; Services / Pipeline)
Системы с моделью (Hexagonal / MVC; Blackboard / Message Bus; Microkernel / Plug-ins)
Фрагментированные системы (Mesh; SOA; Hierarchy; Design Space)
Говорящий не знает, а знающий – не говорит
(БГ)
Какое-то время пытался найти в литературе или интернете описания бекенд-архитектур, построенных из акторов. Спрашивал здесь на форуме. Безрезультатно. При этом в эмбеддед телекоме они распространены, но тоже нет литературы. Поиски работы показали, что за такие знания не платят, поэтому выкладываю опыт предыдущих лет в опенсорс. Анализ большей частью проведен для локальных систем (обмен сообщениями между потоками), но есть шанс, что многие утверждения подходят и для распределенных сервисов. Надеюсь, будет интересно. И надеюсь на фидбек и исправления — у меня нет опыта в бекенде. Если зайдет — можно попытаться оформить публикацию.
Статья открывает небольшой цикл, цель которого — исследование возможных применений акторов в различных типах систем и определение архитектур (статические диаграммы, композиция), в которых их можно использовать. Акторы известны давно (1973), и много лет используются в embedded, telecom, и других областях реального времени. Также, многие современные архитектуры обработки данных по сути сводятся к акторам (просто об этом вслух не говорят). Более того, ООП вначале было сформулировано как взаимодействие объектов через обмен сообщениями (по сути — система акторов), и только позже превратилось в более удобные для написания бизнес-логики синхронные вызовы методов, которые стали стандартом программирования лет 30 как. Итак,
Что такое Акторы?
Актор — это сущность с определенным поведением (код) и состоянием (данные), которая взаимодействует с другими подобными сущностями (акторами) только асинхронной посылкой сообщений. В любой конкретный момент актор или ожидает входящего сообщения (спит), или обрабатывает одно из сообщений. Обычно входящие сообщения складываются в очередь (мейлбокс), а поток-обработчик их оттуда по одному вынимает и процессит. Наверное, знакомо.
Что здесь интересного:
- К памяти (состоянию) актора никто кроме самого актора не имеет доступа.
- По сути, актор является стейт машиной (конечным автоматом), управляемой входящими сообщениями.
- Если внутри актора нет недетерминистических вызовов (работа с ОС), то каждый раз, посылая актору в данном стартовом состоянии одну и ту же последовательность сообщений, будем получать одинаковый результат (event sourcing [MP, DDIA] же). Таким образом, записав события при п(р)оявлении бага, мы можем его выдебажить в день сурка.
- Так как (по классике) актор обрабатывает в любой момент не больше одного сообщения (он однопоточный), и так как в его данные никто не лезет, и он не лезет в чужие — про мютексы и синхронизацию можно забыть всерьез и надолго (кроме защиты очереди входящих сообщений, которая пишется один раз на старте проекта, или предоставляется фреймворком).
- Так как мы забыли про мютексы и синхронизацию, а память между акторами не шарится, у нас де факто нулевые потери производительности на синхронизации потоков — все ядра используются на 100%, если в системе достаточно нагрузки, чтобы акторам было чем заниматься, и акторов создано больше, чем ядер проца, и кеши памяти хорошо работают (хотя в теории есть еще синхронизация на месседжинге и на менеджере памяти).
- Актор очень похож на объект. Отличается тем, что интерфейс у объекта — синхронный, а у актора — асинхронный. Но это отличие приводит к тому, что очень неудобно писать логику, затрагивающую несколько акторов. Сообщения работают по методу fire and forget, то есть, мы не можем заблокироваться и дождаться ответа на наш запрос о чем-то к соседнему актору. Если ответ когда-то и придет, то перед ним может прилететь куча левых запросов, или отмена того самого запроса, на который хотим ответ. В общем, код ужасный, дебажить нереально, и туда лучше не ходить, и ничего там не делать.
- Зато внутрь актора можно запихать что угодно, хоть всю бизнес-логику программы, если в ней каждый запрос получается быстро проработать. То есть, никаких проблем с заворачиванием объектов в акторы. Как, в принципе, и наоборот — акторы за интерфейсом объектов (Active Object [POSA2]).
В части языков есть поддержка отсылки сообщений по каналам (Go), в части — отсылка на адресат (Erlang/Elixir). Разницы никакой, и если поддержки из коробки нет — акторы легко пишутся поверх потоков.
Для передачи сообщений могут использоваться подписки (pub/sub [EIP]), шина сообщений (message bus / event bus [EIP]), или прямая отсылка в очередь/сокет. Принципиальной разницы нет, кроме того, что если акторы живут в одном процессе (шарят адресное пространство), можно не сериализовать сообщения (отсылать древовидные структуры в явном виде, не копировать большие куски памяти).
В системе могут использоваться пары сообщений request/confirm, нотификации событий, или оба варианта вперемешку.
Акторы против объектов
Объекты | Акторы | |
Интерфейс | Синхронный (вызов метода) | Асинхронный (отправка сообщения) |
Работа в системе | Последовательная | Параллельная |
Детерминизм системы | Да если однопоточная | Нет |
Энкапсуляция | Частичная (публичные данные) или полная | Полная (доступен только интерфейс) |
Полиморфизм | Да при наследовании/имплементации интерфейса (статическая типизация) или одинаковых именах методов | Да при одинаковом интерфейсе (и никто не знает, что внутри) |
Наследование имплементации | Да, часто | Редко, но возможно |
Клонирование (копирование) | Да, иногда | Да, иногда |
Кроме объектов, синхронно вызывающих друг друга в одном потоке, и акторов, асинхронно кидающих друг другу нотификации, есть смесь — Remote Procedure Call, когда объект дергает актор (отсылает запрос) и ожидает, пока актор обработает вызов (пришлет ответ). Плюс — более простое программирование внутри вызывающего объекта (последовательное как для обычного вызова метода). Минус — тормоза системы из-за использования сообщений и возможная нестабильность или зависание, если вызываемый актор упал. То есть — серединка (компромисс).
На диаграмме можно заметить:
- Прямые вызовы (объекты) работают в одном потоке, и вся цепочка событий выглядит проще и быстрее других.
- У пересылки событий (акторы) клиентский поток быстрее всего освобождается — он ничего не ждет.
- При пересылке событий (акторы) сервисы выполняют задание одновременно.
- При удаленном вызове (RPC) нет ни одного из перечисленных преимуществ, и этот сценарий — самый длинный.
И объекты, и акторы являются модулями (которые в последнее время мо(д/ж)но называть сервисами. Вот, собственно, вопрос: когда мы бьем систему на части, что лучше использовать в качестве частей: объекты или акторы? Или смесь синхронных и асинхронных интерфейсов (как многие современные микросервисы)?
Объекты не зря долго были (и сейчас есть для простых систем) решением по умолчанию — их намного проще отлаживать (дебаггер нормально работает так как нет асинхронности; однопоточная система из объектов в общем случае детерминистична и баги воспроизводятся). Рассмотрим случаи, в которых акторы получают преимущество:
- Мы хотим делать несколько дел одновременно (для отзывчивости или использования всех ресурсов процессора/кластера). Многопоточность затирает многие преимущества объектов, либо критические секции (синхронизация) сильно мешает эффективной многопоточности.
- Нашу систему формируют противоположные наборы сил, например: нужно одновременно работать с файлами / длительными расчетами, и быстро обрабатывать прерывания / показывать юзеру мультики. Мы можем во время расчета переключаться на анимацию, рендерить следующий кадр, и продолжать расчет, но код будет некрасивым и у расчетов, и у видео. Лучше их разделить на независимо работающие подсистемы — акторы. Расчет будет бежать линейно / рекурсивно и непрерывно в бекграунде, а анимация — обрабатывать события (таймер, клики) в более приоритетном форграунде, прерывая (если проц одноядерный) поток с расчетами.
- Требуется очень шустро реагировать на события, что нереально сделать в огромной синхронизированной кодовой базе (монолите), или
- Требуется легко разрабатывать и деплоить относительно независимый кусок, не завязнув в кодовой базе монолита.
- Мы хотим гарантировать, что соседняя команда не вставит нам костыль куда не нужно, чтобы вытянуть себе какие-то данные. С акторами они просто не смогут взаимодействовать мимо интерфейса, даже если дедлайн и ПМ и горит.
- Система тупо не влазит на один компьютер (по процу, памяти или диску).
- Код настолько разросся, что никто ничего не понимает. Надо переписать по кускам, но так, чтобы история не повторилась (см. пункт 5).
Во всех перечисленных случаях имеет смысл подумать о разделении системы асинхронным интерфейсом с последующим разнесением частей на разные компьютеры, если будет необходимость. Как только мы создали асинхронный интерфейс, и не пытаемся его использовать как синхронный (RPC) — полпути к распределению по нескольким серверам сделано.
Типы систем
Рассмотрим сферический проект в вакууме. На нем пишут идеальный код, который идеально компонуется, и дает возможность компилятору идеально все соптимизировать. Данные идеально расположены в плоскую структуру, и все правильно работает. Это — мир идей, в простонародье известный как спагетти, функциональное или процедурное программирование.
Когда он сталкивается с требованиями бизнеса, проект становится менее сферическим, и более большим. И у архитекторов начинаются проблемы с осознанием всех взаимодействий в домене и выведением из этих взаимодействий единственно верной архитектуры. Как в бородатой истории о временах, когда трава была зеленая, и автор The Timeless Way of Building только начинал изучать даосизм. Как-то команде архитекторов выдали энтерпрайз. Они его и туда, и в другое место — а он не дается. И поняли архитекторы, что надобно расширить сознание. Запаслись доскою, мелками (маркеры в те дни еще не покинули домен коллективного бессознательного) и приняли на ночь немного ЛСД. И посетило их озарение, и изрисовали они доску от края до края, чтобы утром не забыть какая для этого энтерпрайза правильная архитектура. И когда пришло утро, доска все еще была полна информации, но никто из архитекторов не мог осознать целиком запечатленную на доске картинку.
Вот в такие моменты просветления люди задумываются о смысле модульности. И код, и данные делятся по минимально связанным субдоменам, которые уже проще усвоить по-отдельности. Это — модульность, также известная как ООП. Бухгалтер занимается бумажками, сисадмин — разгружает контейнера. Разделение труда.
Но потом наступает жестокая реальность с нефункциональными требованиями, и модульность закапывают туда же, куда недавно проводили осознание доменной модели целиком. Потому что оказывается, что от разных модулей хотят сильно разного, странного, и мы вот это вот все странное никак не можем увязать вместе синхронными связями. И мы делаем модули асинхронными и независимыми. Это — акторы, или (микро-)сервисы.
Если архитектор — энтузиаст, и не знает меры, а дао — далеко и недостижимо, то он свободен сделать еще один шаг, и познать дивный новый мир равиоли, в котором нет синхронных вызовов — все работает через нотификации, прям как в физическом мире. Это — царство наносервисов, также недавно описанное как машина Кузьмина.
Из интересного, но не нарисованного: у хорошего монолита выше пропускная способность (эффективность числомолотилки на одно ядро проца), а у акторов — проще управлять временем отклика (шустрые реал-тайм системы).
Сложность
Сложность — это то, насколько тяжело изменить систему требуемым образом, и не внести при этом в ее поведение неожиданных эффектов. Обычно при росте модели сложность будет расти более чем линейно, то есть, при любых умственных способностях где-то недалеко есть предел размера проекта, за которым фиг сможешь долго нормально работать (можно работать недолго, пока новые баги еще не стали очевидны для пользователей). Различимы следующие основные виды сложности:
- Сложность кода (спагетти): поведение кода, определяющего любую конкретную функциональность, зависит от кода, определяющего другую функциональность. В результате, чтобы понять, как работает один кусок кода, надо разобраться в нескольких других, а чтобы разобраться в них — надо понять остальной код на проекте.
- Сложность интеграции (равиоли): для того, чтобы модуль, определяющий любую конкретную функциональность, работал ожидаемым образом, требуется определенная конфигурация других модулей в системе. А для каждого из других — третьих. В результате, чтобы изменить интерфейс одного модуля, либо понять систему в целом, надо знать все взаимодействия между модулями в системе.
Общую сложность можно считать суммой двух предыдущих.
Для борьбы с одним видом сложности его переводят в другой. В частности, если проект начинается как моноблок, то для того, чтобы все не зависело от всего, вводят модульность: код и данные разделяются интерфейсами на компоненты. В результате уменьшается зависимость кода внутри любого компонента от кода внутри других компонентов (в идеале, для понимания работы компонента достаточно видеть его код, интерфейс и контракт), но появляется сложность интеграции компонентов (как за деревьями увидеть лес? Что вообще вот на этой огромной системной диаграмме нарисовано?). Чем равномернее мы можем распределить сложность между кодом внутри модулей и интеграцией между модулями — тем быстрее будет разработка на большом проекте, и тем большего размера проект мы сможем сносно поддерживать.
Здесь можно упомянуть дядю Боба с рекомендацией делать методы по 7 действий на метод. Такой подход борется со сложностью кода (спагетти), перенося ее в сложность интеграции (равиоли). Проблема в том, что когда у нас на проекте в несколько миллионов строк кода получаются сотни тысяч методов — любой программист просто запутается между этими методами, вне зависимости от того, они лежат в нескольких классах по 10 000 методов в каждом (God Objects) или там 10 000 разных классов по 10 методов у каждого (Ravioli code). От применения правила лучше не стало — стало невозможно работать. Лучше станет, когда при росте проекта мы одновременно и примерно равномерно наращиваем и количество кода в методах, и количество методов в классах, и количество классов в модулях, и количество модулей в сервисе, и количество сервисов в системе. В таком идеализированном случае общая сложность равномерно распределяется по нескольким уровням(типам), и ни на одном из уровней не зашкаливает.
Теперь сравним приведенные выше типы систем (по количеству и жесткости интерфейсов) и метрики сложности:
Получается, что для больших проектов идеальной будет серединка между модульным ООП и асинхронными акторами. Эта common wisdom называется Микросервисы: ООП используется (и берет на себя часть общей сложности) внутри сервиса, асинхронные интерфейсы — между сервисами.
Распределяемость
Еще один фактор, обеспечивший успех микросервисов при разумном подходе (голова может помочь избегать правил (в идеале — любых, обычно — неприменимых к текущей ситуации), если ею привыкнуть думать (или, как любил повторять классик, ‘Think’ is not a four-letter word)) — легкость распределения по (физически) независимым серверам сравнительно с модульным приложением.
Распределенная система — это когда один компонент не имеет эффективного доступа к памяти других компонентов. При этом нет разницы, мы искусственно ввели такое ограничение для акторов, живущих в одном процессе, или у нас взаимодействуют процессы на одной машине, или процессы раскиданы по разным машинам в сети (худший вариант из-за нестабильности связи и расхождения системного времени [DDIA]). Свойства распределенной системы более-менее одинаковы для всех случаев:
- В системе логически (часто и физически) одновременно производится много действий — каждый компонент делает свою работу.
- Взаимодействие компонентов (часто очень) дорогое по сравнению с действиями внутри одного компонента.
- Синхронизация состояния компонентов исключительно дорогая. То есть, при нормальной работе системы никакой компонент не может знать текущее состояние никакого другого компонента.
Когда компоненты связаны синхронно (RPC между ООП модулями — распределенный монолит), недоступность одного из компонентов, вероятно, остановит работу остальных эффектом домино. Любой вызов метода недоступного компонента блокирует вызывающий модуль (скорее всего, с таймаутом и исключением после таймаута, но все равно — скорость работы сильно нарушена, и может произойти распространение ошибки (таймаута) по цепочке вызовов между компонентами.
В асинхронной системе (месседжинг между акторами) стараются избегать зависимостей одного актора от состояний других акторов (просто потому, что такие зависимости исключительно неудобно программировать и отлаживать). В результате взаимодействия больше сводятся к нотификациям, и если соседний сервер упал — мы об этом, скорее всего, не узнаем — ну он не обработает наши нотификации (пока не поднимется), и мы какое-то время не будем получать нотификаций от него (занимаемся своими делами). Даже в некрасивой ситуации, когда нам для нашего запроса нужны данные от соседа, а он уснул — мы как асинхронный актор обрабатываем все остальные запросы, а тот, для которого нужны были данные — лежит в сторонке, используя килобайт оперативки.
Вот эта разница в отказоустойчивости тоже сделала вклад в переход от синхронных модульных систем к асинхронным сервисам.
Принцип построения распределенных систем
Чтобы пытаться делать задачу просто, и не делать сложные задачи (KISS, правило 20/80), нужно примерно понимать, что для нас просто, а что — сложно (кэп).
Отсортируем взаимодействия в системе акторов (асинхронной распределенной системе) по возрастанию сложности:
- Локальная обработка входящего события / нотификации. Очень быстро, без заметных побочных эффектов, занимает одно ядро процессора и не мешает другим акторам. Событие может либо прийти локальным прерыванием / колбеком от железа или ОСи, и так попасть в очередь сообщений, либо как нотификация / подписка от внешнего агента. Идеал.
- Обработка запроса с отсылкой ответа. Тоже хорошо, чуть больше забот для формирования обратного сообщения (confirm) с результатами обработки входящего запроса (request).
- Событие, вызывающее multicast нотификации. Произошло что-то важное, о чем нужно сообщить другим компонентам (акторам). Мы делаем свою часть работы (вероятно, преобразуем логику и данные, присланные внешним источником, в наш внутренний удобный формат — (AntiCorruption Layer [DDD])), а дальше — отсылаем высокоуровневое сообщение с полезной информацией тем, кому она нужна (всем подписчикам или конкретному модулю). Так как в данном случае сообщения распространяются по системе, он дороже предыдущих по затратам ресурсов, и больше времени пройдет, пока вся система адаптируется к / изменит состояние соответственно входящей нотификации (все views отобразят данные из event source, eventual consistency [DDIA]).
- Цепочка обработки / распределенный сценарий. Входим в область хореографии [MP]. Нам пришел запрос, но мы не компетентны решить, как на его отреагировать. Мы перерабатываем формат данных в принятый для нашей системы [DDD], отрезаем лишние данные [EIP], добавляем полезную информацию из нашего локального состояния [EIP], и форвардим получившийся высокоуровневый запрос в модуль, который заведует соответствующей функциональностью. Процесс может повторяться, пока кто-то не примет ответственности за решение и не пошлет ответ обратно по цепочке. Результат — медленная обработка запроса (много асинхронных шагов, акторы в цепочке могут быть заняты чем-то другим и запрос провисит в очереди). Но тут еще спасает то, что решение (логика) принимается локально в одном конкретном акторе.
Вариант: актор, принявший решение, не отсылает обратно по цепочке ответ, а рассылает нотификации об изменении состояния системы. Мы подписаны на такие нотификации, видим, что состояние изменилось в степени свободы, связанной с оригинальным запросом, и отправляем на него ответ. Цикл в графе рассылки. - Решение требует информации о состоянии других акторов. Мы не можем найти кого-то, кто более компетентен для принятия решения, чем текущий актор, но в нем нет всех нужных данных. Для решения потребуется разослать всем вовлеченным акторам запрос на получение данных, запомнить ответы, объединить полученную информацию, и принять на основе ее решение. По сути — получаем распределенный join / создаем views [DDIA]. Сценарий осложняется тем, что пока мы опрашиваем соседей, их состояние может меняться, то есть — наша выборка (join) может состоять из данных (views), относящихся к несовместимым снепшотам общего состояния системы (базы) [DDIA]. Ну и скорость и стабильность сценария будет хуже, чем у самого худшего из участников. И много ненужного копирования данных по системе. И нам надо наворотить много кода и потратить много рамы, чтобы такое взаимодействие реализовать.
Вывод: это красный флаг. Как минимум — новые требования пошли вразрез с нашим изначальным пониманием домена, исходя из которого писалась архитектура (неправильная граница разбития на субдомены), и мы делаем в 10 раз сложнее то, что в монолите — один вызов метода объекта. Как максимум — мы захотели попробовать микросервисы, и теперь начинаем огребать (слишком мелкие субдомены). - Обработка запроса должна синхронизировать состояние нескольких акторов. Если будет запоздание между изменениями их состояний, нарушится системный инвариант, и вся система начнет принимать неверные решения.
Либо: нам надо собрать данные с нескольких акторов (компонентов) и их отредактировать. При этом недопустимо, чтобы кто-то другой изменил эти данные, пока мы их редактируем (конфликт версий).
Поздравляю (R.I.P.): у нас распределенная транзакция [DDIA]. Это вообще не к акторам. Архитектура летит нафиг. Мы пишем распределенный монолит [MP]. В принципе, каждый конкретный случай патчится оркестратором [MP], но очень очень вероятно, что систему разбили на слишком мелкие куски, и пора или переписать все (говорят, нормальный код получается после двух-трех итераций), или уволиться.
Исключение — энтерпрайзные микро(?)сервисы с сотнями программистов, когда объем кода и нагрузка для даже одного субдомена разрослись настолько, что субдомен приходится резать как угодно, лишь бы разделить на части — с целым субдоменом уже организационно нереально справиться. Тут или все будет плохо (шарик испортится, и прочие оркестраторы), или проект можно сразу закапывать из-за сложности домена и количества требований. Но это тоже не значит, что не пора уволиться.
Разновидности Акторов
Идея акторов слишком проста и очевидна, чтобы не вызвать множество извращений. Вот наиболее распространенные варианты:
Использование потоков
- Ванильные акторы (любой язык с потоками и даже Unix pipes). Каждому актору выделен свой поток или процесс, скедюлингом занимается ось. При этом при создании акторов их потокам можно задать реал-тайм приоритеты, настроив скедюлинг под нужды системы.
- Общий пул потоков aka fibers (Эрланг/Эликсир из коробки, можно написать на С/С++). Здесь мы создаем по потоку на ядро процессора, прибиваем их гвоздями (CPU affinity), и пишем свой скедюлер. Свободный поток проходится по известным акторам и запускает среди них того, у которого в очереди лежит сообщение. Можно складывать готовых к работе (имеющих сообщение) акторов в очередь с приоритетами по реалтаймовости (насколько важна быстрая реакция) данного актора. Мы не переключаемся в ядро ОС, когда у текущего актора закончились сообщения в очереди. Также интересно то, что здесь можно плодить миллионы акторов так как пассивный (ждущий пока дадут проца) актор не имеет своего стека, соответственно, вообще не использует лишней памяти. Актор весит как обычный объект + текущие сообщения в очереди. Только вот зачем нам эти миллионы?
- Пул потоков с квотами времени (Эрланг/Эликсир). Скедюлер не ждет, пока текущий актор обработает сообщение, а может переключаться между акторами по таймеру. В результате, если есть прожорливый или зависший актор, то он не мешает остальным — прожорливому достается только незанятый более быстрыми соседями остаток времени процессора.
- Многопоточные акторы (часто микросервисы). Если у актора есть состояние — то жертвуем простотой кода (отсутствием мютексов) ради того, чтобы актор мог одновременно обрабатывать несколько входящих запросов. Вероятно, влом выносить базу данных в отдельный актор, или хочется упрощения распределенных сценариев через RPC вместо событий/корутин. Вместе с простотой пожертвовали еще и воспроизводимостью событий при проигрывании входящих сообщений.
- Акторы на корутинах. Код в акторе состоит из верхней синхронной половины корутин-сценариев и нижней — асинхронных обработчиков событий (Half-Sync/Half-Async [POSA2]). Легковесный по ресурсам вариант многопоточного реактора [POSA2] ценой лишних усилий на асинхронную часть — по сути, мы реализовали RPC движок ручками. Вернемся к ним во второй части статьи.
Работа с состоянием
- У каждого актора свое состояние в раме (стандартный вариант).
- Сериализуемые акторы (Акка). Состояние актора дампится на диск или в сеть, и его можно восстановить в случае креша, и можно выгружать из памяти, если ее мало осталось. Очередной event soursing.
- Распределенные акторы (Эрланг/Эликсир, Акка). Рантайм сам раскидывает акторы по сети, решает, как кому куда переслать сообщение, и поднимает скоропостижно скрешившихся или бутнувшихся. Цена: появляются конфиг файлы с магией, и можно словить нежданчики, когда актора положили на одном компе и еще не подняли на другом.
- Акторы без состояния (CGI/fCGI/FaaS). Кусок кода с интерфейсом (который может состоять из Init() и Handle(message)), получающий все нужное для работы внутри message, а чего не хватает — вынимается из глобальных переменных или сервисов. Можно спавнить в большом количестве, не задумываясь о синхронизации.
Прочие приколы
- Тормозные акторы (работа с периферией). Эти ребята получают запрос, лезут куда-то читать / писать / юзать / считать данные, отправляют ответ, и берут следующий запрос. Получается, поток выполнения блокируется на работе с железом. Примеры: адаптер базы в гексагональной архитектуре, управление манипулятором у робота, рендерер какой-нибудь.
- Акторы с приоритетами (сетевой трафик). Входящие сообщения в зависимости от типа, значения какого-то поля, или функции-фильтра распределяются в одну из очередей разных приоритетов, либо сортируются внутри единой очереди с приоритетами. Далее возможно или выполнение высокоприоритетных запросов одновременно с нормальными (в этом случае, например, можно сообщением отменить выполнение текущей задачи), или следующим обрабатывается сообщение максимального приоритета (можно отменить еще не взятую в работу задачу; можно пустить управляющий реал-тайм трафик одновременно с потоком данных; можно давать квоты времени разным заказчикам).
- Иерархии акторов (Акка). Один актор может в рантайме создавать других акторов, передавать им каналы для отсылки сообщений (адреса акторов) и подписываться на извещение о смерти. Цена: куча сложности с непонятным профитом.
Это все комбинируется под свои капризы и бюджет. Но если перебдеть — то из шустрой моськи с двумя методами сделаете слона с кривой обучения.
Итого:
- Посмотрели дихотомию связность-гранулярность и увидели, что оптимум для разработчиков где-то посредине
- Сравнили объекты и акторы
- Прикинули, что акторам делать просто, а что — сложно
- Ознакомились с вариантами акторов
Теперь давайте попробуем собрать из этих акторов что-то полезное. Продолжение следует...
Литература
[DDD] Domain-Driven Design: Tackling Complexity in the Heart of Software. Eric Evans. Addison-Wesley (2003).
[DDIA] Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems. Martin Kleppmann. O’Reilly Media, Inc. (2017).
[EIP] Enterprise Integration Patterns. Gregor Hohpe and Bobby Woolf. Addison-Wesley (2003).
[MP] Microservices Patterns: With Examples in Java. Chris Richardson. Manning Publications (2018).
[POSA2] Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects. Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann. John Wiley & Sons, Inc. (2000)
Архитектуры на акторах:
Вступление (Actors / Objects; Complexity; When to Distribute; Kinds of Actors)
Монолиты (Control/Data Flow; Reactor / Proactor / Half-Sync/Half-Async; Request State)
Простые системы (Notation for Diagrams; Sharding; Layers; Services / Pipeline)
Системы с моделью (Hexagonal / MVC; Blackboard / Message Bus; Microkernel / Plug-ins)
Фрагментированные системы (Mesh; SOA; Hierarchy; Design Space)
Найкращі коментарі пропустити