Использовать микросервисную архитектуру или нет? Разбираемся с плюсами и минусами, внутренней коммуникацией, принципами и заблуждениями.
Виды архитектуры
Простейший и популярный вариант архитектуры – монолитная. Каждый начинал с неё, и здесь нет никакой изоляции и распределённости: один монолит обрабатывает все запросы.
Отчего возникают следующие проблемы:
- отказоустойчивость;
- горизонтальное масштабирование;
- применение одной технологии или языка и невыгодность переписывать огромный монолит;
- сложность рефакторинга из-за хранения кода в одном месте и куча legacy;
- трудности работы в команде разработчиков;
- чтобы использовать повторно, придётся дробить.
Второй по популярности вид архитектуры – пара монолитов, микс из монолита и сервисов или даже микросервисов. То есть вы сохраняете монолит, а доработки выполняете с использованием современных технологий.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39d7781d.jpg)
Это частично решает проблемы отказоустойчивости, масштабируемости и одного стека технологий.
Микросервисная архитектура – не новая идея, а разновидность сервис-ориентированной архитектуры. Сервис-ориентированная архитектура предусматривает модульность разработки и слабую связанность компонентов, поэтому получаем изолированную и распределённую систему.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39ddb770.jpg)
Главный минус – общая шина данных Enterprise Service Bus с огромными спецификациями и сложностями работы с абстракциями и фасадами.
Микросервисная архитектура
Микросервисная архитектура наследует от предшественницы изоляцию и распределённость. Здесь база данных не используется как шина данных, за исключением отдельных случаев в пользу производительности. По классической схеме компоненты изолируются и на уровне кода, и на уровне базы.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39e94940.jpg)
Для микросервисов применяют контейнеризацию с оркестрацией и другими плюшками.
Следующее преимущество – протоколы обнаружения сервисов. Оцените наглядно разницу коммуникаций сервис-ориентированной и микросервисной архитектуры: у последней нет общей шины, и сервис обращается к любому другому напрямую:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39ee996c.jpg)
Выбор протоколов общения зависит от программиста. Например, вы используете REST для публичных запросов и RPC через AMQP для внутренних либо один общий протокол для всех.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39f717a7.jpg)
Разделяют микросервисы с точки зрения либо бизнеса, либо программиста для переиспользования. Но мешают этому две вещи:
- внутренние связи – при тесном взаимодействии микросервисы объединяют;
- транзакции – у разных микросервисов базы данных изолированы, а нужна одна общая.
Рассмотрим пример разделения сервисов. Вначале поставили задачу прикрутить конфигурацию к авторизации, позже потребовалась аналогичная для кошелька. Само собой, напрашивается выделение конфигурации в отдельный микросервис. Для этого копируют код из предыдущей задачи, добавляют RPC для внутренних связей, абстракцию над клиентом для удобства и начинают использовать в других микросервисах.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef39fe5156.jpg)
Достоинства и недостатки микросервисной архитектуры
Как в любой распределённой архитектуре, получим накладные расходы на коммуникацию.
Концепция непрерывной интеграции и доставки (CI/CD) и построение архитектуры (контейнеризация, оркестрация, мониторинг и другое) требует большого количества времени.
Что насчёт отказоустойчивости? Часто её определяют как «падение одного сервиса не отражается других». Представьте, падает Audit
, а Wallet
теоретически продолжает работать – похоже на отказоустойчивость:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a04ba19.jpg)
А как же запросы по RPC, которые Wallet
продолжает слать? Необходимо программно предусмотреть ситуацию, когда Audit
не отвечает, и грамотно настроить rollback, поскольку базы разные, и транзакционно это сделать не получится.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a0dcac2.jpg)
Или другая ситуация: падает микросервис авторизации, через который ходят другие. Чтобы продолжать обрабатывать запросы, добавляют код для неавторизованного пользователя. По существу, это мощный отказ.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a133b8c.jpg)
Масштабируемость у микросервисов достигается благодаря системам оркестрации, ведь остаётся достаточно серверных ресурсов в противоположность монолиту, который потреблял всю память и процессор.
Стандартный процесс разработки – кодинг, тестирование и развёртывание – в микросервисной архитектуре выглядит иначе. Первые два этапа сливаются, поскольку микросервис взаимодействует с кучей других. Чтобы локально сделать хоть один запрос, придётся запустить все эти микросервисы, поэтому тестирование вручную не подходит для подобной задачи.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a192719.jpg)
Представьте, вы тестируете перевод средств в банковской системе. Для этого вы создаёте двух пользователей, компанию, агента для пополнения счёта, делаете эмиссию в систему, чтобы пополнить хранилище агента деньгами, вносите средства на личный счёт отправителя и только потом делаете перевод получателю.
Будете выполнять столько запросов вручную? Добавьте ещё сложности взаимодействия микросервисов и получите ад.
Поэтому локально разработчик проводит юнит-тестирование, где вместо ответов микросервисов будут mock-объекты. Ещё понадобятся функциональные тесты, например, для отлавливания проблем коммуникации, а также интеграционные тесты. Они прогоняются вместе с юнит-тестами на этапе слияния рабочий копий в главную ветку разработки. И только потом программист проверяет функциональность руками. До развёртывания релизную версию тестирует QA.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a21595d.jpg)
Микросервисная архитектура делает компоненты независимыми при разработке и развёртывании, чего не было в монолите.
Микросервисы используются повторно и экономят средства в плане бизнеса.
Программист получает относительную свободу в выборе языка и технологий для разработки отдельных частей проекта.
Контроль зависимостей
Трудно сопроводить и поддерживать 50 проектов с 50 репозиториями, если вдруг обнаружится баг безопасности, который нужно срочно пофиксить во всех них. Поэтому используйте контроль зависимостей, общие библиотеки для микросервисов и семантическое версионирование. Причём не делайте одну раздутую библиотеку, а разбивайте её на кучу маленьких, чтобы избежать страха выпуска мажорной версии.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a2b7f6f.jpg)
Инвертируйте зависимости: прописывайте версию драйвера в пакет, куда добавляете нужные микросервисы, чтобы потом не пришлось сломя голову перепроверять версии во всех проектах. Для внешних зависимостей лучше зафиксировать версии.
Базы данных
Поскольку базы данных в микросервисной архитектуре изолированные, вы используете разные их виды одновременно и получаете повышенный уровень безопасности, благодаря общению сервисов только через RPC. Но что делать, когда объединяемые данные в разных микросервисах?
Вот возможные решения проблемы:
- храните одно и то же значение в двух микросервисах, но появляются трудности с актуализацией данных;
- делайте RPC, правда, усложните работу с большими объёмами информации;
- выгрузите данные из всех баз для аналитики;
- сделайте миграцию данных, что тоже непросто и повлечёт написание RPC.
Представьте другой случай: при регистрации создаётся 3 сущности (пользователь, профиль и счёт), но на полпути что-то падает. Как откатить изменения, если данные распределяются по разным микросервисам? Думайте об этом на этапе проектирования.
Внутренняя коммуникация в микросервисной архитектуре
Для общения микросервисам нужен контракт: протокол и валидация данных. С последним справляется JSON Schema, но протокол также необходим. Требования при выборе способа коммуникации:
- строгость;
- общий протокол для сервера и клиента;
- генерация кода для любого языка;
- производительность.
В качестве протоколов используют Protocol Buffers, FlatBuffers, Apache Thrift. Сначала вы пишете предметно-ориентированный язык, отдаёте это программе-генератору кода и получаете сгенерированный клиент и сервер.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a3b4253.jpg)
Организация работы в команде
Команды делят по технологиям и следят за их размерами (не более 7–8 человек). Как правило, задачи выдают сеньорам, рядом с которыми хватает разработчиков уровнем ниже.
К примеру, два сеньора из одной команды получили по два микросервиса. Важно, чтобы программисты взаимодействовали между собой не только в локальной группе по конкретной задаче, но и с другими. Тогда в области их знаний будет много общего:
- язык;
- организация и шаблонизация кода для каждого микросервиса;
- библиотеки;
- концепция непрерывной интеграции и доставки;
- протокол коммуникации;
- документация.
А что объединит разнородные по языкам команды? Останется только CI/CD, глобально стандартизованный прокол и подход к документированию.
Устройство микросервисов
Микросервисы состоят из трёх слоёв: небольших обработчиков, бизнес-логики и мапперов данных. Последовательность выполнения запроса выглядит так:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a499e0f.jpg)
В сервисном слое сосредотачивается 99% всего кода. Поскольку в микросервисе несколько обработчиков, используйте Data Transfer Object (DTO), к которому вы будете приводить GET-запрос. Это облегчает обработку и валидацию.
Примеры кода
Не забывайте, что Node.js – асинхронный язык. Рассмотрим пример: приходит запрос, синхронно берёт HolyJSService
, асинхронно работает с маппером, выполняющим запись в базу данных, и процесс повторяется для последующих запросов. Никакого порядка в этом не наблюдается, поэтому выделяют два архитектурных подхода:
- stateless – не хранить состояние;
- stateful – добавлять объекту класса поля, которые нужны для выполнения запроса.
Когда используете stateful, при получении запроса вы создаёте новый объект HolyjSService
и заполняете необходимыми для выполнения данными. Дальше этот сервис асинхронно идёт в базу данных. Следующий запрос берёт новый экземпляр сервиса, и процесс повторяется.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a562e3d.jpg)
В коде вы реализовываете абстрактный класс с кучей полей и сеттеров, наследуете от него новый сервис и наполняете методами.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a65af61.jpg)
Дальше обработчику нужен контейнер DI для получения нового экземпляра, который он пробрасывает в функцию. После её запуска и выполнения всех сеттеров вызываем сервисы в методе create
и возвращаем результат.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a72727a.jpg)
При stateless-подходе используется контекст, который создаётся для каждого запроса отдельно, и единственный экземпляр сервиса, поскольку выполнение запросов от него не зависит.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a7c6238.jpg)
В контексте хранятся все необходимые поля и собственно метод для его создания:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3a88231f.jpg)
На уровне сервиса выглядит как дополнительный параметр:
![](https://evilinsider.ru/wp-content/uploads/2020/10/mikroservisnaya-arxitektura-v-razreze-img_5f7ef3b754e72.jpg)
Для обработчика добавляем контекст вторым параметром – его возвращает метод createContextFromHttpRequest
:
![](https://evilinsider.ru/wp-content/uploads/2020/10/mikroservisnaya-arxitektura-v-razreze-img_5f7ef3b7f2d70.jpg)
Сами по себе хэндлеры предельно простые:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3b345197.jpg)
Поскольку для выполнения запросов часто нужны другие сервисы, контекст приходится прокидывать в них. Допустим, на первый микросервис поступил запрос. Присваивайте ему идентификатор X-Request-Id
, с которым он пройдётся по всем микросервисам. Благодаря логированию вы легко отследите путь внешнего запроса. X-Trace-Id
используется для обозначения единой бизнес-операции, состоящей из нескольких запросов.
Для создания идентификаторов используют OpenTracing.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3b432d6b.jpg)
Рассмотрим пример, когда не получается написать микросервис без сервиса. Вам дали задачу реализовать управление доступом на основе ролей (RBAC). Вы проектируете три сущности: роль, ресурс и правило. Пишете RPC, а из соображений производительности и удобства добавляете дополнительный сервис для работы с этим RPC, ролями, правилами и ресурсами.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3b5057fb.jpg)
Теперь отдельные микросервисы используют созданный сервис и общий интерфейс.
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3b5d20ba.jpg)
Если команде, которая пишет на другом языке, также понадобился RBAC, придётся написать для них дополнительный RPC. Однако не получится создать RPC для добавления фильтров или условий к управлению доступом.
Заблуждения
1. Это легче
После прочитанного вы вряд ли посчитаете микросервисную архитектуру лёгким делом, но это распространённое заблуждение. Сложность создания микросервиса наглядно показывает график:
![](https://evilinsider.ru/wp-content/uploads/2020/10/img_5f7ef3b6b4595.jpg)
С самого начала вы медленно и печально разбираетесь с документацией, бесконечно подбираете протоколы и другие компоненты. К энному микросервису вы нарабатываете стандартные шаблоны, что в несколько раз облегчает процесс.
С точки зрения поддержки – наоборот, потому что увеличивается количество микросервисов. Новый разработчик вынужден будет часами сидеть за изучением диаграмм, чтобы понять глобальную картину и принцип работы системы.
2. Лучше производительность
Представьте, сколько придётся ждать отправителю комплексного запроса, пока он сходит в кучу разных микросервисов. Поэтому ради производительности переезжать на эту архитектуру не всегда целесообразно. В качестве альтернативы перепишите куски проекта на более эффективном языке, или добавьте профилирование к монолиту.
Заключение
Принимайте решение об использовании микросервисной архитектуры, только чётко осознав и взвесив все достоинства и недостатки. Вам будут нужны знания о создании распределённой архитектуры и возможность реализации необходимых структурных элементов со стороны бизнеса. Ведь без логирования, мониторинга, трассировки, непрерывной интеграции и оркестрации вы обрекаете себя на ад.