Микросервисная архитектура на примерах
В этой статье мы рассмотрим некоторые принципы и паттерны микросервисной архитекуры, критерии выбора между микросервисами и монолитом, ограничения, а также разные подходы к организации межсервисного взаимодействия.
Итак приступим. На представленной ниже схеме представлены 4 микросервиса:

Центральным звеном архитектуры является микросервис get_payments_by_client_front, который по коду контрагента должен возвратить информацию о платежных документах (выставленные счета, платежные поручения, кассовые ордера итд), связанных с этим контрагентом за определенный период, и некоторую дополнительную информацию о самом контрагенте (ИНН, ОГРН и проч).
При этом сервис get_payments_by_client_front обращается в 3 сервиса:
get_client_info_by_client_back – для получения и возврата дополнительной информации о самом контрагенте
get_payments_by_client_back – для получения данных по платежным документам
get_payment_additional_info — для получения дополнительной информации о документе, необходимой только для некоторых платежных документов, а именно о платежах связанных со сделками с недвижимостью. Эта информация содержит дополнительные сведения об объекте недвижимости из Росеестра
Сразу могут возникнуть вопросы о правильности такой архитектуры:
1) Зачем понадобился сервис get_payments_by_client_back, который делает почти тоже самое, что и get_payments_by_client_front? Не логичней ли, чтобы get_payments_by_client_front сам ходил в базу данных платежей и делал бы необходимую выборку. Так бы мы сократили затраты на лишнее сетевое взаимодействие (рисунок 2)

2) Почему не построить архитектуру так, чтобы сервис get_payments_by_client_back возвращал сразу всю необходимую информацию по платежам и не требовалось бы обращаться в get_payment_additional_info (рисунок 3)

3) Почему не организовать архитектуру так, чтобы вообще не требовалось вызывать другие сервисы, а get_payments_by_client_front сам получал всю необходимую информацию склеивая данные непосредственно на уровне БД (рисунок 4)

Плюсы и минусы монолита и микросервисов
Мы специально начали данную статью с примера, но теперь нужно немного погрузиться в теоретическую часть. Поскольку в данном материале, да и в других разделах сайта, речь идет именно о микросервисной архитектуре, то сделаем акцент на аспектах, которые позволяют сделать выбор между микросервисом и монолитом. И первое, что нужно отметить, это факт, что микросервисная архитектура привносит определенные сложности в процесс разработки, тестирования и развертывания приложений, поэтому если есть возможность обойтись монолитной архитектурой, сохраняя требуемый цикл выпуска релизов и необходимую масштабируемость, то следует остановиться именно на ней. Однако поддерживать монолит бывает достаточно трудозатратно. Каждый выпуск релиза, каждое изменение, затрагивают все приложение целиком, а это значит, что вероятность сбоя при развертывании (особенно в случае необходимости поддержки бесперебойной работы 24/7) существенно возрастает при монолитном подходе. Разбить группу разработки на команды, каждая из которых поддерживает определенный функционал и может вообще не знать ничего о нюансах реализации другой функциональности в случае монолита также сложнее. Процесс тестирования и развертывания при внесении изменений лишь в одну бизнес-область будет занимать больше времени, так как пересобираться будет все приложение целиком. С масштабируемостью тоже будут проблемы, так как разная функциональность приложения имеет разные требования к нагрузке (например, работа с платежными документами, которые формируются постоянно в течение рабочего дня, требует больше ресурсов, чем модуль данных о контрагентах), а в случае монолита горизонтально масштабировать (грубо говоря, увеличивать количество реплик приложения) можно только все приложение целиком, а не отдельный модуль.
Итак, предположим, что взвесив все за и против (про минусы микросервисной архитектуры будет отдельная статья) разработчики сделали выбор в пользу микросервисов. Теперь необходимо определиться по какому принципу будем дробить функциональность на микросервисы.
Масштабирование микросервисов
Наиболее популярным подходом является разбиение функционала на микросервисы по бизнес-областям. При этом для дальнейшего масштабирования можно управлять количеством реплик каждого микросервиса, а если и этого недостаточно (например, из-за большого объема БД), то можно сделать так, чтобы каждый набор экземпляров микросервиса работал со своей шардой (своим набором данных). В случае шардирования можно назначить каждой группе реплик свой номер, а относить данные к нужной шарде можно беря остаток от деления ключевого идентификатора (например, ИД клиента) на номер шарды. Для некоторой функциональности данные, относящиеся к одной шарде, можно закачать в оперативную память целиком и при запросах делать выборку не с диска, а оперативной памяти. Описанное выше масштабирование принято называть кубом масштабирования, при котором ось Y соответствует функциональному разбиению на бизнес-области, ось X горизонтальному дублированию экземпляров одного микросервиса, а ось Z шардированию (секционированию) данных.

Паттерн CORS
Вернемся к нашему примеру. Исходя из представленной функциональности, можно выделить две бизнес-области (и, как минимум, два микросервиса) — контрагенты и платежные документы. Чтобы исключить взаимное влияние микросервисов, каждый микросервис должен иметь свою БД, но значит ли это, что для реализации описанной в примере функциональности нам понадобится более одного микросервиса? Вовсе нет. Все зависит от выбранных нами подходов. Одним из подходов является паттерн CORS – разделение ответственности командных запросов. Суть данного подхода состоит в поддержке отдельной БД, являющейся витриной для запросов выборки данных (рисунок 4) и микросервиса работающего с данной БД. После изменения данных в БД какого-либо микросервиса формируется событие для микросервиса, поддерживающего БД для запросов выборки, и последний вносит соответствующие изменения в эту БД. Структура БД запросов выборки должна быть такой, чтобы можно было быстро получить информацию в том виде, в котором ее необходимо отдать конечным потребителям. Шаблон CORS эффективен, когда объединить данные из нескольких функциональных областей в оперативной памяти трудозатрано (например, по причине большого объема данных), однако минусом данного подхода является его сложность и отставание репликации данных. Сложность очевидно заключается в поддержке еще одной БД и механизма репликации. При этом, поскольку репликация данных в эту БД производится асинхронно (о минусах синхронного взаимодействия можно прочитать на нашем сайте), то состояние реплицированной БД будет отставать и есть риск получить при таком подходе неактуальную информацию.
Паттерн Объединение API
Альтернативным шаблону CORS является паттерн, который называется объединение API (рисунок 1). Собственно, этот подход и предложен выше, когда сервис get_payments_by_client_front обращается в 3 других сервиса за данными о платежных документах, контрагенте и дополнительной информацией о платежах по сделкам с недвижимостью. В данном примере потребуется объединить данные о платежных документах с дополнительной информацией о них. Выбранный нами подход основан на допущении, что период за который будут затребованы платежные документы небольшой, соответственно их количество не должно превышать несколько десятков и объединить данные сервисов get_payments_by_client_back и get_payment_additional_info не будет трудозатратно. Но, кроме того что надо объединять данные, нам нужно учесть накладные расходы на запрос для получения информации о контрагенте платежа и несколько запросов в сервис получения дополнительной информации о платежных документах. Поскольку данные запросы независимы, то их можно выполнять параллельно, но при выборе архитектуры необходимо заранее учесть требования по времени ответа (обычно они выражаются в виде 95 или 99 перцентилей, подробнее об этом в можно прочитать тут) и убедиться что выбранных подход им соответствует. На этапе проектирования убедиться в том, что get_payments_by_client_front будет отвечать за приемлемое время можно только теоретически, зная или предполагая время отклика используемых сервисов или сравнивая с аналогичными уже разработанными примерами.
Ограничения накладываемые на архитектуру в реальных кейсах
Однако, есть еще один вопрос. Вполне понятно, что данные о контрагентах относятся к другой бизнес области и хранятся в отдельной БД, но почему для получения дополнительной информации о платежных документах, относящихся к сделкам с недвижимостью, требуется обращаться в отдельный сервис get_payment_additional_info? Кажется странным, что для получения информации, относящейся к одной бизнес области, требуется два сервиса, а также странно почему не делать один запрос в get_payment_additional_info параметризуя его списком номеров документов? Ответ прост. Приведенный пример сделан так, чтобы быть максимально приближенным к реальности, а реальность может заключаться в том, что сервис get_payment_additional_info может уже существовать, при этом быть разработанным как другой командой нашей компании так и сторонними разработчиками. Если get_payment_additional_info – внешний сервис (например, сервис предоставляемый Росеестром), то у нас в принципе нет доступа к данным внешней БД и возможности что-то перепроектировать. Но, если предположить, что get_payment_additional_info разработан другой командой нашей компании, то почему нельзя перепроектировать архитектуру? Увы, для крупных компаний это может быть слишком трудозатратно. Понадобиться либо делать интеграцию с БД, используемой get_payment_additional_info, либо хотя бы переделать get_payment_additional_info, чтобы он поддерживал запрос по нескольким документам сразу. Даже второе может быть сложно реализуемо. Например, сервис get_payment_additional_info может быть частью системы процессинга платежей, а остальные сервисы являются частью продукта, который предоставляет информацию о платежах различным потребителям и, как раз, специально разрабатывается для того, чтобы снять часть нагрузки с процессинговой системы. В таком случае может быть сложно согласовать подход, при котором понадобиться дорабатывать мастер-систему процессинга, для реализации другой системы, которая предназначена как раз для снятия нагрузки на мастер-систему.
Паттерн API шлюз
Итак, мы разобрали функциональные роли каждого из 3-х микросервисов — get_client_info_by_client_back, get_payments_by_client_back и get_payment_additional_info. Но зачем нужен еще один сервис get_payments_by_client_front, который сам ничего не делает, кроме агрегации данных, полученных из других сервисов? Его функциональность вполне можно было бы реализовать внутри get_payments_by_client_back. Объяснить данный подход можно, если более широко рассмотреть предлагаемую архитектуру. В общем случае, у нашего продукта (получение информации о платежных документах) может быть несколько разных потребителей, которым глобально нужна одинаковая информация, но представляемая и группируемая по разному. Также различной будет и нагрузка на продукт, поступающая от разных потребителей. Кто-то будет делать несколько запросов в минуту, а кто-то несколько тысяч в секунду. В таком случае целесообразно под каждого потребителя сделать свой фасадный сервис, который будет запрашивать и группировать информацию именно так как нужно конкретному потребителю и масштабироваться с учетом нагрузки от конкретного потребителя. Такой шаблон называется API шлюз и он предполагает, что есть сервисы-поставщики нужных данных и фасадные сервисы, которые комбинируют информацию от сервисов-поставщиков для конкретного потребителя.
Выводы
Итак, в данной статье мы рассмотрели пример и разные варианты построения микросервисной архитектуры. Настало время подвести итоги:
1. Микросервисная архитектура сложна, поэтому перед проектированием информационной системы следует понять можем действительно ли она необходима. Ключевыми критериями выбора между микросервисами и монолитом будут:
— возможность поддерживать необходимую частоту выпуска релизов
— возможность горизонтального масштабирования
2. Для исключения взаимного влияния микросервисов друг на друга, каждый должен иметь свою БД
3. Для слияния данных из разных микросервисов можно использовать шаблоны разделения ответственности командных запросов (CORS) и объединения API. Если есть возможность объединить данные в оперативной памяти и нас удовлетворяет время ответа, с учетом того, что приходится делать несколько запросов в разные микросервисы, то следует сделать выбор в пользу объединения API
4. Проектировать микросервисную архитектуру часто приходится с учетом ограничений. В частности, если мы используем уже существующие микросервисы, то не можем их переделать с учетом наших требований.
5. Для выдачи информации внешним потребителям проектируют отдельные микросервисы, которые собирают данные из разных бизнес-областей в том виде, который необходим конкретному потребителю и с учетом нагрузки, поступающей от этого потребителя