Перевод и адаптация Java Microservices: A Practical Guide. Предыдущие части гайда:
Давайте рассмотрим свойственные Java проблемы микросервисов начиная с абстрактных вещей и заканчивая конкретными библиотеками.
Как сделать микросервис Java устойчивым?
Напомним, что при создании микросервисов вы по сути меняете вызовы методов JVM на синхронные вызовы HTTP или асинхронный обмен сообщениями. В то время как выполнение вызова метода в основном гарантировано (за исключением неожиданного завершения работы 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-вызова.
- ОК: звонок прошел, счет успешно создан.
- ЗАДЕРЖКА: звонок прошел, но потребовалось слишком много времени для этого.
- ОШИБКА. Вызов не состоялся, возможно, вы отправили несовместимый запрос или система не работала.
Messaging Resilience Patterns (Отказоустойчивые паттерны обмена сообщениями)
Давайте подробнее рассмотрим асинхронную коммуникацию. Наша программа BillingService теперь может выглядеть примерно так, при условии, что мы используем Spring и RabbitMQ для обмена сообщениями. Чтобы создать счет, мы теперь отправляем сообщение нашему брокеру сообщений RabbitMQ, где есть несколько работников, ожидающих новых сообщений. Эти работники создают счета в формате PDF и отправляют их соответствующим пользователям.
@Service
class BillingService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
// преобразует счет, например, в json и использует его как тело сообщения
rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
// ...
}
}
Теперь потенциальные ошибки выглядят немного иначе, так как вы больше не получаете немедленных ответов OK или ERROR, как это было с синхронным HTTP-соединением. Вместо этого у нас может быть три потенциальных варианта неправильного развития событий, которые могут вызвать следующие вопросы:
- Было ли мое сообщение доставлено и использовано работником? Или это потеряно? (Пользователь не получает счет-фактуру).
- Мое сообщение было доставлено только один раз? Или доставлено более одного раза и обрабатывается только один раз? (Пользователь получит несколько счетов).
- Конфигурация: От «Использовал ли я правильные ключи маршрутизации/имена для обмена» до «Правильно ли настроен и поддерживается мой брокер сообщений или переполнены его очереди?» (Пользователь не получает счет-фактуру).
- Если вы используете реализации JMS, например, ActiveMQ, вы можете обменять скорость на гарантии двухфазных (XA) коммитов (two-phase (XA) commits).
- Если вы используете RabbitMQ, для начала прочитайте это руководство, а затем хорошенько обдумайте подтверждения, отказоустойчивость и надежность сообщений в целом.
- Возможно, кто-то хорошо разбирается в конфигурировании серверов Active или RabbitMQ, особенно в сочетании с кластеризацией и Docker (кто-нибудь?;))
Какой фрейморк будет лучшим решением для микросервисов Java?
С одной стороны, можно установить очень популярный вариант, такой как Spring Boot. Он позволяет очень легко создавать файлы .jar, поставляется со встроенным веб-сервером, таким как Tomcat или Jetty, и который можно запустить быстро и где угодно. Идеально подходит для создания приложений микросервиса. Не так давно появилась пара специализированных микросервисных фреймворков Kubernetes или GraalVM, частично вдохновлённых реактивным программированием. Вот ещё несколько интересных претендентов: Quarkus, Micronaut, Vert.x, Helidon. В конце концов, вам придется выбирать самостоятельно, но мы можем дать вам парочку рекомендаций, возможно, не вполне стандартных: За исключением Spring Boot, все платформы микросервисов обычно позиционируются как невероятно быстрые, с почти мгновенным запуском, малым объемом используемой памяти, возможностью масштабирования до бесконечности. В маркетинговых материалах обычно фигурируют впечатляющие графики, представляющие платформу в выгодном свете рядом с “бегемотом” Spring Boot или друг с другом. Это по идее щадит нервы разработчиков, поддерживающих легаси-проекты, которые порой загружаются по несколько минут. Или разработчикам, работающим в облаке, которые хотят запустить\остановить столько микроконтейнеров, сколько им сейчас нужно в течение 50 мс. Проблема, однако, заключается в том, что такое (искусственное) время старта «голого железа» и время повторного развертывания едва ли влияют на общий успех проекта. По крайней мере влияют гораздо меньше, чем сильная инфраструктура фреймворка, сильная документация, сообщество и сильные навыки разработчика. Так что лучше смотреть на это так: Если до сих пор:- Вы позволяете своим ORM работать в режиме безудержной генерации сотен запросов для простых рабочих процессов.
- Вам нужны бесконечные гигабайты для запуска вашего монолита умеренной сложности.
- У вас так много кода и сложность столь высока (сейчас мы говорим не о потенциально медленных стартерах, таких как Hibernate), что вашему приложению нужно несколько минут для загрузки.
Какие библиотеки лучше всего подходят для синхронных вызовов Java REST?
На низкоуровневой технической стороне вы, вероятно, получите одну из следующих клиентских библиотек HTTP: Собственный HttpClient Java (начиная с Java 11), HttpClient Apache или OkHttp. Обратите внимание, что здесь я говорю «вероятно», потому что есть и другие варианты, начиная со старых добрых клиентов JAX-RS до современных клиентов WebSocket. В любом случае, существует тенденция к генерации HTTP-клиента, с отходом от самостоятельной возни с HTTP-вызовами. Для этого вам нужно взглянуть на проект OpenFeign и его документацию в качестве отправной точки для дальнейшего чтения.Какие брокеры являются лучшими для асинхронного обмена сообщениями Java?
Скорее всего, вы столкнётесь с популярными ActiveMQ (Classic или Artemis), RabbitMQ или Kafka.- ActiveMQ и RabbitMQ являются традиционными, полноценными брокерами сообщений. Они предполагают взаимодействие “умного брокера” и “глупеньких пользователей”.
- Исторически ActiveMQ имел преимущество простого встраивания (для тестирования), которое можно смягчить с помощью настроек RabbitMQ/Docker/TestContainer.
- Kafka нельзя назвать традиционным “умным” брокером. Наоборот, это относительно «глупое» хранилище сообщений (файл журнала), для обработки которого нужны умные потребители.
Какие библиотеки я могу использовать для тестирования микросервисов?
Это зависит от вашего стека. Если у вас развёрнута экосистема Spring, будет разумно использовать специальные инструменты этого фреймворка. Если JavaEE — что-то вроде Arquillian. Возможно, стоит взглянуть на 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], который захардкожен в файле properties. Однако вы можете сделать всё более динамичным:- Используйте облачный сервер конфигурации, с которого все микросервисы извлекают свои конфигурации вместо развертывания файлов application.properties на своих микросервисах.
- Поскольку экземпляры ваших служб могут динамически менять свое местоположение, стоит присмотреться к службам, которые знают, где живут ваши службы, какие у них IP и как их маршрутизировать.
- Теперь, когда все динамично, появляются новые проблемы, такие как автоматическое избрание лидера: кто является мастером, который работает над определенными задачами, чтобы например, не обработать их дважды? Кто заменяет лидера, когда он терпит неудачу? По какому принципу происходит замена?
Как организовать авторизацию и аутентификацию с помощью микросервисов Java?
Эта тема также достойна отдельного рассказа. Снова таки, варианты варьируются от захардкоженной базовой аутентификации HTTPS с самописными фреймфорками безопасности до запуска установки Oauth2 с собственным сервером авторизации.Как убедиться, что все мои окружения выглядят одинаково?
То, что верно для развертываний без микросервиса, также верно и для развертываний с ним. Попробуете комбинацию Docker/Testcontainers, а также Scripting/Ansible.Не вопрос: кратко о YAML
Давайте ненадолго отойдём от библиотек и связанных с ними вопросов и вкратце рассмотрим Yaml. Этот формат файла, используется де-факто в качестве формата для «записи конфигурации в виде кода». Используют его и простые инструменты, наподобие Ansible и гиганты вроде Kubernetes. Чтобы испытать боль от отступов в YAML, попробуйте написать простой Ansible-файл и посмотрите, сколько вам придётся редактировать файл прежде, чем он заработает как нужно. И это невзирая на поддержку формата всеми крупными IDE! После этого возвращайтесь, чтобы дочитать это руководство.
Yaml:
- is:
- so
- great
А как насчет распределенных транзакций? Тестирование производительности? Другие темы?
Может, когда-нибудь, в следующих редакциях руководства. А пока — всё. Оставайтесь с нами!Концептуальные проблемы микросервисов
Помимо специфических проблем микросервисов в Java, есть и другие проблемы, скажем, те, которые появляются в любом микросервисном проекте. Они касаются по большей мере организации, команды и управления.Несоответствие Frontend и Backend
Несоответствие Frontend и Backend — весьма распространенная проблема многих микросервисных проектов. Что она означает? Лишь то, что в старых добрых монолитах, у разработчиков веб-интерфейса был один конкретный источник для получения данных. В микросервисных проектах у разработчиков веб-интерфейса неожиданно появляются n источников для получения данных. Представьте, что вы создаете какой-то проект микросервисов IoT (интернет вещей) на Java. Скажем, заведуете геодезическими машинами, промышленными печами по всей Европе. И эти печи регулярно отправляют вам обновления с указанием их температуры и тому подобными данными. Рано или поздно вы, возможно, захотите найти печи в пользовательском интерфейсе администратора, возможно, с помощью микросервисов «поиска печи». В зависимости от того, насколько строго ваши бэкэнд-коллеги применяют предметно-ориентированное проектирование или законы микросервисов, микросервис “найти печь” может возвращать только идентификаторы печей, а не другие данные, такие как тип, модель или местоположение. Для этого фронтенд-разработчикам нужно будет выполнить один или n дополнительных вызовов (в зависимости от реализации пейджинга) в микросервисе «получить данные о печи» с идентификаторами, которые они получили от первого микросервиса. И хотя это всего лишь простой пример, пускай и взятый из реального (!) проекта, даже он демонстрирует следующую проблему: супермаркеты стали чрезвычайно популярны. А всё потому, что с ними вам не нужно идти в 10 разных мест, чтобы купить овощи, лимонад, замороженную пиццу и туалетную бумагу. Вместо этого вы идете в одно место.Это проще и быстрее. То же самое касается разработчиков интерфейсов и микросервисов.Ожидания руководства
У менеджмента складывается ошибочное впечатление, что теперь нужно нанимать бесконечное количество разработчиков во (всеобъемлющий) проект, поскольку разработчики теперь могут работать совершенно независимо друг от друга, каждый на своем микросервисе. В самом конце требуется лишь небольшая работа по интеграции (незадолго до запуска). На самом деле такой подход крайне проблематичен. В следующих параграфах мы постараемся пояснить, почему.“Меньшие кусочки” не равно “лучшие кусочки”
Будет большой ошибкой считать, что разделённый на 20 частей код обязательно будет качественнее одного цельного куска. Даже если взять качество с сугубо технической точки зрения: наши отдельные службы по-прежнему могут выполнять 400 запросов Hibernate для выбора пользователя из базы данных, проходясь по слоям не поддерживаемого кода. В очередной раз возвращаемся к цитате Саймона Брауна: если не удастся построить монолиты должным образом, будет сложно создать надлежащие микросервисы. Зачастую об отказоустойчивости в микросервисных проектах крайне несвоевременно. Настолько, что порой страшно смотреть, как микросервисы работают в настоящих проектах. Причина этого кроется в том, что Java-разработчики не всегда готовы изучать отказоустойчивость, сети и другие смежные темы на должном уровне. Сами “кусочки” — меньше, а вот “технических частей” — больше. Представьте, что вашей микросервисной команде предлагается написать технический микросервис для входа в систему базы данных, примерно такой:
@Controller
class LoginController {
// ...
@PostMapping("/login")
public boolean login(String username, String password) {
User user = userDao.findByUserName(username);
if (user == null) {
// обработка варианта с несуществующим пользователем
return false;
}
if (!user.getPassword().equals(hashed(password))) {
// обработка неверного пароля
return false;
}
// 'Ю-ху, залогинились!';
// установите cookies, делайте, что угодно
return true;
}
}
Теперь ваша команда может решить (и, возможно, даже убедить людей бизнеса), дескать, это всё слишком просто и скучно, лучше вместо службы входа в систему написать действительно полезный микросервис UserStateChanged (изменение состояния пользователя) без каких-либо реальных и ощутимых бизнес-требований.
А поскольку к Java в настоящее время некоторые люди относятся как к динозавру, напишем наш микросервис UserStateChanged на модном Erlang. И давайте попробуем где-нибудь использовать красно-черные деревья, потому что Стив Йегге написал, что вы должны знать их изнутри, чтобы подать заявку в Google.
С точки зрения интеграции, обслуживания и общего проекта это так же плохо, как написание слоев спагетти-кода внутри одного монолита. Искусственный и заурядный пример? Так и есть. Тем не менее, подобное может быть и в реальности.
Меньше кусочки — меньше понимания
Затем естественным образом всплывает вопрос о понимании системы в целом, её процессов и рабочих потоков, но при этом вы, как разработчик, несете ответственность только за работу на своём изолированном микросервисе [95: login-101: updateUserProfile]. Он гармонирует с предыдущим параграфом, но в зависимости от вашей организации, уровня доверия и коммуникации это может привести к большому количеству недоумения, пожиманий плечами, обвинениям в случае случайной поломки в микросервисной цепочке. И нет того, кто бы принял на себя полную ответственность за случившееся. И дело вовсе не в недобросовестности. На самом деле очень трудно соединить разные детальки и понять их место в общей картине проекта.Коммуникации и обслуживание
Уровень коммуникации и обслуживания сильно зависит от размера компании. Тем не менее, общая зависимость очевидна: чем больше, тем проблематичнее.- Кто работает на микросервисе № 47?
- Они только что развернули новую несовместимую версию микросервиса? Где это было задокументировано?
- С кем мне нужно поговорить чтобы запросить новую функции?
- Кто будет поддерживать тот микросервис на Erlang, после того, как единственный кто знал этот язык покинул компанию?
- Все наши микросервисные команды работают не только на разных языках программирования, но и в разных часовых поясах! Как мы всё это правильно скоординируем?
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ