Привет, Хабр! Прежде чем погружаться в проблемы, давайте я расскажу, как у нас устроена микросервисная архитектура и куда мы идём. К сожалению или к счастью, в сегменте B2B в банковском и околобанковском обслуживании клиенты чаще пользуются веб-версиями приложений. Большие списки, зарплатные ведомости, работа с документами — всё это проще делать на большом экране. Постепенный переход в мобильную среду начинается только сейчас. 

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

Микросервисы

Что из себя представляет наша архитектура? Она схематично представлена на рисунке 1. Давайте расскажу подробнее, что здесь есть и для чего.

Рисунок 1. Схема архитектуры СберБизнес.
Рисунок 1. Схема архитектуры СберБизнес.

Мобильное приложение устанавливает сетевое соединение с веб-сервером (NGINX) Mobile API Gateway, у которого есть две основные задачи:

  1. Предоставлять единую точку входа (один hostname) для мобильных приложений.

  2. Маршрутизировать запросы от «мобилы» на конкретное продуктовое приложение по контексту в слое Middleware.

Далее идёт слой Middleware — приложения без базы данных, но именно здесь понемногу появляется бизнес-логика. В этом слое реализуется канальная составляющая фронтального сценария на мобильном приложении: сбор данных из SCRUD-сервисов. По сути приложения в этом слое описывается программный конечный API для клиентской части (веб- и мобильного фронтенда). В этом слое есть как кросс-продуктовые сервисы — счета, подписание документов и другие, — так и сервисы исключительно продуктовые — кредитных, депозитных и других продуктов. 

Конечно, без базы данных никуда, и, по сути, Middleware выполняет функцию «второй прокси» в мобильном канале. То есть просто перенаправляет запрос от мобильного приложения в CRUD-сервисы, находящиеся на третьем шаге, обогащая его незначительными данными из авторизационного контекста. Именно на третьем этапе выполняется вся сложная бизнес-логика: калькуляция, расчёты, проверки и т. д.

Такая схема является отличной для веб-канала, где в слое Middleware каждая продуктовая команда описывает необходимый API для фронтенда, выстраивает интеграции с кросс-продуктовыми сервисами, и, в том числе, пишет фронтенд. Вся кодовая база для Middleware и фронтенда находятся в одном проекте репозитория. Максимально обособленная единица с точки зрения микросервисной архитектуры. Однако для мобильного приложения всё немножечко сложнее, ведь фронтенд лежит в другом репозитории — iOS/Android, и там есть свои взаимодействия. 

Архитектура мобильного приложения

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

Рисунок 2. Схема архитектуры мобильного приложения СберБизнес.
Рисунок 2. Схема архитектуры мобильного приложения СберБизнес.

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

Почему мы считаем, что у нас есть проблема?

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

Данные о счетах используются большинством продуктовых функциональностей: карточка счёта на главной, кредитные счета, оформление кредитов, депозитные счета, депозиты.

  1. «Единый источник правды». За получение счетов отвечает модуль «Счета» в мобильном приложении, а продуктовые модули взаимодействуют с его методами (см. рисунок 3).

  2. «Каждый получает сам». То есть для получения данных о счетах каждому модулю в мобильном приложении необходимо выполнить сетевой запрос в свой Middleware, который, в свою очередь, интеграционным методом получит данные о счёте из Middleware счетов. (см. рисунок 4).

Рисунок 3. Схема архитектуры «Единый источник правды».
Рисунок 3. Схема архитектуры «Единый источник правды».
Рисунок 4. Схема архитектуры «Каждый получает сам».
Рисунок 4. Схема архитектуры «Каждый получает сам».

Какие проблемы мы видим в схеме «Единый источник правды»

В этой схеме мы видим проблемы разработки «узкого горлышка» — кросс-продуктового модуля Accounts. Ведь у разных бизнес-функциональностей есть свои потребности по работе со счетами: фильтрация по определённым признакам, добавление новых атрибутов к счетам и т. д. Всё это должно выполняться в рамках CR (change request) на команду, которая отвечает за разработку модуля счетов. То есть другим командам придётся становиться в очередь и ожидать выполнения доработки. Я бы назвал эту схему «проблема замедления T2M новой фичи». 

Но у схем есть и свои преимущества:

  1. Схема «Единый источник правды» позволяет показывать актуальный остаток по счетам во всём приложении, при этом не будет возникать коллизий кеширования. 

  2. В случае, если API кросс-продуктового модуля полностью удовлетворяет потребностям продуктовой функциональности, то со стороны продуктового модуля не требуется разработка логики, инкапсулированной в кросс-продуктовом модуле: получение списка счетов, его фильтрация, обновление данных по счёту и т. д.

  3. Выполняется один сетевой запрос, что, в конечном счёте, положительно влияет на энергоэффективность мобильного приложения. 

Какие проблемы мы видим в схеме «Каждый получает сам»

Когда каждый продуктовый модуль самостоятельно получает данные с бэкенда, мы выделяем следующие потенциальные проблемы:

  1. В момент загрузки приложения отдельные продуктовые функциональности будут отправлять множество сетевых запросов на получение одних и тех же данных. Это, в конечном счёте, негативно скажется на энергоэффективности приложения и интернет-трафике клиента.

  2. Каждый модуль в мобильном приложении самостоятельно выбирает стратегию по работе с данными. Поэтому некоторые модули могут отображать неактуальные данные. В случае со счетами это будут неактуальные остатки.

К преимуществам схемы можно отнести нивелирование проблемы увеличения метрики T2M, так как в этом случае команда может ещё на уровне Middleware обогатить метод API теми данными, которые ей необходимы.

Что в итоге?

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

Комментарии (3)


  1. itmind
    11.04.2023 04:05

    Получается, что вы осознано выбираете более худшую архитектуру и жертвуете качеством ради быстрого добавления новых фич. А увеличение численности команды модуля "Accounts" не ускорит разработку?


  1. yeraa
    11.04.2023 04:05

    "Каждый получает сам"

    Обычно всё, что отображается, всегда приходит из "источника правды" (source of truth) - это всегда тот bounded context, который владеет данными и всеми бизнесс-правилами, которые оперируют над этими данными. Только так можно избежать неактуальность данных на экране. Поэтому UI в случае с распределёнными системами должен всегда строится исходя из принципов composite UI - все критические данные всегда отображаются UI компонентами которые логически принадлежат к модулям / bounded contexts, где эти данные "живут". Все данные, чьей согласованностью (? consistency) можно либо пренебречь, либо же вообще построить UI таким образом, чтобы эти данные были не нужны, могут быть загружены с кэшированой модели, которую каждый bounded context создает под свои нужды используя сообщения получаемые от других bounded contexts / aggregates через service bus.

    Исходя же из требований по минимизации соединений с backend, тут встаёт вопрос оптимизации - наверняка вы рассматривали фасады, которые на стороне сервера могут получить и скомпоновать данные из разных bounded context, тем самым уменьшая количество запросов с клиента.


  1. slepmog
    11.04.2023 04:05
    +1

    Единый источник правды позволяет показывать актуальный остаток по счетам, май эсс!


    Между тем, при работе с веб-интерейсом Сбера приходится после каждого чиха вылогиниваться и логиниться заново.
    Почему, возможно спросите вы?
    Потому что при переводе денег операции выполняются мгновенно, а соответствующие остатки по счетам/картам в интерфейсе не обновляются. Из-за этого интерфейс не позволяет этими деньгами воспользоваться, уверяет, что их там нет, хотя они там есть. И прозревает интерфейс только после повторного входа (Вовочка, выйди и зайди правильно).


    Вот я зашёл в веб-версию. Перевел с одного своего счета/карты на другой свой счет/карту немного денег (консолидировал финансы, так сказать). Операция выполняется мгновенно, СМС-подтверждение, все дела. Обновляю страницу — балансы счетов/карт не изменились. Идут минуты, жмётся F5, когда пореже, когда почаще. Жмется кнопка рефреша мышкой. Выполняются бессмысленные шатания между вкладками — Главная, Платежи, История, Главная… Не показывает интерфейс, что деньги переместились, и всё тут.
    В какой-то момент (не сразу, да) операция перевода даже появляется в "Истории". На интерфейс это не влияет никак. Остатки по счетам не изменяются.


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


    А платить-то надо сейчас.


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


    При переводах иными способами (с участием чужих счетов/СПБ) ситуация ровно такая же.