Архитектуры на акторах: системы с моделью
Архитектуры на акторах:
Вступление (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)
Though my language is dead
Still the shapes fill my head
В предыдущей части мы рассмотрели базовые варианты разделения программы (монолита) асинхронными интерфейсами:
1. Создание идентичных копий (шардинг) — помогает держать нагрузку, может защищать от сбоев и позволяет использовать синхронные вызовы во всей программе.
- Условие 1: данные можно разделить на много сетов, и большинство сценариев не трогают более одного сета данных.
- Условие 2: нет общего (изменяемого в рантайме) состояния, влияющего на сценарии.
2. Разделение на слои — отделяет высокоуровневую логику от низкоуровневой и позволяет каждому уровню работать со своей скоростью и в своем мире.
- Условие 1: мало взаимодействий между уровнями по сравнению с количеством логики внутри уровней.
- Условие 2: нет кусков общего состояния, напрямую влияющих на несколько уровней одновременно.
3. Разделение на сервисы — даем независимость субдоменам.
- Условие 1: большинство сценариев не вовлекают несколько субдоменов одновременно (но могут перепрыгивать между субдоменами по цепочке, цена перепрыгивания — увеличение времени отклика).
- Условие 2: у субдоменов нет общего состояния.
Условия можно нарушать (слово «большинство» в описаниях), но ценой сложной злой тормозной нестабильной фигни
Целью архитектуры всегда было найти такую комбинацию подходов (паттернов), чтобы собрать плюшки (требуемые для определенных компонентов системы свойства) и не вступить в какашки (свойства, которые сделают данный конкретный кусок кода плохо применимым для данного конкретного проекта). Итак, переходим к вариантам одновременной нарезки на слои и сервисы, и посмотрим, как они предоставляют плюсы и нивелируют минусы акторов (или модульности) для разных систем. В этой части рассмотрим
Архитектуры с моделью:
Очень распространены (вездесущи) системы с монолитным (синхронным) горизонтальным слоем, покрывающим весь домен. Назовем его модель. Такой слой может находиться на разных уровнях абстракции и варьироваться по толщине (количеству кода) от основного компонента (гексагоналка) до тонкой прослойки (шина событий). Цель — интегрировать информационные потоки (control / data) и, возможно, данные других компонентов, разделенных по субдоменам. Иными словами — общий уровень собирает все в единую систему, и предоставляет возможность и правила общения для других участников.
В зависимости от того, модель более абстрактна, чем сервисы (находится выше них на диаграмме) или ближе к железу (под ними), будем различать П и Ц архитектуры, а также — их комбинацию Ж (с моделью посредине).
Гексагоналка (П)
Сверху — монолитный слой с высокоуровневой логикой (доменная модель), под ним — сервисы, работающие с периферией (адаптеры).
Плюсы:
- Основная логика монолитна, легко писать и отлаживать 95% кода.
- Поддерживаются сценарии любой сложности, если в них не очень много обращений к периферии (чтобы реже обращаться, можно самое полезное кешировать в верхнем слое).
- Возможность шардинга бизнес-логики (модели), если состояние приходит вместе с запросом (stateless).
- Высокоуровневая логика (доменная модель) и низкоуровневое обслуживание периферии (драйвера или переходники) могут работать в разных режимах (длительные расчеты против реал-тайм событий).
- Высокая независимость в разработке и деплое бизнес-логики и каждого из адаптеров периферии.
- Независимость режима работы и параллелизм между адаптерами периферии.
- Быстрая обработка сообщений от периферии, если нет необходимости обращаться к основной бизнес-логике (канал адаптер-адаптер).
- Легкость замены (или полиморфизма) для любого из адаптируемых компонентов периферии: мы уже отделены от них интерфейсами, приспособленными под нужды модели (бизнес-логики). То есть — защита от vendor lock-in.
- Возможность разработки бизнес-логики при недоступной периферии (адаптеры подменяются заглушками).
- Легкость тестирования бизнес-логики (поверх заглушек).
- Воспроизводимость событий при однопоточной бизнес-логике (запись событий производят на входе или выходе очереди сообщений от адаптеров к доменной модели).
Минусы:
- Бизнес-логика может сильно разрастаться — мы не очень много от нее отрезали. Хотя, интерфейсы к адаптерам, вероятно, будут иметь высокий уровень абстракции (доменные объекты, а не поля в базе данных), что должно слегка уменьшить количество кода в верхнем слое. То есть, переход к гексагоналке не поможет, если монолит умирает от доменной сложности.
- Сценарии, затрагивающие несколько компонентов периферии, будут заметно медленнее и сложнее, чем у монолита.
- Мы не можем напрямую использовать периферию через готовые библиотеки — относительно медленный старт проекта, пока напишутся все интерфейсы и адаптеры (или заглушки для начала работ над моделью, пока адаптерами занимаются другие команды).
Итого: очень много жирных плюсов для проектов среднего размера. Для мелких — плохо из-за медленного старта (не окупаются лишние усилия на интерфейсы и адаптеры). Для крупных — мы не разделили слишком сложную бизнес-логику, и все равно попадаем в Monolithic Hell [MP] нашим горизонтальным слоем (доменной моделью).
Финт ушами в том, что мы взяли легкость написания и железонезависимость бизнес-логики (которая у нас на диаграммах сверху) от Layers [POSA1], и при этом выделили каждой железке / (внешней зависимости) свой сервис (адаптер), разбив нижний слой на независимые субдоменные компоненты. Получилось удобнее, чем доменные сервисы, и гибче, чем слои. Цена по сравнению с этими двумя геометриями? Да, вроде, никакой. Небольшое проседание по скорости для высокоуровневых сценариев в рамках одного субдомена. Итого: пример, как разумная комбинация простых паттернов захватывает рынок — потому что имеет плюсы обоих родителей и не имеет многих их минусов. И да, здесь у нас аутсайдер — паттерн, не вошедший в известные книжки, опубликованный где-то в сетевых блогах, но при этом — полностью захвативший мир:
Классика: Ports and Adapters, (Re)Actor-fest.
Бэкенд: Hexagonal Architecture, Onion Architecture, Clean Architecture.
Почему то, что мы обсуждали — гексагоналка? Классические рисунки (и название) гексагоналки идут от диаграммы в радиальных координатах (радиус — уменьшение абстрактности логики, угол — субдомен). У нас — ортогональные координаты. Преобразование координат будет выглядеть так:
Для control flow систем уровень с доменной моделью включает в себя информацию о последнем известном состоянии системы. Например, если мы делаем управление для робота, то у нас будет синхронно доступно (закешировано) положение всех манипуляторов, скорость перемещения, и карта окружающего пространства. Это позволяет максимально легко и быстро (в оперативке, не обращаясь к периферии) рассчитывать последующие действия и реакцию на события. Для data flow систем весь объем данных (состояние системы) обычно слишком большой — он будет храниться в базе данных под одним из адаптеров, и подниматься в модель при надобности. В модели содержится либо только информация для обработки текущего запроса, либо еще и кеш состояний из недавних запросов, если это запрограммировано и помогает разгрузить базу.
Вызовы из модели к адаптерам могут быть синхронными (блокирующими — RPC) или асинхронными (req/cfm, notifications), либо синхронные для одних адаптеров (база данных) и асинхронные для других (сеть) — делают как удобнее для данной программы (часто определяется доменом).
При использовании control flow + data flow (стандартно в телекоме) модель служит для настройки потоков данных, а сами данные передаются между адаптерами напрямую, возможно — вообще без копирования.
Варианты:
Model-View-Controller [POSA1]
Можно считать (плиз, не материться!) одним из вариантов гексагоналки, по крайней мере — в оригинальном описании паттерна, где VC служили адаптером к оконной системе. Получается control flow пайплайн-гексагоналка (однонаправленная передача управления).
Цели MVC (перечисляю по [POSA1]) — разделение логики и интерфейса чтобы:
- одну модель можно было представить разными видами (у гексагоналки — запустить с разной конфигурацией периферии);
- реал-тайм обновления интерфейса (у гексагоналки — независимость режима (скорости) работы адаптеров друг от друга и от модели);
- независимая разработка интерфейса так как он меняется чаще, чем модель (у гексагоналки — независимая разработка адаптеров и модели);
- легкость переноса программы на другую оконную систему (у гексагоналки — защита от vendor lock-in).
То есть, диаграммы двух старых известных паттернов одинаково выглядят в нашей системе координат, и тут оказывается, что у них идентичные цели и свойства, только описано в разных выражениях.
Еще можно сравнить MVC с нашей диаграммой для пайплайна. Пайплайн тоже содержит два низкоуровневых компонента (source и sink), но верхний уровень с логикой у него разделен на кучу отдельных фильтров. Значит: нет вот этой вот страшной ментальной пропасти между гексагоналкой и пайплайном. Если вдруг мы писали пайплайн, а потом начались такие хотелки заказчика, что фильтры пайплайна перестают быть независимыми друг от друга — да плюньте вы на эту идеальную архитектуру, слепите все фильтры в синхронный моноблок — и получите вместо пайплайна гексагоналку. Раз уж оказалось, что написанный пайплайн плохо подходит под домен или нужно додавить производительность.
Чуть менее легко слепить гексагоналку из нескольких субдоменных сервисов (если они написаны на одном языке) — для этого вначале каждый сервис режется интерфейсом на высокоуровневую (бизнес-логика) и служебную (драйвер) части, а затем — верхние части с логикой объединяются в монолитный слой.
Half-Sync/Half-Async [POSA2]
Мы уже дважды видели это название: сначала — как внутреннее устройство актора-монолита, затем — глянули более детально как слоистую систему, вспомнив, что Half-Sync/Half-Async родился как описание операционных систем. При этом, на самом деле, нижний слой у ОС — не монолитный, а состоит из нескольких независимых компонентов (подсистем, драйверов). И эти драйвера как раз:
- абстрагируют пользовательские программы от железа;
- незаметно для приложений верхнего слоя управляют железом;
- могут обновляться независимо друг от друга и от пользовательских приложений.
Снова видим ключевые свойства гексагоналки. Благодаря этим свойствам операционные системы построены так, как построены: многокомпонентный асинхронный нижний слой, скрытый интерфейсами от пользователя, и синхронное пользовательское пространство с приложениями, живущими с упрощенной и довольно абстрактной моделью системных ресурсов.
Half-Async/Half-Async [POSA2]
То же самое, но с асинхронным общением между верхним и нижним уровнями — когда критична скорость принятия решений и возможность передумать в любой момент в зависимости от ситуации. Если у Half-Sync/Half-Async в верхнем слое живут реакторы [POSA1] с блокирующими вызовами к периферии, то у Half-Async/Half-Async в верхнем слое — один проактор [POSA1], асинхронно отсылающий сообщения и мгновенно реагирующий на входящие события.
Посмотрим, что будет, если инвертировать диаграмму:
Blackboard / Middleware (Ц)
Сверху — раздельные сервисы-субдомены с логикой, внизу — монолитный слой (модель или шина данных) поверх железа.
Плюсы:
- Хорошо распутывает сложность бизнес-логики, если сделать много субдоменных сервисов.
- Отвязывает имплементацию сервисов по свойствам (время отклика, язык).
- Независимый шардинг сервисов.
- Независимый деплой сервисов.
- Возможность для сервисов работать с общими данными без сильного усложнения кода.
Минусы:
- Общая модель данных уменьшает свободу сервисов к независимым изменениям.
- Скорее всего, данные тяжело будет масштабировать и шардить.
- Требования разных сервисов к хранению и передаче данных могут конфликтовать.
- Нижний слой (и его железо) является единой точкой отказа для системы.
Итого: быстрый старт, когда нужно сделать несколько независимых по свойствам сервисов.
Классика: Blackboard [POSA1], Middleware.
Бэкенд: Shared Database (Smart UI of [DDD]), (Message) Broker [POSA1, EIP], Message Bus [EIP].
Здесь видим два варианта, оба широко используются. В обоих, насколько я знаю, обычно используются синхронные вызовы (direct method call / RPC) к модели.
Blackboard/ Shared Database (хранение данных)
Независимые сервисы пользуются общей моделью данных (базой данных в случае бэкенда), которая сама разруливает синхронизацию изменений. Simple & Stupid. При этом оно работает, пока сервисы простые, а база — быстрая и надежная.
Особое внимание можно уделить CQRS без разделения базы. Такая архитектура используется потому, что:
- У команд и кверей сильно разные свойства (время отклика, язык программирования, требования к доменной модели). Поэтому удобно их закодить в независимых высокоуровневых модулях.
- Разделение базы на OLTP + OLAP и поддержка их консистентности — нетривиальны [DDIA] и требуют кучу Ops усилий.
В результате мы берем плюсы Ц архитектуры (независимость кода и свойств приложений) — они как раз нам подходят, а платим только большей нагрузкой на базу данных (одна универсальная база менее эффективна, чем две специализированных) — пока база эту нагрузку выдерживает.
Middleware / Message Bus (передача данных)
В нижнем слое находится модель компонентов, знающая о всех сервисах (Broker) и позволяющая сервисам обращаться друг к другу (Message Bus). Также шина данных может гарантировать доставку (хранить неполученные сообщения) и логировать сообщения (что даст возможность воспроизведения глюков — если сервисы детерминистичны — и регрессионного тестирования).
Применяется повсеместно, но обычно на шине данных не акцентируют внимание — она опущена на большинстве диаграмм (как раз из-за своей тривиальности и вездесущности).
Shared Model (Ж)
Ц поверх П, у них общая модель. В зависимости от типа модели:
Microkernel [POSA1] (broker + message bus as model)
Еще один вариант операционных систем (QNX, будущая Fuchsia), считается более надежным, так как ядро живучее. Под моделью компонентов (микроядром) живут драйвера (сервисы, обслуживающие железо), над ним — программы (сервисы с пользовательской нагрузкой / бизнес-логикой). Ядро, как модель компонентов, одновременно предоставляет коммуникацию между сервисами (Message Bus) и присматривает за их жизнеспособностью (Broker). Поэтому оно «микро» — чтобы негде было заглючить или упасть. В бекендах роль микроядра выполняет DevOps инфра (кафко-k8s) и облачный провайдер. В Akka или Elixir микроядром является сама платформа, распределяющая акторы по серверам и передающая между ними сообщения.
Применение — выживание софта любой ценой. Возможно благодаря тому, что микроядро, как брокер, ни от кого не зависит, но обо всех все знает.
DSL (Interpreter [GoF]) / Metadata (Reflection [POSA1]) / Plug-ins [SAP] (domain model)
Смесь гексагоналки и blackboard. Доменная модель (поддержка бизнес-логики) из гексагоналки является базой для высокоуровневых сценариев вышестоящей blackboard. Как примеры можно привести скриптование поведения в играх, или даже SQL-запросы (DSL), работающие поверх движка базы данных (domain model), сидящего поверх адаптеров ОС и файловой системы.
Сверху — метасервисы или сценарии, манипулирующие моделью, наблюдающие события и настраивающие поведение модели в соответствии с бизнес целями и фидбеком от модели. Под ними — монолитная модель со своими правилами и логикой домена. Ниже — служебные сервисы, предоставляющие ресурсы.
Обычно на диаграмме будут разные субдомены выше и ниже модели (прошлая статья начиналась с абстрактной картинки для игрушки. На ней под моделью идет разделение по железу, над моделью — по модулям метаданных). Плюсы и минусы — сумма от blackboard и гексагоналки.
Применение — создание удобного языка (интерфейса) для написания супервысокоуровневой логики ценой общего усложения системы.
(Re)Actor-with-Extractors (R/W or Black and White) (domain model)
Вырожденный случай рефлексии, а еще — забавный изврат для безлочного использования кучи ядер процессора. Есть пул потоков, обслуживающий все объекты в модели. Жизнь программы делится на череду фаз, как игра в мафию:
- чтение (день), когда любой объект может синхронно читать публичное состояние любого другого объекта и сохранять план своих будущих изменений в приватную область.
- запись (ночь), когда каждый объект применяет свой план действий, днем сохраненный в приватной области, к своим публичным данным. При этом ночью объекты не могут взаимодействовать (не имеют доступа друг к другу).
Скедюлер следит за сменой дня и ночи: он дает возможность запуститься каждому объекту каждый день и каждую ночь. Смена времени суток (read / write режим) наступает только тогда, когда все объекты завершили свои действия для текущего режима (чтение взаимодействий и планирование днем / изменение состояния ночью).
Применение — эффективно выжать все циклы из многоядерного проца для обработки модели системы с большим количеством взаимодействующих, но независимо принимающих решения, компонентов.
Итого:
Мы рассмотрели архитектуры с монолитным горизонтальным слоем: практически вездесущие гексагоналку и message bus, и чуть более экзотическую blackboard. Как обычно — попробовали их смешать. И, как обычно, для каждой из рассмотренных геометрий исторически сложилось много названий и подтипов.
Что еще можно нарисовать на плоскости (или собрать из прямоугольников)? Дальше порисуем фрагментированные (не имеющие больших кусков) системы.
Литература
[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).
[GoF] Design Patterns: Elements of Reusable Object-Oriented Software. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison-Wesley (1994).
[MP] Microservices Patterns: With Examples in Java. Chris Richardson. Manning Publications (2018).
[POSA1] Pattern-Oriented Software Architecture Volume 1: A System of Patterns. Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad and Michael Stal. John Wiley & Sons, Inc. (1996).
[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).
[SAP] Software Architecture Patterns. Mark Richards. O’Reilly Media, Inc. (2015).
Архитектуры на акторах:
Вступление (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)
32 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів