Проблемы общего кода в микросервисах +12


Всем привет!

Недавно на конференции 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».

Спасибо за внимание и удачных вам микросервисов.




К сожалению, не доступен сервер mySQL