Недавно на конференции PGConf в Москве один из докладчиков демонстрировал «микросервисную» архитектуру, упомянув мимоходом, что все микросервисы наследуют от одного общего базового класса. Хотя никаких пояснений по реализации не было, создалось впечатление, что в этой компании термин «микросервисы» понимается не совсем так, как нас вроде бы учили классики. Сегодня мы будем разбираться с одной из интересных проблем — какой может быть общий код в микросервисах и может ли он быть вообще.
Что есть микросервис? Это отдельное приложение. Не модуль, не процесс, не что-то, что просто отдельно деплоится, а полноценное, настоящее, отдельное приложение. У него своя функция main, свой репозиторий в гите, свои тесты, свой API, свой веб-сервер, свой README файл, своя БД, своя версия, свои разработчики.
Как и контейнеры, микросервисы стали использовать, когда вычислительная мощность HW и надёжность сетей достигли такого уровня, что можно себе позволить вызов функции, который длится в 100 раз дольше, чем раньше, можно себе позволить потребление памяти в 100 раз выше, можно себе позволить роскошь поселить каждую «бабушку» не просто в отдельную «квартиру», а в отдельный «дом». Как и любые архитектурные решения, архитектура микросервисов в очередной раз приносит в жертву производительность, выигрывая в сопровождаемости кода для разработчиков. Но поскольку человек и скорость его реакции остались теми же, то системы продолжают удовлетворять требованиям.
Зачем дробить на отдельные приложения? Потому что часть сложности системы мы распределяем уже на уровне архитектуры системы. Процесс программирования — это вообще говоря поэтапное «откусывание» от большого изначального «куска сложности», а декомпозиция (на классы, модули, функции, а в нашем случае на целые приложения) — это реализация части этой сложности в виде структуры. Когда мы разбили систему на микросервисы, мы приняли архитектурное решение (удачное или нет), которое уже не нужно будет принимать разработчикам в дальшейнем при реализации конкретных частей функциональности. Известно, что этот конкретный микросервис отвечает за отправку емейлов, а этот — за авторизацию, так уже заведено, поэтому все мои новые фичи «ложатся» на этот паттерн без обсуждений.
Ключевым аспектом микросервисов является слабая связность. Микросервисы должны быть независимы, от слова «совсем». У них нет общих структур данных, а архитектура, технологии, способ сборки (и прочее) могут/должны быть у каждого микросервиса свои. По определению. Потому, что это есть независимые приложения. Изменения в коде одного микросервиса никак не должны влиять на другие, если только не затрагивается API. Если у меня есть N микросервисов, написанных на Java, то не должно быть никаких сдерживающих факторов, чтобы не написать N+1-вый микросервис на Python, если вдруг это выгодно по какой-то причине. Они слабосвязаны, и поэтому разработчик, который начинает работать с конкретным микросервисом:
а) Очень чутко следит за его API, потому что это единственный компонент, видимый снаружи;
б) Чувствует себя полностью свободным в вопросах рефакторинга;
в) Понимает назначение микросервиса (тут вспоминаем про SRP) и реализует новую функцию сообразно;
г) Выбирает способ персистентности, который наиболее подходит;
и т.д.
Всё это хорошо и звучит логично и стройно, как многие идеологии и теории (и тут идеолог-теоретик ставит точку и идёт на обед), но мы-то с вами практики. Код приходится писать вовсе не на сайте martinfowler.com. И рано или поздно мы сталкиваемся с тем, что все микросервисы:
- логируют информацию;
- содержат авторизацию;
- обращаются к брокерам сообщений;
- возвращают правильные сообщения об ошибках;
- должны как-то понимать общие сущности в системе, если таковые есть;
- должны работать с общим форматом (и протоколом) сообщений;
и делают это идентично.
И вот в какой-то момент идеолог-архитектор приходит утром на работу и обнаруживает, что ночью в системе появилась «библиотека» — новый репозиторий с общим кодом, который используется во многих микросервисах. Стоит ли архитектору приходить в ужас?
It depends.
Чтобы грамотно оценить ситуацию, следует вернуться к главной идее: микросервисы — это совокупность независимых приложений, взаимодействующих друг с другом через (сетевой) API. В этом мы видим главное преимущество и простоту архитектуры. И мы не хотим это преимущество потерять ни при каких обстоятельствах. Мешает ли этому общий код, который поместили в «библиотеку»? Рассмотрим примеры.
1. В библиотеке живёт класс «пользователь» (или какая-то другая бизнес-сущность).
- т.е. бизнес сущность не инкапсулируется в одном микросервисе, а размазывается по разным (иначе зачем её помещать в библиотеку общего кода?);
- т.е. микросервисы становятся связаны через эту бизнес сущность, изменение логики работы с сущностью повлияет на несколько микросервисов;
- это плохо, очень плохо, это уже не микросервисы совсем, хоть это не «big ball of mud», но весьма быстро мировоззрение команды приведёт к «big ball of distributed mud»;
- но ведь микросервисы в системе работают с одними и теми же концепциями, а концепции — это часто энтити, или просто структуры с полями, как быть? читать DDD, оно ровно про то, как инкапсулировать сущности внутри микросервисов, чтобы они не «летали» через API.
К сожалению любая бизнес-логика, помещённая в общую библиотеку, будет иметь такой вот эффект. Библиотеки общего кода имеют тенденцию разрастаться, в итоге посередине системы образуется логическая «опухоль», не принадлежащая никакому конкретному микросервису, и архитектура терпит крах. «Центр логической тяжести» системы начинает перемещаться в репо с общим кодом, и мы получаем адскую смесь монолита и микросервисов, а туда нам совсем не надо.
2. В библиотеку помещён код парсинга формата сообщений.
- Код скорее всего на Java, если все микросервисы написаны на Java;
- Если завтра я напишу сервис на Python, то использовать парсер не смогу, но вроде как это вовсе и не проблема, напишу питоновский вариант;
- Ключевой момент: если я пишу новый микросервис на Java, обязан ли я использовать этот вот парсер? Да наверно и нет. Пожалуй что не обязан, хотя мне, как разработчику микросервиса, это может весьма пригодиться. Ну как если бы я нашёл что-то полезное в Maven Repository.
Парсер сообщений, или улучшеный логгер, или обёрнутый клиент для посылки данных в RabbitMQ — это вроде как хелперы, вспомогательные компоненты. Они наравне со стандартными библиотеками из NuGet, Maven или NPM. Разработчик микросервиса — всегда король, он решает, использовать ли стандартную библиотеку, или сделать свой новый код, или использовать код из общей библиотеки хелперов. Как ему будет удобнее, потому что он пишет ОТДЕЛЬНОЕ И НЕЗАВИСИМОЕ ПРИЛОЖЕНИЕ. Конкретный хелпер может развиваться? Может, у него наверняка будут версии. Пусть разработчик ссылается в своём сервисе на конкретную версию, никто не заставляет обновлять сервис, при обновлении хелперов, это вопрос к тому, кто поддерживает сервис.
3. Java интерфейс, абстрактный базовый класс, трейт.
- Или другая штука из разряда «вырваный кусок кода»;
- Т.е. я вот тут вот, самостоятельный и независимый, а кусок моей печени лежит где-то еще;
- Тут появляется связанность микросервисов на уровне кода, поэтому мы не будем это рекомендовать;
- На начальных этапах это вероятно не принесёт каких-то ощутимых проблем, но суть проектирования архитектуры — это ведь гарантия комфорта (или дискомфорта) на годы вперёд.
Команда, начинающая работать над новым продуктом, закладывает основу архитектуры и имеет наибольшее влияние на то, какими тенденциями будет обладать продукт. Если в систему изначально заложены принципы SRP, удачной декомпозиции, низкой связности и т.д., то у неё есть шанс и дальше правильно развиваться. Если нет, то центробежное ускорение «факторов времени» (другая команда, мало времени, срочные заплатки, недостаток документации) выкинет дальше эту систему на обочину быстрее, чем кажется.
Вопрос общего кода в микросервисах остаётся непрост, потому что связан с некоторого сорта trade-off: мы взвешиваем, что в перспективе будет нам выгоднее — степень независимости микросервисов, меньше повторений в коде, квалификация инженеров, простота системы и т.д. Каждый раз это размышления и обсуждения, которые могут приводить к разным конкретным архитектурным решениям. Тем не менее, позволим себе суммировать некоторые рекомендации:
Рекомендация 0: Не называй микросервисами любую штуку, которая разбита на независимо существующие кусочки. Не всякая таблица с колоночками — матрица, давайте использовать термины правильно.
Рекомендация 1: Очень желательно, чтобы общего кода у микросервисов не было вообще.
Рекомендация 2: Если общий код всё же есть, пусть это будет совокупность (библиотека) необязательных к использованию «хелперов». Разработчик сервиса сам решает, использовать их или написать свой код.
Рекомендация 3: Ни при каких обстоятельствах в общем коде не должно быть бизнес-логики. Вся бизнес логика инкапсулируется в микросервисах.
Рекомендация 4: Пусть библиотека общего кода будет оформлена как типовой пакет (NuGet, Maven, NPM, etc), с возможностью версионирования (или, еще лучше, несколько отдельных пакетов).
Рекомендация 5: «Центр логической тяжести» системы должен всегда оставаться в самих микросервисах, а не в общем коде.
Рекомендация 6: Если задумал писать в формате микросервисов, то заранее смирись с тем, что код между ними будет порой дублироваться. До какой-то степени следует подавить в себе наш природный «инстинкт DRY».
Спасибо за внимание и удачных вам микросервисов.
Комментарии (16)
igormich88
30.05.2019 12:08Кстати по поводу хелперов общих и самописных. На работе столкнулся с тем что в двух проектах использовались разные библиотеки для работы с JSON. Одна принимала JSON начинающийся как с массива [...], так и с объекта {...}, а другая только с объекта. В итоге пришлось делать на стыке костыль с упаковкой массива в объект перед передачей, а затем распаковывать его на принимающей стороне. Но там были не микросервисы, а два монолита которые решили интегрировать.
valery1707
30.05.2019 14:25Рекомендация 1: Очень желательно, чтобы общего кода у микросервисов не было вообще.
Это возможно только в одном случае: каждый микросервис пишется на языке, не совместимом с другими или использует совершенно разные источники данных.
Если есть два сервиса на одном языке использующих один источник данных: у вас в любом случае будет общий код для доступа к этому источнику данных.
Банальный пример: Java-сервисы работающие с очередями. У них будет как миниум библиотекаJMS
, а то и какая-нибудь обёртка над ней, например в видеSpring JMS
.r47717 Автор
30.05.2019 16:03имеется ввиду не использование стандартных библиотек, а собственный написанный Вами для этого проекта кастомный общий код
eefadeev
30.05.2019 15:21Я правильно понимаю что если у меня есть 10 микросервисов, то в каждом из них должна быть своя реализация сортировок, сетевых стеков, утилит работы со строками и т.п.?
EvgeniiR
30.05.2019 15:29Я правильно понимаю что если у меня есть 10 микросервисов, то в каждом из них должна быть своя реализация сортировок, сетевых стеков, утилит работы со строками и т.п.?
Общие библиотеки / пакеты жеeefadeev
30.05.2019 17:46А, то есть статью кратко можно переписать так: «Разрабатывая микросервисы делайте нормальную архитектуру»?
EvgeniiR
30.05.2019 15:26Очень важные советы на самом деле. Это не так сложно, но чаще всего из-за отсутствия нормальных границ и возникают проблемы с SOA. печально что в топе популярности и в принципе на виде оказываются не полезные статьи, а «дерзкие» и «хайповые».
Стоит упомянуть, что сам по себе «общий код» проблем не приносит, проблемы приносит «общая логика», ключевое различие в том что общий код мог получиться в разных модулях случайно, и меняется независимо, а общую логику нужно менять во всех местах сразу, что усложняет систему.
jakobz
30.05.2019 20:59Откуда вы все это тащите то? Куча гигантов, во главе с гуглом, сидит в монорепе, куча статей в которых люди пишут как все равно пришли к монорепе. Но при этом можно взять, написать статью что микоосервисы вообще нельзя с монорепой. Как так-то? У гугла не микросервисы? Ну ок, тогда я не хочу микросервисы.
r47717 Автор
31.05.2019 11:141. Монорепо — отдельный сложный вопрос, я про это не писал вообще, и монорепо — это не есть «общий код» в моём понимании (если это «правильный» монорепо, в котором сервисы лежат все отдельно);
2. По поводу монорепо и гугл — очень популярный вопрос, стоит начать отсюда: habr.com/ru/post/450230
3. «тащим» из опыта, обобщения статей и книг (ну например, www.amazon.co.uk/Building-Microservices-Sam-Newman/dp/1491950358), это всё не я выдумал, я лишь суммировал, как я это понимаю, после того, как не раз проверил на опыте.
chainick
30.05.2019 22:47Рекомендация 1: Очень желательно, чтобы общего кода у микросервисов не было вообще.
Это несколько странная рекомендация. Самый банальный пример — единая библиотека для RPC и трейсинга запросов.
dididididi
Рекомендация 7. Не юзайте микросервисы, если у Вас общий код.