В архитектуре программного обеспечения существует один малоизвестный паттерн, заслуживающий большего внимания. Архитектура, ориентированная на данные, (data-oriented architecture, DOA) была впервые описана Радживом Джоши в отчете RTI 2007 года, а затем в 2017 году Кристианом Ворхемом и Эрихом Шикутой из Венского университета в статье iiWAS. DOA — это инверсия традиционной дихотомии между монолитным кодом и хранилищем данных (монолитная архитектура) с одной стороны, и небольшими распределенными независимыми компонентами с собственными хранилищами (микросервисы и сервис-ориентированная архитектура) с другой. В архитектуре, ориентированной на данные, монолитное хранилище данных является единственным источником состояния в системе, на которое воздействуют слабосвязанные микросервисы без состояния.   Мне повезло, что мой бывший работодатель выбрал такую необычную архитектуру. Это напомнило, что одни и те же вещи можно делать по-разному. Архитектура, ориентированная на данные, ни в коем случае не является панацеей; у нее есть собственный уникальный набор издержек и преимуществ. Однако я обнаружил, что многие крупные компании и экосистемы сталкиваются с проблемами, для устранения которых и предназначена данная архитектура. Поскольку многие архитектуры часто определяются в противоположность монолитной, стоит потратить немного времени на её описание. В конце концов, это легендарные отголоски первобытности в разработке серверного ПО.   В монолитном сервисе основная часть серверного кода является одной программой, которая взаимодействует с одной или несколькими базами данных, управляя различными аспектами функциональных вычислений. Представьте себе торговую систему, которая получает запросы от клиентов на покупку или продажу какой-либо ценной бумаги, оценивает их и выполняет заказы. В рамках монолитного сервера код все еще может быть разделен на отдельные модули, но между различными компонентами программы нет принудительной API-границы. Единственные жестко определенные API-интерфейсы в программе обычно находятся (a) между UI и сервером (вне зависимости от REST/HTTP протокола), (b) между сервером и хранилищами данных (вне зависимости от языка запросов), или (c) между сервером и его внешними зависимостями.   Сервис-ориентированные архитектуры (SOA), с другой стороны, разбивают монолитные программы на сервисы для каждой независимой, компонентной функции. В нашем торговом приложении у нас может быть отдельный сервис в качестве внешнего API, который принимает запросы и обрабатывает ответы для клиента, вторая система, получающая цены и другую информацию о рынке, третья система, отслеживающая заказы, риски и т.д. Интерфейс между каждым из этих сервисов представляет собой формально определенный уровень API. Сервисы обычно взаимодействуют один на один через RPC, хотя распространены и другие методы, такие как передача сообщений и pubsub. Сервис-ориентированные архитектуры позволяют обдумывать и разрабатывать различные сервисы независимо (и параллельно), если это необходимо. Сервисы являются слабосвязанными — это означает, что совершенно новый сервис может переиспользовать другие.   Поскольку каждый сервис в SOA определяет свой собственный API, к каждому можно получить независимый доступ и с каждым можно взаимодействовать. Разработчики, отлаживающие отдельные части или симулирующие их работу, могут вызывать компоненты обособленно, а новые потоки могут комбинировать эти сервисы, чтобы обеспечить новое поведение. Микросервисы — это тип сервис-ориентированной архитектуры. Здесь мнения разнятся; одни считают, что микросервисы должны быть особо мелкими и легковесными, другие — что это в целом синоним SOA. В SOA отдельные компоненты напрямую взаимодействуют друг с другом через специальный API, определенный каждым из компонентов. В целях коммуникации каждый компонент можно индивидуально адресовать (т. е. использовать IP-адрес, адрес сервиса или какой-либо другой внутренний идентификатор для отправки запросов/сообщений туда и обратно). Это означает, что каждый компонент в архитектуре должен знать о своих зависимостях, и должен быть специально интегрирован с ними.   В зависимости от топологии архитектуры это может значить, что дополнительный компонент, возможно, должен знать обо всех предыдущих компонентах. Кроме того, замена отдельного сервиса, с которым уже взаимодействуют N других компонентов, вероятно, окажется сложной задачей: нужно позаботиться о сохранении любых ad hoc API, определенных вами, и убедиться, что у вас есть план миграции для перехода каждого из компонентов от обращения к старому сервису к новому. Поскольку API-интерфейсы «сервис-сервис» являются ad hoc1, это часто указывает на то, что RPC между компонентами могут быть сколь угодно сложными, что потенциально увеличивает количество возможных изменений API в будущем. Каждое изменение API в сервисе, от которого зависят многие другие, является серьезным мероприятием. Здесь я подразумеваю, что по мере роста экосистемы микросервисов она начинает подвергаться следующим проблемами масштабирования: N2 рост сложности интеграции по мере увеличения числа компонентов2  О форме сети становится трудно рассуждать априори; т.е. для создания или поддержки тестовой или изолированной среды потребуется много рассуждений, чтобы убедиться, что ни один компонент в графе не имеет внешней зависимости. Несколько друзей добровольно поделились собственными проблемами с масштабированием сервис-ориентированной архитектуры: Еще одна проблема с ростом SOA, которую я замечал, — это циклы зависимостей между сервисами. Так как вы развертываете их независимо и редко запускаете всю систему с нуля, то легко ввести циклы и сломать DAG. Еще одна проблема с масштабированием SOAs, о которой, возможно, стоит упомянуть, заключается в том, что требуется, чтобы вы заранее знали все будущие рабочие процессы ваших клиентов. Если не знаете — и разделяете данные для одного рабочего процесса по нескольким вертикалям — то вы столкнетесь либо с проблемами производительности, пытаясь гарантировать транзакционность между несколькими постоянными хранилищами, либо с переопределением того, где дублируются (кэшируемые, но действительно присутствующие в бд) данные. В архитектуре, ориентированной на данные (DOA), системы по-прежнему организованы вокруг небольших, слабосвязанных компонентов, как в микросервисах SOA. Но DOA отходит от микросервисов двумя ключевыми способами: Компоненты всегда не имеют состояния  Вместо того, чтобы разбивать хранилища данных для каждого соответствующего компонента, DOA предписывает описывать данные или уровень состояния в терминах централизованно управляемой глобальной схемы. Межкомпонентное взаимодействие сведено к минимуму, вместо этого предпочтение отдается взаимодействию через уровень данных. В нашей торговой системе компонент, получающий цены на различные ценные бумаги, просто публикует цены в канонической форме в нашем хранилище данных. Система может использовать эти цены, запрашивая цены на уровне данных, а не у определенного сервиса (или набора сервисов) через конкретный API.   Здесь стоимость интеграции линейна. Изменение схемы DAO означает, что в обновлении нуждаются до N компонентов, а не до N2 связей между ними. Такой поход действительно проявляет себя, когда отдельные высокоуровневые типы данных заполняются разными провайдерами. Если мы заменим один сервис одной таблицей, то мы не сильно упростим ситуацию. Преимущество есть, когда существует несколько источников одного и того же общего типа данных. Если торговая система подключается к нескольким торговым площадкам, каждая из которых публикует запросы клиентов в RFQ таблицу, то последующие системы могут запрашивать эту таблицу и не беспокоиться о том, откуда поступает запрос клиента. Поскольку в DOA межкомпонентное взаимодействие минимизировано, возникает вопрос — как можно было бы сегодня заменить межкомпонентное взаимодействие в SOA взаимодействием через уровень данных?   1. Производители и потребители данных Распределение компонентов по производителям и потребителям данных — основной способ разработать систему DOA. Если вы можете на высоком уровне описать свою бизнес-логику в виде серии операций map, filter, reduce, flatMap и других операций, то вы также можете написать систему DOA в виде серии компонентов, каждый из которых запрашивает или подписывается на входные данные и производит выходные данные. Проблема в DOA заключается в том, что на этих промежуточных этапах данные являются видимыми и запрашиваемыми, это означает, что они должны быть хорошо инкапсулированными, хорошо представленными и соответствовать определенной концепции бизнес-логики. Преимущество, однако, состоит в том, что поведение системы является внешне наблюдаемым, отслеживаемым и проверяемым. В торговой системе SOA компонент, принимающий заказы с торговой площадки, может делать RPC, чтобы выяснить, как устанавливать цены, котировки или торговать по заказу. В DOA микросервис принимает запросы от торговых площадок (обычно в стиле SOA) и выдает RFQ, в то время как другие производители предоставляют данные о ценах и т. д. Еще один микросервис запрашивает RFQ, объединенные со всеми ценами, и выдает котировки, заказы или любые другие данные, необходимые пользователю. 2. Запускающие действия и поведения Иногда самый простой способ думать о связи между компонентами — это RPC. В то время как хорошо спроектированная система DOA3 должна следить за большей частью межкомпонентной коммуникации, замененной парадигмами производитель/потребитель, вам все равно могут потребоваться прямые способы, чтобы компонент X сказал Y сделать Z. Во-первых, важно подумать, могут ли RPC быть реорганизованы как события и их результаты. Т.е. спросите себя, может ли компонент X вместо отправки RPC в компонент Y, где происходит событие E, производить события E и заставлять компонент Y управлять ответами, потребляя эти события?  Такой подход, который я буду называть событиями на основе данных, может быть мощной инверсией обычного взаимодействия компонентов. Причина, по которой он настолько мощный, заключается в том, что он позволяет нам перевести термин «слабосвязанный« на следующий уровень. Системам не нужно знать, кто потребляет их события (в отличие от вызывающей стороны RPC, которой абсолютно необходимо знать, кого она вызывает), и производителям не нужно беспокоиться о том, откуда берутся эти события, им просто нужно понимать семантическое значение бизнес-логики этих событий. Конечно, существует наивный способ реализации событий на основе данных, когда каждое событие сохраняется в базе данных в своей собственной таблице, соответствующей 1:1 с сериализованной версией запроса RPC. В этом случае события, основанные на данных, вообще не разъединяют систему. Для того чтобы события, основанные на данных, работали, преобразование запроса/ответа в сохраняемые события требует, чтобы они были значимыми конструкциями бизнес-логики. События, основанные на данных, могут не подходить друг другу. Например, если вы хотите вызвать поведение в определенном компоненте. В этих случаях, вероятно, все еще желательно оставить ограниченное количество реальных RPC от компонента к компоненту. Одна из причин, по которой я продолжаю упоминать торговое/финансовое ПО в качестве примера, заключается в том, что финансы часто требуют большой площади интеграции. Типичная торговая фирма, позволяющая мелким клиентам торговать, часто интегрируется со многими торговыми площадками для взаимодействия с клиентами, а также со многими поставщиками ликвидности для получения цен и размещения заказов. Бизнес-логика, которая должна происходить между поступлением запроса на рынок и получением ответа от клиента, представляет собой сложный, многоступенчатый процесс.   В проблемном пространстве с высокой степенью интеграции отдельным сервисам может потребоваться знать о множестве других сервисов. Чтобы избежать сложности O(N2) в интеграции и сложных отдельных сервисов с высоким коэффициентом разветвления, перестройка системы вокруг производителей и потребителей данных позволяет упростить интеграцию. Если появится новая интеграция, вместо того, чтобы редактировать N новых систем или одну систему, которая имеет сложное разветвление для N других систем, процесс интеграции может включать написание одного адаптера, который создает данные в общей схеме DOA, принимает окончательный результат и отображает его в правильном проводном формате. Неявно в интеграции возникает новый вид сложности: размышление о схеме. Любая новая интеграция должна быть естественной для вашей системы, а вашу схему нужно расширять без добавления прокладок, хаков и особых случаев. Это само по себе трудное задание. Однако когда количество интеграций достаточно велико, сложность амортизируется и часто окупается. Если вы создаете прототип или тестируете что-то вручную, надеюсь, вы это делаете вне продакшена. Тем не менее то, как устроены некоторые SOA-экосистемы, часто говорит, что нелегко понять, в какой среде находится сервис, или является ли конкретная среда вообще автономной. Среда — это внутренне согласованный, последовательно связанный набор сервисов, обычно/идеально структурированный в той же топологии, что и продакшн. Поскольку сервисы SOA обычно адресуются независимо друг от друга, согласованность среды предполагает, что каждый сервис должен согласовываться со всеми другими сервисами в среде, по какому адресу для чего обращаться. RPC, pubsub и поток данных не должны просачиваться из одной среды в другую. Очевидно, что в SOA есть способы обойти это, например, перейти на сервисные реестры, которые генерируют правильную конфигурацию для сервисов4 , или, когда доступ к сервисам осуществляется через URI, скрывающий прямые служебные адреса в пользу различных путей под префиксом среды5. Однако в DOA концепция среды намного проще. Зная, к какому уровню хранилища данных подключается компонент, достаточно описать, в какой среде он находится. Поскольку все компоненты не хранят внутреннее состояние, данные изолированы по определению. Нет никакой опасности утечки данных из одной среды в другую, поскольку компоненты взаимодействуют только через хранилище данных. На данный момент существует множество распространенных примеров, приближенных к архитектуре, ориентированной на данные. Монолит данных, в котором все (или большинство) данных хранятся в одном большом хранилище, часто говорит, что архитектура системы приближена к DOA.   Knowledge Graphs, например, представляют собой обобщенный монолит данных. Тем не менее они часто недостаточно универсальны; при этом многие состояния, связанные с бизнес-логикой, потенциально отсутствуют. GraphQL часто используется в качестве нормализованного слоя хранилища данных, такого как монолит данных. Степень, в которой GraphQL можно успешно сделать серверной частью системы DOA, в большей степени зависит от дизайна схемы: например, можно выбрать обобщенную схему и таблицы, связанные с концепциями бизнес-логики, а не схемы и таблицы, специфичные для конкретного источника этих данных. Такая архитектура не является панацеей. Там, где архитектура, ориентированная на данные, стирает ряд проблем, возникают новые: от дизайнера требуется тщательного обдумывания владения данными. Ситуации, когда несколько авторов изменяют одну и ту же запись, могут быть незначительными, что часто побуждает системы тщательно разделять права собственности на запись. И поскольку межкомпонентные API-интерфейсы закодированы в данных, существует одна общая глобальная схема, которую нужно хорошо продумать.   Мне вспоминается документация по Google’s Protocol Buffers, которая при обсуждении маркировки полей в схеме как required предупреждает: Required Is Forever. В Broadway Technology технический директор Джошуа Вальски говорил нечто подобное о схеме DOA: Data Is Forever. По причинам, аналогичным предупреждению Protobuf, оказывается, что удалить столбец из таблицы в слабосвязанной распределенной системе очень, очень сложно. Мои пять копеек: Если вы беспокоитесь о горизонтальном масштабировании своей архитектуре, то подумайте о проектировании с монолитом данных в центре. API-интерфейсы между сервисами необязательно ad hoc, но прямая связь между компонентами часто означает, что между ними легко передавать параметры для определенной цели.  Действительно ли архитектура достигает роста сложности интеграции N2 зависит от топологии ее архитектуры. Если вы находитесь в системе, в которой интеграция является одним из узких мест, то вы, скорее всего, с этой проблемой. Например, торговая система, интегрирующаяся с различными поставщиками ликвидности и внебиржевыми торговыми площадками, в идеале не должна находиться в ситуации, когда каждый компонент, управляющий заказами с рынка, должен знать о каждом компоненте, обеспечивающем ликвидность.  Система, которая хорошо подходит для DOA и хорошо спроектирована.  Предполагая, что сервисы вызывают друг друга на основе прямого адреса (например, IP-адреса или некоторой внутренней схемы адресов для запущенного процесса), и знают, где можно связаться с определенной сервисом на основе параметров командной строки, может потребоваться обернуть их с помощью более подходящей логики, которая создает правильные флаги в зависимости от среды. например, вместо того, чтобы обращаться к определенной службе по IP-адресу или какому-либо внутреннему URI, специфичному для этой службы, структурируя каждую услугу, которая будет обслуживаться по «пути», маршрутизируемому одним сервером. Например, вместо://process1.namespace.company.com /*, вы общаетесь с ://env.namespace.company.com/Employees/*.