Pull to refresh

Микросервисы на Java: практическое руководство

Reading time29 min
Views88K
Original author: Marco Behler

Вы можете использовать это руководство, чтобы понять что такое Java микросервисы, как вы будете их разрабатывать и создавать. А также получить обзор библиотек для разработки Java микросервисов.


Примечание: Статья ~ 7000 слов, вероятно, не стоит читать ее на мобильном устройстве. Добавьте ее в закладки и вернитесь позже.


Содержание



Основы Java микросервисов


Чтобы получить реальное представление о микросервисах Java, имеет смысл начать с самых основ: печально известного монолита на Java, что это такое и каковы его преимущества или недостатки.


Что такое монолит Java?


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


Для этого коде на Java требуется класс контроллера, который, упрощенно, выглядит примерно так, как показано ниже.


@Controller
class BankController {

    @PostMapping("/users/register")
    public void register(RegistrationForm form) {
        validate(form);
        riskCheck(form);
        openBankAccount(form);
        // etc..
    }
}

Вы хотите:


  1. Выполнить валидацию регистрационной формы.
  2. Проверить риск по адресу пользователя, чтобы решить, хотите ли вы открыть ему банковский счет или нет.
  3. Открыть банковский счет

Ваш класс BankController будет упакован вместе со всем остальным исходным кодом в файл bank.jar или bank.war для развертывания: старый добрый монолит, содержащий весь код, необходимый для работы вашего банка. (По грубой оценке изначально ваш файл .jar / .war будет иметь размер в диапазоне 1-100 МБ).


На вашем сервере вы просто запускаете файл .jar — это все, что вам нужно для развертывания приложений Java.



В чем проблема с Java монолитами?


По своей сути, в монолите Java нет ничего плохого. Просто опыт проекта показал, что если у вас:


  1. Есть много разных программистов / команд / консультантов ...
  2. Работа над тем же монолитом выполняется под высоким давлением и непонятными требованиями…
  3. Работа — на пару лет ...

В результате ваш маленький файл bank.jar превращается в гигантский кодовый гигабайт, который все боятся развертывать.


Как уменьшить размер монолита Java?


Это естественно приводит к вопросу о том, как уменьшить монолит. На данный момент ваш bank.jar работает в одной JVM, один процесс на одном сервере. Ни больше ни меньше.


Теперь вы можете придумать идею: «Служба проверки рисков используется другими отделами в моей компании, и она на самом деле не имеет ничего общего с моим доменом Mono (lithic) Bank, поэтому мы можем попробовать вырезать его из монолита и развернуть как его отдельный продукт, или, если говорить технически, запустите его как свой собственный процесс Java.


Что такое микросервис Java?


В практическом плане это означает, что вместо вызова метода riskCheck() внутри вашего BankController вы переместите этот метод / bean-компонент со всеми его вспомогательными классами в свой собственный проект Maven / Gradle, поместите его под контроль системы управления исходным кодом и разверните его независимо от вашего. банковского монолита.


Весь этот процесс извлечения не делает ваш новый модуль RiskCheck микросервисом как таковым, и это потому, что определение микросервисов открыто для интерпретации (что приводит к изрядному количеству обсуждений в командах и компаниях).


  1. Это микро, если в нем только 5-7 классов?
  2. 100 или 1000 классов все еще микро?
  3. Это как-то связано с количеством классов?

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


  1. Назовите все отдельно развертываемые сервисы микросервисами — независимо от размера или границ домена.
  2. Сосредоточьтесь на важной теме межсервисного общения, потому что вашим микросервисам нужны способы общения друг с другом.

Итак, подведем итог: сначала у вас был один процесс JVM, один банковский монолит. Теперь у вас есть процесс JVM банковского монолита и микросервис RiskCheck, который работает в своем собственном процессе JVM. И ваш монолит теперь должен вызвать этот микросервис для проверки рисков.


Как это сделать?


Как общаться между Java микросервисами?


По существу, у вас есть два варианта: синхронное взаимодействие или асинхронное взаимодействие.


Синхронное взаимодействие с помощью HTTP/REST сервисов


Синхронное взаимодействие микросервисов обычно осуществляется через HTTP и REST-подобные сервисы, которые возвращают XML или JSON — хотя это ни в коем случае не является обязательным (посмотрите, например, на Google Protocol Buffers).


Используйте REST-коммуникацию, когда вам нужен немедленный ответ, который мы используем в нашем случае, так как проверка риска обязательна перед открытием счета: нет проверки риска, нет счета.


Посмотрите ниже раздел Какие библиотеки лучше всего подходят для синхронных вызовов Java REST?


Асинхронное взаимодействие с помощью обмена сообщениями


Асинхронная микросервисная связь обычно осуществляется посредством обмена сообщениями с помощью реализации JMS и / или с помощью протокола, такого как AMQP. Обычно, на практике не следует недооценивать интеграцию по электронной почте / SMTP.


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


Посмотрите ниже раздел Какие инструменты лучше всего подходят для асинхронного обмена сообщениями Java?


Пример: вызов REST API в Java


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


@Controller
class BankController {

    @Autowired
    private HttpClient httpClient;

    @PostMapping("/users/register")
    public void register(RegistrationForm form) {
        validate(form);
        httpClient.send(riskRequest, responseHandler());
        setupAccount(form);
        // etc..
    }
}

Глядя на код, становится ясно, что теперь вы должны развернуть два Java (микро) сервиса: ваш банк и сервис RiskCheck. В итоге вы получите две JVM, два процесса. Графическое изображение этого будет выглядеть так:



Вот и все, что вам нужно для разработки проекта Java Microservices: создавать и развертывать небольшие фрагменты (файлы .jar или .war) вместо одного большого.


Но остается вопрос: как именно вы вырезаете или настраиваете эти микросервисы? Что это за мелкие фрагменты? Какой правильный размер?


Давайте посмотрим что в реальности.


Микросервисная архитектура на Java


На практике компании пытаются разработать или спроектировать микросервисные проекты различными способами. Это зависит от того, пытаетесь ли вы превратить существующий монолит в проект микросервисов или начинаете с нового проекта.


От монолита к микросервисам


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


Давайте рассмотрим немного теории.


Идея: разбить монолит на микросервисы


Унаследованные проекты выигрывают от микросервисного подхода. Главным образом по трем причинам:


  1. Их часто трудно поддерживать / изменять / расширять.
  2. Каждый, от разработчиков, менеджеров до менеджеров, хочет сделать вещи проще, хочет, чтобы вещи были проще.
  3. У вас есть относительно четкие границы домена, это означает: вы знаете, что должно делать ваше программное обеспечение.

Это означает, что вы можете взглянуть на свой монолит Java-банка и попытаться разбить его по границам домена — это вполне разумный подход.


  1. Можно сделать вывод, что должен существовать микросервис «Управление учетными записями», который обрабатывает пользовательские данные, такие как имена, адреса, номера телефонов.
  2. Или вышеупомянутый «Модуль риска», который проверяет уровни риска пользователя и который может использоваться многими другими проектами или даже отделами вашей компании.
  3. Или модуль выставления счетов, который отправляет счета в формате PDF или по почте.

Реальность: пусть кто-то другой сделает это


Хотя этот подход определенно выглядит хорошо на бумаге и UML-подобных диаграммах, у него есть свои недостатки. В основном, вам нужны очень сильные технические навыки, чтобы справиться с этим. Почему?


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


Большинство корпоративных проектов достигают стадии, когда разработчики боятся, скажем, обновить 7-летнюю версию Hibernate до более новой, которая является лишь обновлением библиотеки, но требуются значительные усилия, чтобы не сломать ничего.


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


Здесь уместна цитата из @simonbrown в Twitter:


Я буду повторять это… если люди не могут правильно строить монолиты, микросервисы не помогут.
I'll keep saying this… if people can't build monoliths properly, microservices won't help.
Simon Brown

Новый проект с микросервисой архитектурой


Ситуация выглядит немного иначе при разработке новых проектов на Java. Теперь эти три пункта, перечисленные выше выглядят немного иначе:


  1. Вы начинаете с чистого листа, так что нет старого багажа для сопровождения.
  2. Разработчики хотели бы, чтобы в будущем все было просто.
  3. Проблема: у вас гораздо более туманная картина границ доменов: вы не знаете, каким на самом деле должно быть ваше программное обеспечение (подсказка: agile :) )

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


Техническая микросервисная архитектура


Первый подход является наиболее очевидным для разработчиков, вопреки настоятельным рекомендациям против него. Hadi Hariri (https://twitter.com/hhariri) рекомендует использовать рефакторинг «Extract Microservice» в IntelliJ.



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


До микросервиса


@Service
class UserService {

    public void register(User user) {
        String email = user.getEmail();
        String username =  email.substring(0, email.indexOf("@"));
        // ...
    }
}

С использованием Java микросервиса подстроки


@Service
class UserService {

    @Autowired
    private HttpClient client;

    public void register(User user) {
        String email = user.getEmail();
        // now calling the substring microservice via http
        String username =  httpClient.send(substringRequest(email), responseHandler());
        // ...
    }
}

Таким образом, вы, по сути, включаете вызов метода Java в вызов HTTP, без очевидных причин для этого. Одна из причин, однако, заключается в следующем: отсутствие опыта и попытка форсировать подход на основе микросервисов Java.


Рекомендация: не делайте этого.


Микросервисная архитектура, ориентированная на workflow (рабочий процесс)


Следующим распространенным подходом является разработка ваших микросервисов Java на основе рабочего процесса.


Пример из реальной жизни: в Германии, когда вы обращаетесь к (государственному) врачу, он должен записать ваше назначение в своем программном обеспечении CRM для здравоохранения.


Чтобы получить оплату от страховой компании, он отправит ваши данные о лечении и всех других пациентов, которых он лечил, посреднику через XML.


Посредник рассмотрит этот XML-файл и (упрощенно):


  1. Проверит файл, что это правильный XML
  2. Проверит данные на правдоподобие: чтобы 1-летний ребенок не получал три чистки зубов в день от гинеколога?
  3. Дополнит XML некоторыми другими бюрократическими данными
  4. Перешлет XML в страховую компанию, чтобы инициировать платежи
  5. И смоделирует все ответы обратно к врачу, включая сообщение «об успехе» или «пожалуйста, отправьте эту запись еще раз — как только это будет иметь смысл»

Если вы сейчас попытаетесь смоделировать этот рабочий процесс с помощью микросервисов, у вас получится это как минимум.


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



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


  1. Чувствуете ли вы необходимость развертывания шести приложений для обработки одного XML-файла?
  2. Действительно ли эти микросервисы независимы друг от друга? Они могут быть развернуты независимо друг от друга? С разными версиями и схемами API?
  3. Что делает микросервис правдоподобия, если микросервис проверки не работает? Система еще работает?
  4. Разделяют ли эти микросервисы одну и ту же базу данных (им, безусловно, нужны некоторые общие данные в таблице базы данных), или вы собираетесь взять еще больший молот, предоставив им все свои собственные базы данных?
  5. И тысяча других вопросов об инфраструктуре / операциях.

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



Несмотря на то, что можно спорить о простоте этих диаграмм, теперь вам определенно нужно решить дополнительные операционные задачи.


Вы ...


  • Должны просто развернуть не одно приложение, но как минимум шесть.
  • Возможно, даже потребуется развернуть несколько баз данных, в зависимости от того, как далеко вы хотите пройти продвинуть свою микросервисную архитектуру.
  • Должны следить за тем, чтобы каждая система работала в режиме онлайн, работала нормально.
  • Необходимо убедиться, что ваши вызовы между микросервисами действительно устойчивы (см. ниже Как сделать микросервис Java устойчивым?).
  • И все остальное, что подразумевает эта настройка — от локальных настроек разработки до интеграционного тестирования.

Рекомендация:


если:


  • вы не Netflix (а вы не) ...
  • вы не обладаете суперсильными операционными навыками, позволяющую вам открыть свою среду разработки, которая вызывает Chaos Monkey, которая удаляет вашу производственную базу данных, легко восстанавливаемую через 5 секунд.
  • или вы не чувствуете, что @monzo пытаетесь попробовать 1500 микросервисов просто потому, что можете.

→ Не делай этого.


Прим. перев.:
Chaos Monkey — архитектурный принцип Netflix для поддержки автомасштабируемых stateless-микросервисов — любой инстанс может быть остановлен и автоматически заменен без какой-либо потери состояния. Chaos Monkey следит за тем, чтобы никто не нарушал этот принцип.

Однако с меньшей гиперболой.


Попытка моделировать микросервисы по доменным границам — очень разумный подход. Но граница домена (скажем, управление пользователями или выставление счетов) не означает, что нужно взять один рабочий процесс и разделить его на крошечные отдельные части (получить XML, проверить XML, переслать XML).


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


И убедитесь, что у вас есть исключительно сильные навыки DevOps в вашей команде / компании / подразделении для поддержки вашей новой инфраструктуры.


Полиглото или командно-ориентированная микросервисная архитектура


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


Таким образом, приведенная выше служба проверки XML может быть написана на Java, в то время как микросервис правдоподобия написан на языке Haskell (чтобы сделать его математически обоснованным), а микросервис пересылки страховки должен быть написан на языке Erlang (потому что он действительно должен масштабироваться :) ).


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


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


Что интересно: историческая стандартизация зашла слишком далеко. Разработчикам в больших компаниях из списка Fortune 500 иногда даже не позволяли использовать Spring, потому что это «не входило в план компании по технологиям». Но переход на полноценный полиглотный подход — это почти то же самое, просто другая сторона той же монеты.


Рекомендация: если вы собираетесь использовать полиглотный подход, попробуйте меньшее разнообразие в одной и той же экосистеме языка программирования. Пример: Kotlin и Java (на основе JVM со 100% совместимостью друг с другом), а не Haskell и Java.


Развертывание и тестирование Java микросервисов


Это помогает быстро оглянуться назад на основы, упомянутые в начале этой статьи. Любая серверная Java-программа, а следовательно, и любой микросервис — это просто файл .jar / .war.


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


Это важно понимать, особенно когда речь идет о таких темах, как Docker, Kubernetes или The Cloud. Почему? Давайте рассмотрим различные сценарии развертывания:


Пример минимального развертывания микросервиса Java


Продолжая пример с банком, мы получили файл monobank.jar (монолит) и наш недавно извлеченный riskengine.jar (первый микросервис).


Предположим также, что обоим приложениям, как и любому другому приложению в мире, нужен файл .properties, будь то только URL базы данных и учетные данные.


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


-r-r------ 1 ubuntu ubuntu     2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 monobank-384.jar

ubuntu@somemachine:/var/www/www.monobank.com/java$ java -jar monobank-384.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...

-r-r------ 1 ubuntu ubuntu     2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 risk-engine-1.jar

ubuntu@someothermachine:/var/www/risk.monobank.com/java$ java -jar risk-engine-1.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...

Это оставляет открытым вопрос: как вы получаете файлы .properties и .jar на сервер?


К сожалению, есть множество соблазнительных ответов на этот вопрос.


Как использовать инструменты сборки, SSH и Ansible для развертывания микросервисов Java


Скучный, но совершенно прекрасный ответ на развертывание Java-микросервисов — это то, как администраторы разворачивали любую серверную Java-программу в компаниях за последние 20 лет. С использованием набора инструментов:


  • Ваш любимый инструмент для сборки (Maven, Gradle)
  • Старый добрый SSH / SCP для копирования ваших .jars на серверы
  • Сценарии Bash для управления сценариями развертывания и серверами
  • Или даже лучше: некоторые скрипты Ansible.

Если вы не зациклены на создании «дышащего» облака на основе серверов с автоматической балансировкой нагрузки, chaos monkeys, управляющих вашими машинами, или горячего и неясного ощущения, что выборы ведущего в ZooKeeper работают, то эта установка уведет вас достаточно далеко.


Олдскулно, скучно, но работает.


Как использовать Docker для развертывания микросервисов Java


Вернемся к заманчивым выборам. Пару лет назад на сцену вышел Docker или тема контейнеризации.


Если у вас нет опыта работы с ним, это то, что нужно для конечных пользователей или разработчиков:


  • Контейнер (упрощенно) похож на старую добрую виртуальную машину, но более легкий. Взгляните на этот ответ Stackoverflow, чтобы понять, что означает легкий в этом контексте.
  • Контейнер гарантирует вам, что он переносим, он работает где угодно. Это звучит знакомо?


Интересно, что с переносимостью и обратной совместимостью JVM это не кажется значительным преимуществом. Вы можете просто скачать JVM.zip на любом сервере Raspberry Pi (или даже на мобильном телефоне), распаковать его и запустить любой файл .jar.


Это выглядит немного иначе для языков, таких как PHP или Python, где несовместимость версий или настройки развертывания исторически были более сложными.


Или, если ваше Java-приложение зависит от множества других установленных служб (с правильными номерами версий): например от базы данных, такой как Postgres, или хранилища значений ключей, таких как Redis.


Итак, основное преимущество Docker для микросервисов Java, а точнее для приложений Java, заключается в:


  • Настройке унифицированных сред тестирования или интеграции с помощью таких инструментов, как Testcontainers.
  • Делая сложные дистрибутивы "проще" в установке. Возьмите программное обеспечение для форума Discourse. Вы можете установить его с помощью одного Docker образа, который содержит все, что вам нужно: от программного обеспечения Discourse, написанного на Ruby, до базы данных Postgres, и Redis.

Если ваши развертываемые файлы похожи или вы хотите запустить небольшую базу данных Oracle на своем компьютере разработки, попробуйте Docker.


Итак, чтобы подвести итог, вместо простого просмотра файла .jar, вы теперь:


  • Упакуете ваш файл JAR в образ Docker
  • Поместите этот образ докера в личный реестр докеров
  • Разместите и запустите этот образ на вашей целевой платформе
  • Или скопируйте образ Docker прямо в вашу рабочую систему и запустите его

Как использовать Docker Swarm или Kubernetes для развертывания микросервисов Java


Допустим, вы хотите попробовать Docker. Каждый раз, когда вы развертываете свой микросервис Java, вы теперь создаете образ Docker, который объединяет ваш файл .jar. У вас есть пара таких микросервисов Java, и вы хотите развернуть эти службы на нескольких машинах: кластере.


Теперь возникает вопрос: как вы управляете этим кластером, что означает запуск ваших контейнеров Docker, проверки работоспособности, развертывание обновлений, масштабирование (brrrr)?


Два возможных ответа на этот вопрос — Docker Swarm и Kubernetes.


Подробное описание обоих вариантов невозможно в рамках данного руководства, но на самом деле важно следующее: оба варианта в конце концов полагаются на то, что вы пишете файлы YAML (см. ниже Не вопрос: Рассказы об отступах в Yaml) для управления вашим кластером. Сделайте быстрый поиск в Твиттере, если хотите знать, какие чувства это вызывает на практике.


Таким образом, процесс развертывания для ваших микросервисов Java теперь выглядит примерно так:


  • Настройка и управление Docker Swarm / Kubernetes
  • Все из докера выше
  • Напишите и выполняйте YAML, пока ваши глаза не будут кровоточить все не заработает.

Как протестировать микросервисы Java


Предположим, вы решили внедрить микросервисы в производство, но как вы тестируете интеграцию n-микросервисов во время разработки? Чтобы увидеть, работает ли полный рабочий процесс, а не только отдельные части?


На практике вы найдете три разных способа:


  1. Немного поработав (и если вы используете фреймворки, такие как Spring Boot), вы можете обернуть все ваши микросервисы в один класс запуска и загрузить все микросервисы с одним классом Wrapper.java — в зависимости от того, достаточно ли у вас памяти на вашем компьютере. запустить все ваши микросервисы.
  2. Вы можете попробовать скопировать настройки Docker Swarm или Kubernetes локально.
  3. Просто больше не проводите интеграционные тесты локально. Вместо этого имейте специальную среду DEV / TEST. Это то, что на самом деле делает немало команд, поддающихся боли местных микросервисных установок.

Кроме того, в дополнение к вашим микросервисам Java вам, вероятно, также понадобится работающий брокер сообщений (например, ActiveMQ или RabbitMQ) или, возможно, сервер электронной почты или любой другой компонент обмена сообщениями, с которым ваши микросервисы Java должны взаимодействовать друг с другом.


Это приводит к значительному занижению сложности на стороне DevOps. Взгляните на Microservice Testing Libraries, чтобы смягчить эту боль.


В любом случае, эта сложность приводит нас к общим проблемам микросервиса:


Общие вопросы о Java микросервиах


Давайте рассмотрим проблемы микросервисов, характерные для Java, от более абстрактных вещей, таких как устойчивость к конкретным библиотекам.


Как сделать микросервис Java устойчивым?


Напомним, что при создании микросервисов вы по сути заменяете вызовы методов JVM
на синхронные вызовы или асинхронный обмен сообщениями.


В то время как выполнение вызова метода в основном гарантировано (за исключением того, что JVM неожиданно завершает работу), сетевой вызов по умолчанию ненадежен.


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


Чтобы увидеть, какое это имеет значение, давайте взглянем на примерный пример BillingService.


Шаблоны устойчивости HTTP / REST


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


Сейчас мы сделаем этот вызов синхронно, через HTTP. (Было бы более разумно вызывать эту службу асинхронно, потому что генерация PDF не обязательно должна быть мгновенной с точки зрения пользователя. Но мы хотим повторно использовать этот самый пример в следующем разделе и увидеть различия.)


@Service
class BillingService {

    @Autowired
    private HttpClient client;

     public void bill(User user, Plan plan) {
        Invoice invoice = createInvoice(user, plan);
        httpClient.send(invoiceRequest(user.getEmail(), invoice), responseHandler());
        // ...
    }
}

Подумайте, какие возможные результаты может иметь этот HTTP-вызов. Обобщая, вы получите три возможных результата:


  1. ОК: звонок прошел, и счет был успешно создан.
  2. DELAYED: звонок прошел, но потребовалось необычайно много времени для этого.
  3. ERROR: Вызов не состоялся, возможно, вы отправили несовместимый запрос или система не работала.

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


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


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


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


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


Шаблоны устойчивости обмена сообщениями


Давайте подробнее рассмотрим асинхронное общение. Наш код BillingService теперь может выглядеть примерно так, при условии, что мы используем Spring и RabbitMQ для обмена сообщениями.


Чтобы создать счет, мы теперь отправляем сообщение нашему брокеру сообщений RabbitMQ, у которого есть несколько работников, ожидающих новых сообщений. Эти работники создают счета в формате PDF и отправляют их соответствующим пользователям.


@Service
class BillingService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

     public void bill(User user, Plan plan) {
        Invoice invoice = createInvoice(user, plan);
        // converts the invoice to,for example, json and uses it as the message's body
        rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
        // ...
    }
}

Теперь потенциальные ошибки выглядят немного иначе, так как вы больше не получаете немедленных ответов OK или ERROR, как это было с синхронным HTTP-соединением. Вместо этого у вас будут примерно три случая ошибки:


  1. Было ли мое сообщение доставлено и использовано работником? Или оно потеряно? (Пользователь не получает счет-фактуру).
  2. Мое сообщение было доставлено только один раз? Или доставлено более одного раза и обрабатывается только один раз? (Пользователь получит несколько счетов).
  3. Конфигурация: От «Использовал ли я правильные ключи маршрутизации / имена для обмена» до «Правильно ли настроен и поддерживается мой брокер сообщений или переполнены его очереди?» (Пользователь не получает счет-фактуру).

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


  • Если вы используете реализации JMS, такие как ActiveMQ, вы можете пожертвовать скоростью в обмен на гарантии двухфазных (XA) коммитов.
  • Если вы используете RabbitMQ, вам по крайней мере, необходимо прочитать и понять это руководство, а затем тщательно продумать подтверждения, подтверждения и надежность сообщений в целом.
  • Если есть кто-то с опытом в установке, например, ActiveMQ или RabbitMQ сервера и их правильной настройки, особенно если они используются в сочетании с кластеризацией и Docker.

Какой Java-микросервисный фреймворк лучший?


Во-первых, вы сделали очень популярный выбор, такой как Spring Boot, который позволяет очень легко создавать файлы .jar, которые поставляются со встроенным веб-сервером, таким как Tomcat или Jetty, и который вы можете сразу запустить в любом месте. Он идеально подходит для создания микросервисных приложений.


Недавно, частично вдохновленные параллельными разработками, такими как реактивное программирование, Kubernetes или GraalVM, возникла пара специализированных микросервисных сред.


Можно назвать несколько: Quarkus, Micronaut, Vert.x, Helidon.


В конце концов, вам придется сделать свой собственный выбор, но эта статья может дать некоторые, возможно, нетрадиционные рекомендации:


За исключением Spring Boot, все фреймворки для микросервисов обычно позиционируют себя как невероятно быстрые с очень быстрым временем запуска, малый объемом памяти, возможностью масштабирования до бесконечности, с впечатляющими графиками, сравнивающими себя с бегемотом Spring Boot или друг с другом.


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


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


Вы должны смотреть на это так.


Если до сих пор:


  • Вы позволяли своим ORM работать под большой нагрузкой и генерировать сотни запросов для простых рабочих процессов.
  • Вам нужны огромные гигабайты для работы умеренно сложного монолита.
  • Вы добавили так много кода и сложности, что (не считая потенциально медленных стартеров, таких как Hibernate), вашему приложению теперь нужно несколько минут для загрузки.

Тогда добавление дополнительных требований для микросервисов (устойчивость, сеть, обмен сообщениями, DevOps, инфраструктура) будет иметь гораздо большее влияние на ваш проект, чем загрузка пустого hello world. А для горячих повторных развертываний во время разработки вы, наконец, вы можете использовать такие решения, как JRebel или DCEVM.


Вернемся к цитате Саймона Брауна (Simon Brown): если люди не могут создавать (быстрые и эффективные) монолиты, им будет трудно создавать (быстрые и эффективные) микросервисы — независимо от фреймворка.


Итак, выбирайте свой фреймвок с умом.


Какие библиотеки лучше всего подходят для синхронных REST вызовов Java?


Поговорим о более практических аспектах вызова HTTP REST API. На низком техническом уровне вы, вероятно, придете к одной из следующих клиентских библиотек HTTP:


Собственный Http клиент Java (начиная с Java 11), Http клиент Apache или OkHttp.


Обратите внимание, что здесь я говорю «вероятно», потому что есть и другие способы, от старых добрых клиентов JAX-RS до современных клиентов WebSocket.


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


Какие брокеры являются лучшими для асинхронного обмена сообщениями Java?


Начиная разработку с асинхронного обмена сообщениями, вы, скорее всего, придете к ActiveMQ (Classic или Artemis), RabbitMQ или Kafka. Опять же, это просто популярный выбор.


Вот несколько основных моментов:


  • ActiveMQ и RabbitMQ являются традиционными, полноценными брокерами сообщений. Этот выбор означает довольно умного брокера и тупых потребителей.
  • Исторически ActiveMQ имел преимущество в силу простого встраивания (для тестирования), его можно сгладить с помощью настроек RabbitMQ / Docker / TestContainer.
  • Kafka не является традиционным брокером. Наоборот, это относительно «тупое» хранилище сообщений (файл журнала), для обработки которого нужны более умные потребители.

Чтобы лучше понять, когда использовать RabbitMQ (или традиционных брокеров сообщений в целом) или Kafka, в качестве отправной точки взгляните на соответствующий пост Pivotal в качестве отправной точки.


Тем не менее, в общем, старайтесь игнорировать любые искусственные причины производительности при выборе вашего брокера. Было время, когда команды и интернет-сообщества много спорили о том, насколько быстрым был RabbitMQ и насколько медленным был ActiveMQ.


Теперь у вас те же аргументы в отношении того, что RabbitMQ медленен, с только 20-30K сообщений в секунду. Для Kafka сообщается о 100K сообщений в секунду. С одной стороны, такие сравнения можно игнорировать учитывая, что вы на самом деле сравниваете яблоки и апельсины.


Но даже более того: оба значения пропускной способности могут быть на нижней или средней стороне для Alibaba Group, но вы и автор, никогда не видели проекты такого размера (миллионы сообщений в минуту) в реальном мире. Они определенно существуют, но эти цифры — не то о чем нужно беспокоиться для остальных 99% обычных бизнес-проектов Java.


Так что не обращайте внимания на шумиху и выбирайте мудро.


Какие библиотеки я могу использовать для микросервисного тестирования?


В зависимости от вашего стека вы можете использовать инструменты Spring (экосистема Spring) или что-то вроде Arquillian (экосистема JavaEE).


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


Для макетирования целых HTTP-серверов, посмотрите на Wiremock. Для тестирования асинхронного обмена сообщениями попробуйте встраивание (ActiveMQ) или докеринг (RabbitMQ), а затем написать тесты с помощью Awaitility DSL.


Кроме этого, применяются все ваши обычные «подозреваемые», такие как Junit, TestNG для AssertJ и Mockito.


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


Как включить ведение журнала для всех моих микросервисов Java?


Ведение журнала в микросервисах — интересная и довольно сложная тема. Вместо того, чтобы иметь один файл журнала, с которым вы можете использовать less или grep, теперь у вас есть n файлов — журнала, которые вы хотели бы просматривать вместе.


Отличной отправной точкой для всей экосистемы ведения журнала является эта статья. Обязательно прочитайте ее, особенно раздел «Централизованное ведение журнала» с точки зрения микросервисов.


На практике вы найдете различные подходы:


  • Системный администратор пишет некоторые сценарии, которые собирают и объединяют файлы журналов с различных серверов в один файл журнала и помещают их на FTP-серверы для загрузки.
  • Запускаются комбинации cat / grep / unig / sort в параллельных сеансах SSH. Вы можете сказать своему менеджеру: это то, что Amazon AWS делает внутренне.
  • Используется такой инструмент, как Graylog или ELK Stack (Elasticsearch, Logstash, Kibana)

Как мои микросервисы находят друг друга?


До сих пор мы предполагали, что все наши микросервисы знают друг друга, знают соответствующий IPS. Это статическая настройка. Итак, наш банковский монолит [ip = 192.168.200.1] знает, что ему нужно поговорить с сервером риска [ip = 192.168.200.2], который жестко задан в файле свойств.


Однако вы можете сделать вещи более динамичными:


  • Вы больше не размещаете файлы application.properties на своих микросервисах, вместо этого используйте облачный сервер конфигурации, с которого все микросервисы извлекают свои конфигурации.
  • Поскольку экземпляры ваших служб могут динамически менять свое местоположение (представьте, что экземпляры Amazon EC2 получают динамические IP-адреса, а вы эластично-автоматически масштабируете этот «ад» в облаке), вы вскоре, возможно, посмотрите на реестр служб, который знает, где «живут» ваши службы, с каким IP и может соответственно маршрутизировать вызовы.
  • И теперь, когда все динамично, у вас появляются новые проблемы, такие как автоматический выбор лидера: кто является мастером, который работает над определенными задачами, чтобы, например, не обработать их дважды? Кто заменяет лидера, когда он терпит неудачу? С кем?

В общих чертах, это то, что называется микросервисной оркестровкой и еще одна огромная тема.


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


Просто спросите любого, кто когда-либо запускал установку ZooKeeper.


Как сделать авторизацию и аутентификацию с помощью микросервисов Java?


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


Как мне убедиться, что все мои окружения выглядят одинаково?


То, что верно для развертываний без микросервиса, также верно и для развертываний микросервиса. Вы можете попробовать комбинацию Docker / Testcontainers, а также сценариев / Ansible.


Попробуйте и сохраняйте его проще.


Не вопрос: Рассказы об отступах в Yaml


Сделаем резкий разворот от конкретных библиотечных вопросов, давайте кратко рассмотрим Yaml. Это формат файла, который используется в качестве фактического формата файла для «записи конфигурации в виде кода». От более простых инструментов, таких как Ansible, до могучих Kubernetes.


Чтобы испытать «мучения» от отступов в YAML, попробуйте написать простые файлы Ansible и посмотрите, как часто вам нужно повторно редактировать файл, чтобы заставить отступ работать правильно, несмотря на различные уровни поддержки IDE. А затем вернитесь, чтобы закончить это руководство.


Yaml:
  - is:
    - so
    - great

А как насчет распределенных транзакций? Тестирование производительности? Другие темы?


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


Концептуальные проблемы микросервисов


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


Несоответствие фронтенд и бэкеенд


То, что происходит во многих микросервисных проектах, я бы назвал несоответствием внешнего интерфейса и интерфейса микросервиса. Что это обозначает?


Что в старых добрых монолитах, у разработчиков веб-интерфейса был один конкретный источник для получения данных. В микросервисных проектах у разработчиков веб-интерфейса неожиданно появляются n источников для получения данных.


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


Рано или поздно вы, возможно, захотите выполнять поиск печи в пользовательском интерфейсе администратора, возможно, с помощью микросервисов «поиска печи». В зависимости от того, насколько строго ваши бэкэнд-коллеги могут интерпретировать domain driven design или законы о микросервисах, может случиться так, что микросервис «поиск печи» возвращает только ваши идентификаторы, но не другие данные, такие как тип, модель или местоположение.


Для этого разработчикам фронтенда может потребоваться выполнить один или n дополнительных вызовов (в зависимости от вашей реализации пейджинга) в микросервисе «получить данные о печи» с идентификаторами, которые они получили от первого микросервиса.



И хотя это всего лишь простой (но взятый из реального проекта (!)) Пример, он демонстрирует следующую проблему:


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


Это проще и быстрее. То же самое касается разработчиков интерфейсов и микросервисов.


Ожидания руководства


Эта проблема вызывает нежелательные побочные эффекты со стороны отдельных разработчиков, программистских журналов или облачных компаний, предлагающих микросервисы:


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


Давайте рассмотрим в следующих параграфах, почему этот взгляд является серьёзной проблемой.


Меньшие части не означают лучшие части


Одна довольно очевидная проблема состоит в том, что 20 меньших частей (как в микросервисах) на самом деле не означают 20 лучших частей. Чисто с точки зрения технического качества это может означать, что ваши отдельные службы по-прежнему выполняют 400 запросов Hibernate для выбора пользователя из базы данных по слоям и слоям не поддерживаемого кода.


Возвращаясь к цитате Саймона Брауна: если людям не удастся построить монолиты должным образом, им будет сложно создать надлежащие микросервисы.


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


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


Меньшие части приводят к большему количеству технических частей


Кроме того, существует печальная тенденция к тому, чтобы пользовательские истории становились все более и более техническими (и, следовательно, глупыми), более микро и отвлеченными от пользователя, от которого они получены.


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


@Controller
class LoginController {

    // ...

    @PostMapping("/login")
    public boolean login(String username, String password) {
        User user = userDao.findByUserName(username);
        if (user == null) {
            // handle non existing user case
            return false;
        }
        if (!user.getPassword().equals(hashed(password))) {
            // handle wrong password case
            return false;
        }
        // 'Yay, Logged in!';
        // set some cookies, do whatever you want
        return true;
    }
}

Теперь ваша команда может решить (и, возможно, даже убедить бизнесменов): это слишком просто и скучно, вместо службы входа в систему давайте напишем действительно «умный» микросервис UserStateChanged — без каких-либо реальных и ощутимых бизнес-требований.


А поскольку Java в настоящее время не в моде, давайте напишем микросервис UserStateChanged в Erlang. И давайте попробуем где-нибудь использовать красно-черные деревья, потому что Steve Yegge написал, что вы должны знать их наизнанку, чтобы подать резюме в Google.


С точки зрения интеграции, обслуживания и общего проекта это так же плохо, как написание слоев спагетти-кода внутри одного монолита.


Придуманный и заурядный пример? Да.


К сожалению, также не редкость в реальной жизни.


Меньшие кусочки ведут к меньшему пониманию


Затем возникает тема понимания всей системы, ее процессов и рабочих процессов, если вы, как разработчик, несете ответственность только за работу на изолированном микросервисе [95: login-101: updateUserProfile].


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


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


Коммуникации и обслуживание


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


Кто работает на микросервисе № 47?


Они только что развернули новую несовместимую версию микросервиса? Где это было задокументировано?


С кем мне нужно поговорить для запроса новой функции?


Кто будет поддерживать микросервис на Erlang после того, как Макс покинул компанию?


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


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


Заключение


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


Баланс микросервисов


Полное использование Java-микросервисов — это одно положение маятника. Другое положение — что-то вроде сотен старых добрых модулей Maven в Монолите. Вы должны найти правильный баланс.


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


Микросервисы создают тысячи дополнительных сложностей


Имейте в виду, что чем больше у вас микросервисов и чем меньше у вас действительно сильных талантов DevOps (нет, выполнение нескольких сценариев Ansible или развертывание на Heroku не учитывается), тем больше проблем у вас возникнет позже в работе.


Прочитать раздел Общие вопросы о Java микросервисах этого руководства уже утомительно. Затем подумайте о реализации решений для всех этих инфраструктурных задач. Вы очевидно поймете, что все это больше не связано с бизнес-программированием (за что вам платят), а скорее с фиксацией большего количества технологий на еще большем количестве технологий.


Siva Prasad Reddy (Шива Прасад Редди) отлично подвел итог в своем блоге:


Я не могу объяснить, как это ужасно, когда команда тратит 70% времени на борьбу с этой современной инфраструктурой и 30% времени на реальную бизнес-логику.

Стоит ли создавать микросервисы Java?


Чтобы ответить на этот вопрос, я хотел бы закончить эту статью очень дерзко, похоже на тизер Google интервью. Если вы знаете ответ на этот вопрос по своему опыту, даже если он, по-видимому, не имеет ничего общего с микросервисами, вы можете быть готовы к микросервисному подходу.


Сценарий


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


И давайте также предположим, что ваш Java-монолит может обрабатывать рабочие процессы, такие как регистрация пользователей, и вы создаете не сотни запросов к базе данных на рабочий процесс, а только разумное количество (<10).


Вопрос


Сколько соединений с базой данных должен открыть ваш монолит Java (пул соединений) на вашем сервере баз данных?


Почему? Как вы думаете, сколько одновременно активных пользователей может (приблизительно) масштабировать ваш монолит?


Ответ


Оставьте свой ответ на эти вопросы в разделе комментариев. Я с нетерпением жду всех ответов.


Теперь делайте свой выбор сами


Если вы все еще здесь со мной: Спасибо за чтение!

Tags:
Hubs:
+7
Comments7

Articles

Change theme settings