Введение: Начальная проблема…
Поскольку мы все больше и больше склоняемся к микросервисным архитектурам, мы замечаем, что они, как правило, изначально ненадежны, и в зависимости от варианта использования даже небольшие сбои могут потенциально вызвать нетерпение пользователей или заставить их искать лучший сервис где-то в другом месте. Хотя микросервисы привносят в игру свои преимущества, они, как правило, во многом зависят друг от друга и взаимозависимы, поэтому необходимо решить проблему потенциального падения одного из них.
… и решение
Становится все более важным, чтобы мы создавали отказоустойчивые микросервисы, но нам также нужны инструменты, которые могут немного облегчить работу с ненадежностью, позволяя нам сосредоточиться на бизнес-логике, а не заканчивать стрессовыми, сложными операторами if-then-else. Именно здесь вступает в игру Отказоустойчивость малого размера , предоставляя стандартизированный набор общих шаблонов обработки сбоев с помощью аннотаций, которые могут сделать код читаемым и ремонтопригодным. Отказоустойчивость Small Rye – это реализация отказоустойчивости микропрофиля Eclipse. Первоначально он был основан на Hystrix, библиотеке Netflix для задержки и отказоустойчивости в распределенных системах , которая активно продвигала концепции отказоустойчивости и изоляции для микросервисов. Hystrix уже давно является очень популярной библиотекой в отрасли, но активность сообщества снижается, и недавно она остановилась, объявив, что прекратит обслуживание. Отказоустойчивость Small Rye – это простое в использовании расширение, которое предоставляет нам политики повторных попыток , тайм-ауты и автоматические выключатели , которые определяют, должны ли выполняться и когда. Он также включает резервные варианты, которые предлагают альтернативную реализацию, когда выполнение не завершается успешно.
Давайте взглянем на это
Что я использую
- Java 11
- Apache Maven 3.8.1
- ИДЕЯ IntelliJ
- macOS Catalina
Мы смоделируем небольшую бизнес-логику в компании по доставке/логистике, которая занимается управлением и отслеживанием перевозок. Это чрезмерно упрощенный пример серверной части, в то же время сохраняющий реалистичность. Причина, по которой я выбрал более сложную модель, а не просто базовую сущность для передачи, заключается в том, что я хотел бы, чтобы она отражала повседневный образ корпоративного проекта. Я уже говорил здесь о запуске приложения Quark u.s. Вы можете добавлять свои расширения везде, где вы решите создать свой проект, будь то непосредственно на quarkus.io , в вашей IDE или с помощью команды Maven, например:
mvn io.quarkus:quarkus-maven-plugin:1.13.4.Final:create \ -DprojectGroupId=org.tinyg \ -DprojectArtifactId=microprofile-fault-tolerance-quickstart \ -DclassName="org.tinyg.TransportRest" \ -Dpath="/transports" \ -Dextensions="resteasy,smallrye-fault-tolerance,resteasy-jackson"
В уже существующем проекте Quarks вы можете просто добавить расширение, выполнив следующее в вашем базовом каталоге:
./mvnw quarkus:add-extension -Dextensions="smallrye-fault-tolerance"
Полный проект можно найти здесь , в папке “начальная” будет чистая настройка, которую необходимо улучшить, а в папке “окончательная” мы увидим конечную версию с желаемыми улучшениями. Давайте быстро взглянем на классы. По соображениям удобства чтения я решил сделать эти разделы разборными, чтобы их можно было расширять по мере необходимости. Давайте запустим сервер разработки и посмотрим, работает ли он:
./mvnw compile quarkus:dev
Войти в полноэкранный режим
Выйти из полноэкранного режима
Модель состоит из 3 объектов: Транспорт, который содержит набор контейнеров, которые, в свою очередь, в зависимости от их типов, могут содержать один или несколько контейнерных грузов. Простой компонент CDI будет содержать объект хранилища данных. В реальной жизни это было бы обработкой запросов к базе данных, но поскольку это не является основной целью этого поста, мы пока сделаем это так. Наконец, класс Transport Rest – это то место, где мы будем предоставлять наши конечные точки, которые мы можем вызвать через Postman (или команду CURL, или браузер), чтобы получить информацию о перевозках. Предполагается, что один из методов REST неисправен, что вызывает проблемы при попытке получить список доступных транспортов.
Повторные попытки
Иногда один из наших микросервисов может вести себя не так, как ожидалось, и может повлиять на другие, если он станет неисправным. Но не все ситуации являются показательными, и сбоев можно легко избежать, просто позвонив в ту же службу еще раз. Чтобы предотвратить нарушение восходящего или нисходящего потока, мы можем использовать механизм повторной попытки, который восстановит операцию после сбоя, вызвав ее снова, пока она не достигнет своих критериев остановки. Чтобы использовать эту функцию, мы просто добавляем аннотацию @Retry к неисправному методу, и по умолчанию мы знакомимся с некоторыми параметрами конфигурации: Максимальное количество попыток , повторите попытку На , Прерывание , задержка . Давайте немного рассмотрим их. При первоначальной настройке иногда будет выдаваться исключение, когда мы пытаемся получить список транспортов. Мы можем аннотировать этот метод с помощью @Retry , и на основе того, что необходимо решить, мы также можем настроить его:
@Retry( retryOn = HTTPException.class, maxRetries = 5, abortOn = KnownProcessingException.class, delay = 500 )
Что мы здесь говорим, так это то, что мы хотим повторить попытку вызова этого метода в случае возникновения исключения HttpException. Это довольно общее исключение, но можно выбрать более конкретное исключение в зависимости от их варианта использования. Максимальное время повторной попытки этого вызова будет равно 5, задержка между вызовами составляет 500 миллисекунд, и ни при каких обстоятельствах это не должно повторяться при возникновении исключения KnownProcessingException. Последнее исключение является пользовательским, просто для демонстрации функциональности, но оно подчеркивает, что существуют также способы устранения известных преднамеренных сбоев (например, техническое обслуживание). Итак, теперь все будет по-другому, при попытке получить наш список транспортов консоль будет выглядеть по-другому, и хотя связь с другой службой не совсем гладкая, пользователь не будет знать об этом. И если что-то случится, что наша функциональность не сможет продолжаться, то это исключение будет обработано соответствующим образом, и возвращаемое значение будет пустым списком.
Тайм-ауты
Давайте представим, что у нас есть функциональность в приложении, которая может отображать, сколько CO2 производит транспортное средство из пункта А в пункт В, на основе текущей загрузки контейнеров и некоторых других региональных экологических факторов. Это определенно не является важной функцией, она в основном предназначена для информирования пользователей, а также для демонстрации приверженности будущим улучшениям. Таким образом, при отслеживании транспорта также будет отображаться обновленная панель выбросов. В случае, если система перегружена, а логика получения этих данных слишком затратна с точки зрения времени, мы можем просто обойтись без нее и визуализировать компоненты пользовательского интерфейса. Мы расширяем наше приложение с помощью новой конечной точки и класса обслуживания, который позаботится об этом:
Калькулятор выбросов Отдыхает Войдите в полноэкранный режим Выход из полноэкранного режима
Сервис Калькулятора выбросов Войдите в полноэкранный режим Выход из полноэкранного режима
Намеренная задержка была установлена где-то между 0 и 5 секундами (обычно мы не хотели бы иметь такое долгое время ожидания, это просто легче воспринимать как человека), а тайм-аут был настроен на 2,5 секунды. После запроса этой информации несколько раз мы видим в журналах, что у некоторых из них истекает время ожидания с помощью org.eclipse.microprofile.терпимость к ошибкам.исключения. Исключение TimeoutException
. Успешные из них принесут нам информацию: “Отчет по CO2 был рассчитан”..
Отступление
Сейчас самое подходящее время внести исправление для предыдущего org.eclipse.microprofile.отказоустойчивость. исключения. Исключение TimeoutException
. Давайте снабдим метод аннотацией @Fallback и предоставим другую функцию, которая может заменить его в случае сбоя. В нашем случае он может обеспечить более простой и быстрый расчет выбросов или может возвращать общее число, которое обычно находится где-то в пределах средних значений.
@GET @Path("/{trackingId}") @Timeout(2500) @Fallback(fallbackMethod = "genericInformationAboutCO2") public String calculateEmissions(@PathParam final String trackingId) { ... }
Имейте в виду, что запасной метод должен соответствовать исходному с точки зрения параметров, это будет выглядеть примерно так:
private String genericInformationAboutCO2(final String trackingId) { return "An average consumption of 4,2 kg / 100 km corresponds to 4,2 kg x 2666 g/kg = 112 g of CO2/km."; }
Запросив эти данные о выбросах еще несколько раз, мы можем увидеть, насколько хорошо запасной вариант справляется с ними:
Автоматические выключатели
Если какая-то часть нашей системы становится временно нестабильной, мы можем захотеть отключить доступ к ней на некоторое время, чтобы ограничить количество сбоев. Задача автоматического выключателя состоит в том, чтобы записывать успешные и неудачные вызовы метода, и когда количество сбоев достигает определенного порогового значения, он отключает вызовы в эту область. В классе транспортных услуг у нас будет:
@CircuitBreaker( requestVolumeThreshold = 4, failureRatio = 1 / 2, delay = 10000 ) public ListgetValidTrackingIds() { possibleFail(); return transports.values().stream().filter(transport -> LocalDate.now().compareTo(transport.getArrivalDate()) <= 0).map(Transport::getTrackingId).collect(Collectors.toList()); } private void possibleFail() { final Long invocationNumber = counter.getAndIncrement(); if (invocationNumber % 4 > 1) { // alternate 2 successful and 2 failing invocations throw new RuntimeException("Service failed."); } }
Проверив параметры конфигурации, мы увидим, что пороговое значение объема запроса
равно 4, что означает, что будут проверены последние 4 вызова. Коэффициент отказов равен 1/2 (значение по умолчанию), что указывает на то, что автоматический выключатель разомкнется при сбое 2 последовательных вызовов из последних 4 вызовов. Мы устанавливаем
delay на 10 секунд, именно столько времени автоматический выключатель будет оставаться разомкнутым. Десять секунд - это значение, выбранное таким образом, чтобы его можно было легко воспринять.
Мы можем видеть в журналах, что в этот 10-секундный интервал автоматический выключатель разомкнут и блокирует любое выполнение.
Когда службы взаимодействуют синхронно, существует вероятность того, что вызываемая служба может оказаться недоступной или с высокой задержкой до такой степени, что она станет непригодной для использования. Это может привести к тому, что драгоценные ресурсы, такие как потоки и время, будут расходоваться без всякой причины, иногда даже доходя до истощения. Сбой службы в приложении потенциально может привести к сбою других служб по всей системе, поэтому важно знать о возможных сбоях и быть готовым.
Добавление запасного варианта к вашим автоматическим выключателям
Как только мы внедрим автоматический выключатель, может быть желательно также предоставить альтернативу для простоя. Это может быть достигнуто путем предоставления резервного класса. Мы еще раз используем аннотацию @Fallback :
@CircuitBreaker( requestVolumeThreshold = 4, failureRatio = 1 / 2, delay = 10000 ) @Fallback(CircuitBreakerFallback.class) public ListgetValidTrackingIds() { ...
Затем мы реализуем интерфейс Callbackhandler в нашем резервном классе, где мы переопределяем метод “handle”. Имейте в виду, что “handle” должен иметь тот же возвращаемый тип, что и аннотированный метод.
import org.eclipse.microprofile.faulttolerance.ExecutionContext; import org.eclipse.microprofile.faulttolerance.FallbackHandler; import java.util.Collections; import java.util.List; public class CircuitBreakerFallback implements FallbackHandler> { @Override public List
handle(ExecutionContext executionContext) { return getValidTrackingIds(); } public List getValidTrackingIds() { return Collections.singletonList("Has reached fallback for circuit breaker. You can provide and alternative " + "here."); } }
Теперь мы можем видеть, что, хотя автоматический выключатель разомкнут, альтернативная реализация может взять на себя управление и обработать запрос.
Выводы
И вот оно, мы рассмотрели некоторые из самых полезных и простых в использовании способов поддержания наших приложений на плаву, даже когда они постоянно сталкиваются с проблемами.
Ресурсы
Оригинал: “https://dev.to/tinyg210/adding-fault-tolerance-to-your-quarkus-microservice-with-smallrye-5bm8”