А вы уверены, что пользуетесь микросервисами?

Автор оригинала: Michał Matłoka
  • Перевод
Привет, Хабр!

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

Термин “микросервисы” впервые появился около 2011/2012 года. С тех пор без микросервисов в IT-среде почти никуда. Правда, по прошествии стольких лет, успели ли мы как следует понять, что такое микросервисы?

Сначала давайте рассмотрим несколько определений:

Микросервисы – это подход к разработке программного обеспечения (…), согласно которому приложение структурируется как набор слабо связанных сервисов. В микросервисной архитектуре сервисы отличаются высокой детализацией, а используемые протоколы легковесны. Польза декомпозиции приложения на отдельные более мелкие сервисы заключается в улучшении модульности. В таком виде приложение легче осмыслить, разрабатывать, тестировать, а само приложение становится более устойчивым к эрозии архитектуры. При таком подходе к архитектуре разработка распараллеливается, а небольшие автономные команды получают возможность независимо разрабатывать, развертывать и масштабировать те сервисы, за которые отвечают. Также при таком подходе можно применять к архитектуре каждого сервиса непрерывный рефакторинг. Также микросервисные архитектуры рассчитаны на непрерывную доставку и непрерывное развертывание.
en.wikipedia.org/wiki/Microservices

Вот еще пассаж из Мартина Фаулера:

Термин “Микросервисная архитектура” в последние несколько лет прочно укоренился для описания особого способа проектирования таких приложений, каждое из которых представляет собой набор независимо развертываемых сервисов. Хотя, точная дефиниция такого архитектурного стиля отсутствует, есть ряд общих характеристик, касающихся организации приложения вокруг его бизнес-возможностей, автоматизированного развертывания, сбора сведений на терминалах и децентрализованного управления языками и данными.
martinfowler.com/articles/microservices.html

Вероятно, вы заметили, что основное сходство между двумя этими формулировками – независимость сервисов и вариантов. Теперь давайте попробуем ответить на несколько вопросов и попытаемся выяснить, в самом ли деле реализуемая вами система имеет микросервисную основу!

Позволяет ли ваша система независимо перезапускать сервисы?

Либо у вас необходимо перезапускать сервисы в определенном порядке? Такое поведение может диктоваться конкретными кластерными системами, где узел соединяется с затравочным узлом (seed node), например, в кластере Akka, если обращаться с ним неправильно. Другие случаи могут быть связаны с долгоиграющими соединениями или сокетами, которые могут быть открыты только на одной из сторон. Помните, что коммуникация должна оставаться простой и легковесной. Еще лучше, если она будет полностью асинхронной и основанной на сообщениях. Все ограничения, вызывающие зависимость сервисов друг от друга, в долгосрочной перспективе могут обернуться сложностями при поддержке. Проблемы с одним из сервисов могут спровоцировать неполадки и во всех остальных сервисах, которые от него зависят, что когда-нибудь может вылиться в серьезный глобальный отказ.

Можете ли вы независимо развертывать сервисы?

Известны случаи, когда релиз и развертывание всех сервисов в системе должно происходить одновременно. Такой процесс занимает несколько дней. Разумеется, в таком случае ограничиваются возможности внедрения непрерывного развертывания, возникают задержки, а оперативность реагирования ухудшается. На то, чтобы разобраться с неотложными багами, уходит слишком много времени, просто из-за издержек, сопряженных с процессом развертывания. Когда разные команды заняты поддержкой различных сервисов, любая зависимость при развертывании может помешать другим командам развернуть свои сервисы, из-за чего работу придется приостановить, и эффективность ее снизится.

Может ли модификация повлечь необходимость выполнить синхронизированные модификации других сервисов?

Можно вообразить множество причин, приводящих к такому поведению. В первую очередь это изменения API. Бывает так, что достаточно добавить новое поле к запросу API, чтобы нарушилась совместимость между сервисами. Однако, существуют обходные маневры, помогающие справляться с такими ситуациями:

  • Возможно, вы хотите поддерживать обязательную обратную совместимость для всех изменений, вносимых в API; то есть, всякий раз при внесении принципиального изменения добавляется новая версия API. Это не означает, что вам потребуется вечно поддерживать множество версий API; они понадобятся вам лишь на период до завершения миграции. Можно просто поговорить с другими командами и убедиться, что все завершили миграцию, либо включить метрики, позволяющие судить, используется ли до сих пор конкретный API.
  • Также можно рассмотреть и такой вариант: развернуть сразу несколько версий вашего приложения, позволив другим сервисам переключиться на новейший релиз, а затем отключить старый.
  • Еще попробуйте использовать форматы, поддерживающие эволюцию схем, например, Avro, где при добавлении нового поля совместимость может не нарушаться. Когда значение для некоторого поля отсутствует, может использоваться заданное по умолчанию.

Другое основание для синхронизированного развертывания – использование разделяемого кода разными сервисами. В зависимости от того, как построен процесс релиза в разделяемой части кода, любое изменение может потребовать обновления во всех сервисах.

Провоцирует ли изменение базы данных изменение во множестве сервисов?

Да, коммуникация через базу данных – хорошо известный антипаттерн. В такой ситуации необходимо обновить схему и одновременно изменить код сразу нескольких микросервисов. Лучше сделать так, чтобы за управление схемой отвечал всего один сервис. В таком случае становится гораздо проще поддерживать базу данных (то есть, выполнять ее обновления) и осуществлять релизы. Если вы уже оказались в ситуации с разделяемым использованием базы данных, то попытайтесь ограничить количество операций записи, так, чтобы другой сервис работал только на потребление.

Можете ли вы обновить зависимость или версию Java в единственном сервисе?

По идее, это всегда должно быть возможно, верно? Но на самом деле нет. Известно множество примеров «разделяемых» библиотек или просто проектов, в которых содержатся определения всех зависимостей, используемых в системе. Цель парадигмы микросервисов – добиться, чтобы каждый из них был независимой сущностью, причем, разные микросервисы даже не должны быть написаны в одном и том же технологическом стеке. Начиная новый проект, вы должны быть в состоянии сами решать, какая технология лучше всего для него подходит. При любом связывании возникают новые проблемы. В какой-то момент вам может потребоваться обновить Scala или JDK, но вы, конечно, не хотите делать это сразу во всей системе. В системе, где правильно соблюден уровень детализации можно выполнять обновления отдельно взятого сервиса, развертывать его, тестировать и наблюдать за ним несколько дней или недель, а затем, если все правильно, то можно выполнить обновления и в остальной части системы. Однако, когда существует common или другие сильные механизмы унификации, такие возможности сильно ограничены.

Вынуждает ли вас выбранный вами фреймворк или библиотека останавливаться на том же выборе и в других сервисах?

Некоторые библиотеки довольно инвазивны. Когда вы решаете интегрировать их с внешними системами, может внезапно оказаться, что выбор у вас не так велик, когда вы собираетесь реализовать новый сервис в ином технологическом стеке. Издержки, связанные с его интеграцией в том же духе, что и остальные сервисы, могут оказаться просто неподъемны.

Допустим, например, у вас есть несколько сервисов, написанных на NodeJS, активно использующих JSON:API. Далее вы хотите реализовать новый сервис на Scala и представляется, что подходящей клиентской библиотеки нет (несколько лет назад у нас был похожий случай, но теперь ситуация в этой области немного улучшилась).

Вызывает ли отказ единственного сервиса отказ всей системы?

Допустим, коммуникация в вашей системе полностью синхронна. Один сервис выполняет запрос к другому и ожидает отклика от него. Если первый сервис откажет, то второй сервис вообще не сможет сработать. Если у вас в системе складывается именно такая ситуация, попробуйте внедрить в ней асинхронную коммуникацию на основе сообщений. Возможно, смоделировать все процессы будет немного сложнее, но такой подход позволяет сгладить эффекты отказов.

В случаях, когда это невозможно, можно попробовать временно ограничить набор возможностей приложения. Допустим, на странице интернет-магазина выводится информация о товаре и отзывы по нему, причем, информация о товаре и обзоры поступают из двух разных источников. Если микросервис обзоров не работает, вы все равно можете отобразить страницу с товаром, просто проинформировав пользователя, что система отзывов временно недоступна.

Есть ли в вашей системе какая-либо разделяемая составляющая?

У вас есть разделяемый код? Конфигурации? Хоть что-нибудь? Да, это вопрос нетривиальный. Поборники чистоты микросервисов считают, что лучше всего вообще не допускать в системе разделяемых сущностей! Код лучше даже просто копировать, чем разделять. На практике иногда считается приемлемым иметь несколько разделяемых библиотек, но со строгими ограничениями. Возможно, это будут библиотеки, содержащие файлы proto для буферов протоколов или просто обычные объекты POJO. Лучше избегайте зависимостей в таких библиотеках. У нас был случай, когда простая клиентская библиотека, вызывавшая конфликты зависимостей в большой системе. В какой-то момент стало казаться, что самые старые элементы приложения используют старую HTTP-библиотеку, обновить которую невозможно. К счастью, таких ситуаций можно избежать, пользуясь переименованием зависимостей в процессе сборки (dependency shading), но будьте очень осторожны.

Вред от чрезмерной связанности сервисов намного больше вреда от проблем, вызываемых повторяемостью кода.

Сэм Ньюмен, "Создание микросервисов"

Выводы

Внедрить совершенную систему на основе микросервисов – непростая задача. Люди склонны «срезать углы», внося в систему тесные связи, возникающие из-за разделяемого кода, и в какой-то момент такая система на практике превращается в распределенный монолит. Часто такое случается, если микросервисы начинают использоваться уже на старте проекта, когда предметная область еще недостаточно хорошо изучена, а у вас нет солидного готового бэкграунда в DevOps. Лучше начать с правильно структурированного монолита, где полностью известны все зоны ответственности, и их легко отделить друг от друга. Однако, с самого начала нужно быть уверенным, что ваша система спроектирована чисто, и в ней соблюдается аккуратная организация пакетов, что впоследствии обеспечит вам легкую миграцию кода в новые сервисы (например, единственный пакет «почти верхнего уровня» может быть основой для нового микросервиса). ArchUnit может помочь с анализом зависимостей, возникающих между пакетами.

(…) не следует сразу начинать работу над новым проектом с внедрения микросервисов, даже если вы уверены, что ваше приложение получится достаточно крупным, чтобы микросервисы себя оправдали.
martinfowler.com/bliki/MonolithFirst.html

Помните, микросервисы должны упрощать как разработку, так и поддержку! Все, что вам нужно – слабосвязанные сервисы, каждый из которых можно развертывать независимо от других.
  • +14
  • 7,3k
  • 5
Издательский дом «Питер»
172,57
Компания
Поделиться публикацией

Комментарии 5

    0
    Отличная статья. Жаль что проблема версионнированности протоколов пропущена.
      +2

      Микросервисы способны превратить любую бизнес проблему в проблему распределённых транзакций.


      Особенно такие радикальные, как в этой статье.

        +1

        Как по мне, то слишком идеализированы требования к МСА, причём включают в себя ещё и требования к процессам разработки в динамике. По пунктам:


        Позволяет ли ваша система независимо перезапускать сервисы?

        Пожалуй, это единственное с которым согласен на 100%. Если при рестарте сервиса нужно рестартовать и те, для которых он является зависимостью, то сложно это назвать МСА


        Можете ли вы независимо развертывать сервисы?

        Вот тут уже есть нюансы. По-моему, МСА или нет особо не влияет проходит развёртывание какого-то сервиса указанием его версии в каком-то конфиге в его личном репозитории или в каком-то глобальном мета или даже моно репозитории, где описан текущий состав и схема всей системы.


        Если релиз происходит, например, раз в 2 недели, то вполне может быть, что при релизе все сервисы развернутся заново, но это ещё не повод считать систему не МСА, если это произошло лишь потому, что за эти две недели были изменения во всех. Тут система развёртывания должна определять что нужно развёртывать заново, а что нет. Например по наличию коммитов с прошлого развёртывания или по изменившумуся номеру версии.


        Может ли модификация повлечь необходимость выполнить синхронизированные модификации других сервисов?

        Это уже больше не к архитектуре, а к процессам разработки. Обеспечение обратной совместимости отдельных модулей в процессе разработки это тактические решения на каждой задаче, главное чтобы архитектура в принципе это поддерживала, а необходимость определяется локально, стоит ли овчинка выделки. И сильно зависит от сложности коммуникаций, согласований с разработчиками клиентов твоего сервиса. Если ты сам можешь провести все изменения малой кровью, и зарелизить не только новую версию своего сервиса, но и всех его клиентов, то почему бы и нет.


        Можете ли вы обновить зависимость или версию Java в единственном сервисе?

        Неоднозначный вопрос. Это всегда можно сделать в принципе, если не стараться запретить это специально. Вопрос в том сколько времени займёт. Может оказаться, что дольше чем во всей системе одновременно и/или следующий релиз других сервисов будет вынужден тоже обновится. Это вопрос решаемый на уровне проектирования и напрямую к МСА отношения не имеет по-моему. Вполне могут быть пулы микросервисов, использующих одну версию зависимостей, хотя предусмотреть возможность относительно простого перевода из одного пула в другой полезно. Вот тут у нас используется Java 8 или какая там типа LTS, а тут 10. Минорные апдейты пулов проводятя одноврменно, плюс постепенно мигрируем сервисы из старого пула в новый.


        Вызывает ли отказ единственного сервиса отказ всей системы?

        Точно не показатель МСА. Есть такое бизнес-требование — надо его реализовывать. Нет — вполне могут быть даже несколько точек глобального отказа типа сервиса аутентификации, авторизации или API Gateway


        Есть ли в вашей системе какая-либо разделяемая составляющая?

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

          0
          С трудом представляю систему без таковой. Вернее с трудом представляю себе набор полностью независимых сервисов, которые представляют собой систему, а не кучу.


          Речь не про то что между сервисами не должно быть зависимостей, а про shared libraries. Например когда у вас один сервис вызывается из десятка других и все сервисы написаны на одном языке, то возникает желание один раз написать клиентскую библиотеку и использовать ее в 10 сервисах, вместо дублирования/генерирования клиента. Если сервисов много а разработчиков/команд мало — то кажется что это частая ситуация.

          www.microservices.com/talks/dont-build-a-distributed-monolith
          www.infoq.com/presentations/netflix-play-api
            0

            Пишем библиотеку и выпускаем разные её версии, каждый сервис обновляется по мере надобности.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое