Всем привет!

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


  1. dididididi
    30.05.2019 11:17

    Рекомендация 7. Не юзайте микросервисы, если у Вас общий код.


  1. igormich88
    30.05.2019 12:08

    Кстати по поводу хелперов общих и самописных. На работе столкнулся с тем что в двух проектах использовались разные библиотеки для работы с JSON. Одна принимала JSON начинающийся как с массива [...], так и с объекта {...}, а другая только с объекта. В итоге пришлось делать на стыке костыль с упаковкой массива в объект перед передачей, а затем распаковывать его на принимающей стороне. Но там были не микросервисы, а два монолита которые решили интегрировать.


  1. valery1707
    30.05.2019 14:25

    Рекомендация 1: Очень желательно, чтобы общего кода у микросервисов не было вообще.

    Это возможно только в одном случае: каждый микросервис пишется на языке, не совместимом с другими или использует совершенно разные источники данных.


    Если есть два сервиса на одном языке использующих один источник данных: у вас в любом случае будет общий код для доступа к этому источнику данных.
    Банальный пример: Java-сервисы работающие с очередями. У них будет как миниум библиотека JMS, а то и какая-нибудь обёртка над ней, например в виде Spring JMS.


    1. r47717 Автор
      30.05.2019 16:03

      имеется ввиду не использование стандартных библиотек, а собственный написанный Вами для этого проекта кастомный общий код


      1. mayorovp
        30.05.2019 16:09

        Чем кастомный код отличается от стандартных библиотек кроме авторства?


        1. r47717 Автор
          30.05.2019 17:19

          тем, что он (более-менее активно) меняется в процессе развития проекта


  1. eefadeev
    30.05.2019 15:21

    Я правильно понимаю что если у меня есть 10 микросервисов, то в каждом из них должна быть своя реализация сортировок, сетевых стеков, утилит работы со строками и т.п.?


    1. EvgeniiR
      30.05.2019 15:29

      Я правильно понимаю что если у меня есть 10 микросервисов, то в каждом из них должна быть своя реализация сортировок, сетевых стеков, утилит работы со строками и т.п.?

      Общие библиотеки / пакеты же


      1. eefadeev
        30.05.2019 17:46

        А, то есть статью кратко можно переписать так: «Разрабатывая микросервисы делайте нормальную архитектуру»?


  1. EvgeniiR
    30.05.2019 15:26

    Очень важные советы на самом деле. Это не так сложно, но чаще всего из-за отсутствия нормальных границ и возникают проблемы с SOA. печально что в топе популярности и в принципе на виде оказываются не полезные статьи, а «дерзкие» и «хайповые».

    Стоит упомянуть, что сам по себе «общий код» проблем не приносит, проблемы приносит «общая логика», ключевое различие в том что общий код мог получиться в разных модулях случайно, и меняется независимо, а общую логику нужно менять во всех местах сразу, что усложняет систему.


  1. jakobz
    30.05.2019 20:59

    Откуда вы все это тащите то? Куча гигантов, во главе с гуглом, сидит в монорепе, куча статей в которых люди пишут как все равно пришли к монорепе. Но при этом можно взять, написать статью что микоосервисы вообще нельзя с монорепой. Как так-то? У гугла не микросервисы? Ну ок, тогда я не хочу микросервисы.


    1. EvgeniiR
      30.05.2019 21:57

      Подавляющему большинству проектов монорепа не нужна, «Вы — не google»


      1. jakobz
        30.05.2019 22:00
        +1

        Гораздо большему количеству проектов не нужны микросервисы.


    1. r47717 Автор
      31.05.2019 11:14

      1. Монорепо — отдельный сложный вопрос, я про это не писал вообще, и монорепо — это не есть «общий код» в моём понимании (если это «правильный» монорепо, в котором сервисы лежат все отдельно);
      2. По поводу монорепо и гугл — очень популярный вопрос, стоит начать отсюда: habr.com/ru/post/450230
      3. «тащим» из опыта, обобщения статей и книг (ну например, www.amazon.co.uk/Building-Microservices-Sam-Newman/dp/1491950358), это всё не я выдумал, я лишь суммировал, как я это понимаю, после того, как не раз проверил на опыте.


  1. chainick
    30.05.2019 22:47

    Рекомендация 1: Очень желательно, чтобы общего кода у микросервисов не было вообще.

    Это несколько странная рекомендация. Самый банальный пример — единая библиотека для RPC и трейсинга запросов.


    1. r47717 Автор
      31.05.2019 10:46

      да, поэтому это скорее идеальный случай, читайте Рекомендацию 2