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

    Всем привет!

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

    Спасибо за внимание и удачных вам микросервисов.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

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

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

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

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


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

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

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

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

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

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

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

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

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

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

                    Only users with full accounts can post comments. Log in, please.