Если неудачно выбрать подход к взаимодействию приложений или их компонентов между собой, команда столкнётся со сложностями при масштабировании. Зачастую эта проблема становится узким горлышком для систем, которые сталкиваются с высокой нагрузкой.
Например, если выбрать синхронное взаимодействие, компоненты будут зависеть от доступности друг друга. В этом подходе есть и дополнительная сложность: нужно сделать так, чтобы каждый из компонентов смог работать с заданной нагрузкой. Если один из компонентов не способен масштабироваться под требуемую нагрузку, это создаст проблемы для всего приложения. Использование асинхронных способов интеграции тоже не панацея: этот тип взаимодействия ставит определённые вызовы перед командой разработки и может усложнить архитектуру.
Поэтому для каждой ситуации важно выбрать оптимальный паттерн взаимодействия и правильную технологию для его реализации.
В этом уроке мы расскажем про наиболее распространённые паттерны back-to-back взаимодействий и разберём, когда их стоит использовать.
Виды back-to-back взаимодействий
Глобально все взаимодействия между приложениями можно разделить на две категории. Их определяет:
Характер взаимодействия. На этом уровне можно выбрать между паттернами «точка-точка» и «публикация-подписка».
Способ взаимодействия. Он может быть синхронным или асинхронным. В первом случае система получит ответ в рамках того же сетевого подключения, а во втором случае — в рамках уже другого подключения.
Разберём каждую из категорий по порядку.
Характер взаимодействия
«Точка-точка». При таких взаимодействиях система-инициатор (отправитель сообщения) обращается к конкретной системе за конкретным действием. Система, которая получает сообщение, должна вернуть какие-то данные или выполнить команду.
«Публикация-подписка». При этих взаимодействиях систему, которая отправляет сообщение, не интересует, кем это сообщение будет прочитано, как оно будет обработано и будет ли оно обработано вообще. Такое сообщение может быть получено несколькими приложениями. В этом контексте мы говорим об обмене событиями, который вы разбирали в прошлых уроках.
Способ взаимодействия
Синхронные взаимодействия. При таких взаимодействиях система-инициатор ожидает, что результат обращения ей вернут в рамках того же сетевого подключения. Хороший пример — взаимодействие по HTTP. Даже если система-отправитель не будет блокировать поток на своей стороне, то есть будет применять асинхронную обработку (например, посредством реактивного фреймворка), такое взаимодействие всё равно считаем синхронным. Так как в случае разрыва соединения получить результат можно только отправив новый запрос.
Асинхронные взаимодействия. Они предполагают, что результаты или ответы могут быть получены асинхронно, в рамках другого сетевого подключения, а на стороне системы, получающей ответ, сообщение будет обработано в рамках нового процесса. Такое взаимодействие предполагает использование брокеров сообщений, таких как Kafka и RabbitMQ.
Теперь нужно разобраться, как правильно выбрать вид взаимодействия в разрезе этих двух категорий. Начнём с выбора характера взаимодействия.
Как выбрать характер взаимодействия
Чтобы выбрать между паттернами «точка-точка» и «публикация-подписка», вам нужно хорошо понимать сценарий взаимодействия между компонентами.
Паттерн «точка-точка» в первую очередь подходит для сценариев, когда вам необходимо запросить данные (query) или отправить команду (command/mutation), чтобы система-получатель выполнила какое-то действие. Во втором случае система-получатель может, например, создать, модифицировать или удалить какие-то данные. Если рассматривать такие взаимодействия в контексте операций CRUD, то на одной стороне у нас будет Read, на другой Create, Update, Delete.
Итак, глобально есть два сценария:
Вам нужно получить данные. В этом случае выбор характера взаимодействия зависит от того, будете вы использовать запрос данных или репликацию.
Вам нужно инициировать какое-то действие. Здесь выбор стоит между командой и событием.
Сейчас разберём, чем руководствоваться для ответа и на что именно он повлияет. Будем двигаться по порядку.
Вам нужно получить данные: запрос данных или репликация?
Чтобы выбрать паттерн взаимодействия в этом сценарии, вам нужно определить, как именно вы будете получать данные. С помощью запроса или репликации?
В случаях, когда вам нужно получить данные из другой системы, проще всего «дёрнуть ручку» приложения — отправить ему запрос для получения данных. Но это не такое простое решение, как может показаться на первый взгляд. Здесь есть свои подводные камни.
Чтобы этот способ работал эффективно, нужно понять:
Достаточно ли быстрое время отклика у вызываемого сервиса? Время отклика вызываемого приложения напрямую влияет на время отклика вашего приложения. В ситуациях, когда вам требуется выполнить операцию в реальном времени, например, чтобы отобразить какие-то данные клиенту в его UI, долгое время ответа снизит скорость отображения страницы для ваших пользователей. В итоге это может плохо повлиять на удовлетворённость пользователей вашим приложением.
Сможет ли вызываемое приложение обеспечить нужное вам количество запросов в секунду (RPS)? Если приложение не может обеспечить нужный RPS, это также может повлиять на производительность вашего приложения. В итоге вам так или иначе потребуется троттлить запросы, то есть искусственно замедлять работу вашего приложения. Это тоже может привести к негативному клиентскому опыту.
Устраивает ли вас класс критичности вызываемого приложения? Если показатели доступности вызываемого приложения ниже, чем у вашего приложения, это может привести к полным или частичным сбоям в работе. Поэтому важно проанализировать, для каких процессов требуются запрашиваемые данные. Если данные нужны для критичных процессов, а показатели доступности системы-источника ниже требуемых, то, вероятно, это приведёт к негативному опыту пользователей.
Все эти вопросы крайне актуальны для высоконагруженных приложений. К ним предъявляются самые высокие требования по времени отклика, количеству обрабатываемых запросов в секунду и доступности. Нельзя оценивать приложение отдельно от его «зависимостей».
Если показатели системы, из которой вам нужно получить данные, не устраивают вас по какой-то причине, стоит рассмотреть два типовых варианта решения этой проблемы. В этом случае можно:
Доработать и улучшить требуемые показатели в системе, которая является источником данных. Этот вполне рабочий вариант, он используется на практике. Но нужно понимать, что этот путь потребует много ресурсов, в том числе — много времени. Поэтому такой способ подходит далеко не для всех случаев.
Использовать CQRS и репликацию данных. Можно применить паттерн CQRS с репликацией данных на основе событий. Вы разбирали эту технику в уроках об Event-Driven архитектуре. В такой ситуации вы придёте к выбору между взаимодействием типа «точка-точка» для запроса данных (query) и взаимодействием типа «публикация-подписка» с целью репликации данных на основе событий.
Репликация позволит вам повысить независимость вашего сервиса от системы-источника по всем показателям, которые мы перечислили выше. Дополнительно вы получите гибкость в управлении ресурсами и масштабировании систем. В этом случае рост нагрузки на систему, которая является потребителем данных, не повлияет на систему-источник.
Но и здесь есть свои сложности.
Сергей
С какими проблемами можно столкнуться при репликации?
Их всего четыре:
Несогласованность данных. Например, могут возникать ситуации, при которых событие потеряется. Из-за этого данные в реплике и системе-источнике будут несогласованными. Рекомендуем использовать паттерн Transactional Outbox, чтобы повысить гарантию отправки.
Итоговая согласованность данных. Могут возникать ситуации, при которых данные в реплике будут «отставать» от данных в системе-источнике. Чтобы минимизировать риски, можете использовать паттерн Transaction Log Tailing и инструменты CDC. Так вы сократите время, которое требуется для публикации событий.
Частота изменения данных. Необходимо учитывать, как часто данные меняются в разрезе одной сущности данных. Если они меняются часто, могут возникнуть две проблемы:
1. Данные меняются настолько часто и быстро, что реплика всегда находится в неактуальном состоянии. Например, это может произойти, если вы работаете с котировками акций. Здесь важно понять, какие требования у вашей системы к актуальности данных. Возможно, вам не критично иметь актуальный курс на текущее время, но критично, например, иметь в быстром доступе срез значений за последний час.
2. Большое количество событий об изменении данных создают дополнительную нагрузку на БД. Если вы храните реплику данных в одной БД с «основными» данными приложения, это может создать избыточную нагрузку на запись в вашей БД. В итоге на обновление реплики будет уходить слишком много ресурсов и общая производительность приложения ухудшится. Чтобы решить эту проблему, рекомендуем использовать изолированную БД или кеширование.
Выбор ресурсов для хранения данных. В зависимости от требований реплику данных можно хранить как на дисках, так и в оперативной памяти. Прежде чем использовать репликацию, рекомендуем оценить объёмы данных, которые будут храниться в реплике, и затраты на их хранение. Это поможет понять, насколько эта стратегия вам подходит.
Итак, подведём промежуточный итог. Если вам нужно получить данные из другой системы, первый и очевидный выбор — это взаимодействия по паттерну «точка-точка». Обычно такое решение можно реализовать быстрее всего. К тому же оно зачастую оказывается самым эффективным. Но это справедливо только в том случае, если система-источник удовлетворяет вашим требованиям.
Если показатели системы-источника вам не подходят, рассмотрите альтернативный подход — взаимодействия типа «публикация-подписка» с целью репликации данных на основе событий.
А теперь посмотрим, как это работает на примере.
Сергей
Пример
Обратимся к приложению «ОнлиСхемы». Напомним — его архитектура в начале нашей работы выглядела так:
Обратим внимание на взаимодействие между сервисом досок и сервисом пользователей. Сервису досок требуется информация о пользователях, чтобы отображать её на доске. Изначально команда выбрала тип взаимодействия «точка-точка». Это неудачное решение. Если использовать взаимодействия типа «точка-точка», мы сделаем сервисы зависимыми друг от друга. Когда вырастет количество одновременных пользователей, которые работают на досках, каскадным образом возрастёт и нагрузка на сервис пользователей.
В данном случае лучше использовать взаимодействия типа «публикация-подписка». Тогда можно сделать репликацию данных о пользователях в сервисе досок на основании событий, которые публикует сервис пользователей. Это удачное решение, потому что:
Мы избавляемся от зависимости между сервисами. Даже если сервис пользователей будет недоступен, это не повлияет на сервис досок.
Мы экономим на ресурсах приложения. Если нагрузка на сервис досок возрастёт, нам не придётся масштабировать сервис пользователей.
Сервису досок требуется только маленькая часть данных о пользователях. Благодаря этому мы можем не хранить все данные целиком, что значительно снизит объём реплики.
После внедрения EDA архитектура приложения стала такой:
Мы подробно разобрали, как выбрать характер взаимодействия, если вам нужно получить данные. Перейдём ко второму сценарию и обсудим, как выбрать паттерн, если требуется инициировать действия.
Вам нужно инициировать действие: команда или событие?
В случаях, когда требуется инициировать какое-то действие или изменение в другой системе, обязательно оцените, можно ли команду заменить событием.
Мы уже разбирали этот вопрос в уроке о проектировании Event-Driven архитектуры. Событие и команду довольно часто путают.
💡 Коротко напомним: ключевой характеристикой события является то, что поставщик напрямую не зависит от результата обработки события его потребителем. Если процесс внутри поставщика события может быть завершён вне зависимости от результата обработки, то это скорее событие. В случае, если для логического завершения процесса в системе-инициаторе нужно получить результат обработки сообщения, перед вами команда.
Важным отличием команды от события также является то, что в случае команды сообщение предназначается для одного получателя. В случае события круг получателей потенциально не ограничен: одно событие может быть прочитано несколькими получателями.
Пример. Представьте, что работаете над приложением, где есть сервис продаж и сервис уведомлений. Сервису продаж нужно, чтобы сервис уведомлений отправил пользователю письмо — сообщил, что заказ пользователя размещён. А сервису уведомлений нужно отправить пользователю письмо через почтовый сервер.
Сервис продаж не управляет логикой уведомлений, он вообще ничего об этом не знает. Поэтому здесь нужно использовать событие, а следовательно, тип интеграции будет «публикация-подписка». Сервис продаж публикует событие об изменении в статусе заказа. При этом его не интересует, будет ли сообщение прочитано кем-то и будет ли отправлено уведомление.
В случае с отправкой уведомления всё работает по-другому. Почтовый сервер — это внешний сервис. Он предоставляет общий публичный API, который просто выполняет служебную функцию — отправляет письмо. При этом он не содержит в себе какой-либо логики. Сервис уведомлений, напротив, содержит в себе логику: он знает, кому отправить письмо, как отправить и что делать, если вдруг письмо не отправится. Он отправляет определённую команду, ожидает результат и принимает решение, что делать дальше в зависимости от результата. Здесь уже используется команда и тип взаимодействия «точка-точка».
В контексте интеграций выбор между командой и событием важен. Он может повлиять на выбор используемой технологии. О том, как выбрать технологию для интеграции, мы поговорим в следующем уроке.
Мы обсудили характер взаимодействия и определили, как выбрать между паттернами «точка-точка» и «публикация-подписка» в разных сценариях. Теперь перейдём ко второй категории — способу взаимодействия. Расскажем, как выбрать между синхронным и асинхронным взаимодействием.
Как выбрать способ взаимодействия
После того, как вы определились с выбором между взаимодействием «точка-точка» и «публикация-подписка», нужно решить, будет интеграция синхронной или асинхронной.
Обратите внимание: способ взаимодействия зависит от характера взаимодействия.
Если характер взаимодействия «публикация-подписка»
💡 В случае с типом взаимодействия «публикация-подписка» выбора у вас нет. Такие взаимодействия по умолчанию считаются асинхронными.
Синхронная реализация теоретически возможна, но она противоречит основным условиям реализации паттерна:
Система-поставщик событий должна быть независима от потребителей.
Потребители должны иметь возможность подписываться на события, но это не должно требовать изменений в системе, которая публикует события.
Второе требование ещё можно выполнить — например, с помощью конфигураций. Но первое условие реализовать очень трудно: синхронное взаимодействие делает систему-поставщика ответственной за доставку, а значит, она становится зависимой от доступности и сбоев потребителей.
Если вы решили реализовать взаимодействия типа «публикация-подписка», по умолчанию выбирайте асинхронные способы интеграции.
Если характер взаимодействия «точка-точка»
💡 Взаимодействия типа «точка-точка» могут быть как синхронными, так и асинхронными.
Синхронные взаимодействия, как правило, рекомендуется использовать в ситуациях, когда ответ нужен в режиме реального времени. Чаще всего такая необходимость продиктована требованием отобразить какую-то информацию пользователю на UI. При этом UX построен таким образом, что информацию пользователю нельзя доставить позже — например, отправить в виде уведомления.
Учитывайте, что синхронные взаимодействия могут доставлять сообщения быстрее, чем асинхронные. Всё потому, что при использовании технологий асинхронной доставки сообщений возникают дополнительные накладные расходы на операции, которые выполняет сам брокер. Однако реальные показатели сильно зависят от среды, где происходит взаимодействие, а также от конфигураций средств, которые используются при взаимодействии.
Плюсы асинхронного взаимодействия
В условиях высоконагруженных приложений по умолчанию рекомендуется рассматривать реализацию асинхронного взаимодействия. Многое зависит от выбранного средства интеграции, однако в общем случае асинхронное взаимодействие обладает несколькими важными плюсами:
Независимость систем потребителя и провайдера сервиса. Системы не зависят от доступности друг друга. Задача потребителя — отправить сообщение в брокер. Большинство брокеров предлагает целый ряд механизмов для повышения гарантии доставки. Многие из них могут гарантировать доставку At Least Once, то есть одно сообщение будет доставлено как минимум один раз. Если вы к тому же применяете идемпотентную обработку-сообщение, вы сможете, по сути, гарантировать доставку Exactly Once.
Гибкое масштабирование. Использование асинхронного взаимодействия позволяет более гибко масштабировать системы. Даже если на систему-потребитель резко выросла нагрузка, в результате чего она отправила большое количество сообщений, система-провайдер может продолжить обрабатывать их с той скоростью, которая ей доступна. Конечно, если это изначально было оговорено с её потребителями и такие SLA всех устраивают.
Обеспечение высокой производительности. Современные брокеры имеют пропускную способность в сотни тысяч и даже в миллионы сообщений в секунду.
Когда использовать синхронное взаимодействие
Синхронное взаимодействие рекомендуется использовать при двух условиях:
Ответ на запрос требуется в реальном времени. Например, для дальнейшего отображения пользователю на экране. Допустим, клиент разместил заказ и хочет сразу узнать, успешно ли завершилась операция.
Операция действительно в среднем выполняется не более чем за несколько секунд. А когда мы говорим о высоконагруженных приложениях, то, как правило, не более пары сотен миллисекунд. Если среднее время отклика больше, оно может привести к негативному опыту пользователей и избыточному использованию ресурсов сети. Долгое время ответа в условиях высокой нагрузки означает большое количество одновременно открытых сетевых подключений, что может привести к ошибкам забивания пула соединений. Это усложнит сопровождение вашей системы.
Синхронные интеграции, которые не соответствуют этим условиям, часто становятся узкой точкой и мешают масштабированию системы.
Сергей
Можно ли комбинировать подходы?
Как комбинировать синхронное и асинхронное взаимодействие
Иногда могут возникать ситуации, когда ответ требуется в режиме реального времени, но при этом очевидно, что вызываемая операция не может быть выполнена быстро. Например, при создании заказа требуется провести оплату, забронировать товары, создать накладную, забронировать слот в доставке и так далее.
Если рассматривать самую простую реализацию, то можно было бы отправить сообщение на создание заказа асинхронно, а пользователю отобразить, что запрос в обработке. Однако в некоторых ситуациях так сделать не получится.
В этих случаях рекомендуем проанализировать процесс более детально.
Пример. В случае с заказом, возможно, достаточно просто отобразить клиенту на экране номер заказа. Это будет означать, что заказ гарантированно принят. При этом о дальнейших статусах можно сообщать посредством уведомлений или через обновление состояния на экране.
В таком случае вы можете комбинировать подходы. Например, использовать синхронный вызов, где в ответе будет получен номер заказа. При этом получение ответа будет гарантировать, что система-провайдер приняла заказ в работу и, например, положила во внутреннюю очередь сообщений с заказами. Это, в свою очередь, гарантирует, что заказ не потеряется. Дальнейшая обработка может происходить асинхронно, а изменения состояния заказа будут публиковаться асинхронно в виде событий.
💡 Не путайте комбинирование способов взаимодействия с их «вложением». Не стоит без необходимости использовать синхронное взаимодействие внутри асинхронного.
Давайте разберёмся с этим на примере.
Как реализовать асинхронное взаимодействие внутри синхронного
Иногда на практике может возникнуть такая ситуация: вы разрабатываете приложение, которое предоставляет API для UI, в котором работают пользователи. В рамках одного из запросов требуется обратиться к другой системе, которая предоставляет только асинхронный API.
Например, в рамках всё той же обработки заказа вам необходимо вызвать приложение для проверки доступности товаров на складе. Это нужно сделать до того, как вы подтвердите заказ пользователю. Однако для проверки товара доступно только асинхронное API — например, через очереди в RabbitMQ.
Дополнительно предлагаем исходить из того, что мы разрабатываем распределённое приложение, запущенное в нескольких экземплярах. Главная особенность асинхронного взаимодействия в условиях распределённой среды заключается в том, что сообщение может прочитать любой из экземпляров. Например, сообщение с запросом может отправить экземпляр № 1, а ответ прочитать экземпляр № 2.
Теперь представим, что запрос на создание заказа получил один экземпляр приложения. Он отправил сообщение с запросом остатков на складе. Ответное сообщение прочитал уже другой экземпляр. Всё это время соединение клиента с экземпляром, который обработал заказ, остаётся открытым в ожидании ответа.
Необходимо решить, как синхронизовать информацию между экземплярами.
Сергей
Как решить эту задачу?
Как правило, в качестве решения используется какое-то внешнее хранилище. Например, результат может быть сохранён в таблицу, которую необходимо постоянно опрашивать, а при получении результата вернуть ответ на UI. Зачастую как более эффективное решение можно использовать кеш. Более того, в таком решении, как Redis, есть механизм обмена событиями, который позволяет повысить общую производительность процесса синхронизации.
Однако все эти решения значительно увеличивают общую сложность архитектуры и стоимость сопровождения системы. Такие реализации вполне применимы в случае использования паттерна взаимодействия Server Push для front-to-back взаимодействия. В случае использования Client Pull рекомендуется изначально определить, что результат не будет получен в рамках одного подключения, и использовать Long Polling на стороне клиента.
Потребителем API необязательно должен быть UI. Все эти проблемы актуальны, когда потребителем является и какой-то другой сервис. Однако в случае, когда вы проектируете именно back-to-back взаимодействие, использование решений, описанных выше, считается антипаттерном. В этом случае гораздо эффективнее сразу использовать асинхронное взаимодействие вместо реализации асинхронного внутри синхронного.
Мы разобрали разные виды back-to-back взаимодействий. Осталось только закрепить знания, выполнив пару заданий, и подвести итоги урока.
Задание 1
Выберите, какие особенности относятся к типу взаимодействия «точка-точка», а какие — к типу «публикация-подписка».
Точка-точка
Система-инициатор обращается к конкретной системе, чтобы получить данные или отправить команду
Система-инициатор обращается к конкретной системе, чтобы получить данные или отправить команду
Сообщение обрабатывает одна система
Сообщение обрабатывает одна система
Публикация-подписка
Сообщение может быть обработано потенциально неограниченным количеством потребителей
Сообщение может быть обработано потенциально неограниченным количеством потребителей
Система-инициатор не ожидает ответа от систем, получивших сообщение
Система-инициатор не ожидает ответа от систем, получивших сообщение
Правильных ответов: 4, неправильных ответов: 0.
Задание 2
Перед вами несколько ситуаций. Выберите для каждой ситуации подходящий вид взаимодействия.
Для сервиса платежей лучше использовать синхронное взаимодействие «точка-точка», так как требуется получение актуальных данных в режиме онлайн, а время ответа очень короткое. Такой SLA проще контролировать посредством синхронных взаимодействий. Однако на стороне сервиса балансов потребуется обеспечить те SLA, которые нужны его потребителям.
Для сервиса аналитики оптимально будет использовать «публикацию-подписку», так как речь идёт об обмене данными между двумя полностью независимыми процессами. Реализация получения данных посредством взаимодействия точка-точка приведёт к лишней зависимости между несвязанными сервисами и лишней нагрузке на сервис заявок.
Для сервиса оценки кредитных рисков лучше использовать асинхронный запрос-ответ: нужно отправить команду в конкретный сервис, но этот сервис может отвечать довольно долго. Держать всё это время открытое соединение неэффективно.
Итоги
В этом уроке вы изучили основные виды back-to-back взаимодействий.
Глобально все взаимодействия между приложениями можно разделить на две категории:
По характеру взаимодействия выделяют паттерны «точка-точка» и «публикация-подписка». При взаимодействиях «точка-точка» система-инициатор обращается к конкретной системе за конкретным действием. При взаимодействиях «публикация-подписка» систему-инициатор не интересует, кем это сообщение будет прочитано, как оно будет обработано и будет ли оно обработано вообще. В этом случае получателей сообщения может быть несколько.
По способу взаимодействия интеграция может быть синхронной или асинхронной. При синхронных взаимодействиях система-инициатор ожидает, что результат обращения ей вернут в рамках того же сетевого подключения. Асинхронное взаимодействие предполагает, что результаты или ответы могут быть получены асинхронно.
Чтобы выбрать между паттернами «точка-точка» и «публикация-подписка», сначала определите сценарий взаимодействия. Их всего два:
Вам нужно получить данные. В этом случае выбор характера взаимодействия зависит от того, будете вы использовать запрос данных или репликацию. Запрос данных — это простой и эффективный способ, но перед его использованием убедитесь, что у вызываемого сервиса достаточно быстрое время отклика, он сможет обеспечить нужное вам RPS и вас устраивает его класс критичности. Если это не так, можно доработать вызываемую систему (дорого, долго, но эффективно) или использовать CQRS и репликацию данных. У репликации данных есть четыре возможных риска. Мы обсудили, что может произойти и как минимизировать проблемы.
Вам нужно инициировать какое-то действие. Здесь выбор стоит между командой и событием. В случаях, когда требуется инициировать какое-то действие или изменение в другой системе, обязательно оцените, можно ли команду заменить событием.
Между синхронным и асинхронным взаимодействием выбирают так:
Если вы используете паттерн «публикация-подписка», то взаимодействия по умолчанию будут асинхронными.
Если вы используете паттерн «точка-точка», можно выбрать синхронное или асинхронное взаимодействие. У каждого из них есть как свои плюсы, так и свои ограничения. В некоторых случаях разные способы взаимодействия можно сочетать между собой, но рекомендуется не делать их вложенными — не реализовывать асинхронное взаимодействие внутри асинхронного. Этот подход — антипаттерн в back-to-back взаимодействиях.
С типом взаимодействия определились. В следующем уроке расскажем, какие технологии можно использовать для разных типов.