Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124

Circuit Breaker – Zapobieganie Awariom Kaskadowym w Systemach

W aplikacjach mikroserwisowych (ale i nie tylko), często występuje sytuacja, że jeden system zależy od drugiego. Gdy mamy całą siatkę takich zależności, awaria jednego serwisu, może spowodować wiele awarii w nadrzędnych systemach, może nawet zaistnieć kaskadowość awarii.

Dla przykładu: co jeśli dany serwis komunikuje się z bankiem, bramką płatniczą lub OAUTH2 providerem i dany serwis nie będzie odpowiadał? Taka sytuacja wymaga obsłużenia w sposób jaki narzuca nam logika biznesowa naszej aplikacji. Może to być np. :

 

  • wyświetlenie odpowiedniego komunikatu użytkownikowi
  • pobranie danych z cache’u/bazy
  • wykonanie akcji alternatywnej

Czym jest i jak działa Circuit Breaker?

Circuit Breaker jest to wzorzec architektoniczny dedykowany dla aplikacji mikroserwisowych, ale jego zastosowania są na tyle uniwersalne, że możemy z niego korzystać w monolitach i modularnych monolitach. Działanie tego wzorca opiera się głownie na 3 trybach:

  • CLOSED –  stan, gdy oba systemy komunikują się bezawaryjnie 
  • OPEN – podczas stanu CLOSED, komunikacja między systemami zostaje zablokowana do pewnego momentu, dając tym samym czas systemowi na restart/powrót do prawidłowego działania. Wywoływana jest tutaj także funkcja fallback.
  • HALF-OPEN – w tym stanie, Circuit Breaker, będzie przepuszczał pewną liczbą zapytań do systemu docelowego, sprawdzając tym samym czy poprawna komunikacja jest już możliwa. 

W Spring Boocie jedną z popularniejszych implementacji tego wzorca dostarcza Resilience4j. Pokażę teraz implementacje Circuit Breaker’a w aplikacji z architekturą rozproszonego monolitu.  Jako moduł, z którym będziemy się komunikować posłuży nam API, które stworzyłem na potrzeby większego projektu, które zwraca poziom cieczy w danym naczyniu. Link do projektu na githubie:
https://github.com/newmon13/esp32-sensors-api-client

Zaczniemy od dodania odpowiedniej zależności w sekcji <dependecies> w pom.xml.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
    <version>3.2.1</version>
</dependency>

Teraz tworzymy klasę, która będzie się komunikować z zewnętrznym systemem,. Schemat jest taki jak w artykule o WireMock. Będziemy mieć klienta http i serwis wykorzystujący tego klienta. Poniżej kod mojego klienta:

@Component
public class Esp32ApiClient {
@Value("${esp32.url}")
private String ESP32_URL;

private final RestClient restClient;

public Esp32ApiClient() {
this.restClient = RestClient.create();
}

public Map<String, Integer> getWaterLevel() {
return restClient.get()
.uri(ESP32_URL + "/api/data")
.retrieve()
.body(new ParameterizedTypeReference<>() {});
}
}

Teraz stworzymy serwis, korzystający z tego klienta, który będzie posiadał metodę chronioną przez Circuit Breakera. Dodatkowo dodatmy także TimeLimitera, który ograniczy czas oczekiwania na odpowiedź z API. 

Wszystko skupia się tutaj na dodaniu odpowiednich adnotacji. Przyjmuje ona 2 parametry. Pierwszy określa nazwę, konkretnego CB/TL (będziemy potrzebować tej nazwy w konfiguracji, którą dodamy za chwilę), a drugi wskazuje na metodę, która zostanie wywołana 

@Service
public class WaterLevelSensorService {
private final static String WATER_SENSOR_CB_NAME = "waterSensorCb";
private final static String WATER_SENSOR_TL_NAME = "waterSensorTl";

private final Esp32ApiClient esp32ApiClient;

public WaterLevelSensorService(Esp32ApiClient esp32ApiClient) {
this.esp32ApiClient = esp32ApiClient;
}

@CircuitBreaker(name = WATER_SENSOR_CB_NAME, fallbackMethod = "getWaterLevelFallback")
@TimeLimiter(name = WATER_SENSOR_TL_NAME, fallbackMethod = "getWaterLevelFallback")
public CompletableFuture<WaterSensorDataDto> getWaterLevel() {
return CompletableFuture.supplyAsync(() -> {
Map<String, Integer> waterLevel = esp32ApiClient.getWaterLevel();
return new WaterSensorDataDto(waterLevel.get("water_level"));
});
}

private CompletableFuture<WaterSensorDataDto> getWaterLevelFallback(Throwable throwable) {
throw new SensorApiException("Lost connection to water sensor API");
}
}

Sygnatura metody fallback musi odpowiadać sygnaturze metody chronionej. Aby uniknąć zwracania w odpowiedzi tego samego DTO z sztucznie niepoprawną wartością, w funkcji fallback rzucam wyjątek, który następnie zostanie przechwycony przez RestControllerAdvice. Tam zostanie obsłużona ta sytuacja wyjątkowa z odpowiednim error DTO.

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(SensorApiException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public SensorErrorResponse handleSensorApiException(SensorApiException err) {
return new SensorErrorResponse(err.getMessage());
}
}
public class SensorApiException extends RuntimeException {
public SensorApiException(String message) {
super(message);
}
}
public record SensorErrorResponse(String message) {
}

Teraz w application.properties dodamy odpowiednią konfigurację. Omówię za co odpowiada każdy wpis.

  • slidingWindowSize – circuit breaker, , będzie śledził rezultaty n poprzednich zapytań. Na ich podstawie dalej ustalimy próg zapytań zakończonych niepowodzeniem po przekroczeniu którego CB wejdzie w stan OPEN
  •  failureRateThreshold – wspomniany wyżej próg, wartość podajemy w procentach.
  •  waitDurationInOpenState – czas, w którym CB pozostanie w stanie OPEN
  •  automaticTransitionFromOpenToHalfOpenEnabled – po upływie czasu ustawionego w waitDurationInOpenState, CB może automatycznie przejść do trybu HALF_OPEN, bez czekania na kolejne żądanie.
  • timeout_duration – jest to opcja konfiguracyjna dla Time Limitera. Zapobiega zbyt długiemu czekaniu klienta na odpowiedź z API.
resilience4j.circuitbreaker.instances.waterSensorCb.slidingWindowSize=10
resilience4j.circuitbreaker.instances.waterSensorCb.failureRateThreshold=20
resilience4j.circuitbreaker.instances.waterSensorCb.waitDurationInOpenState=15000
resilience4j.circuitbreaker.instances.waterSensorCb.permittedNumberOfCallsInHalfOpenState=5
resilience4j.circuitbreaker.instances.waterSensorCb.automaticTransitionFromOpenToHalfOpenEnabled=true

resilience4j.timelimiter.instances.waterSensorTl.timeout-duration=2000

W tym momencie aplikacja jest odporna na blędy zewnętrznego systemu. Poniżej znajdują się dwie róźne odpowiedzi, dla happy i bad path.

{
    “water_level”: 100
}
{
    “message”: “Lost connection to water sensor API”
}

Podsumowanie: Circuit Breaker w systemach rozproszonych

W artykule przedstawiłem sposób działania wzorca architektonicznego Circuit Breaker i pokazałem jak go użyć w podstawowej konfiguracji w aplikacji spring bootowej.  

Leave a Reply

Your email address will not be published. Required fields are marked *