파란하늘의 지식창고
article thumbnail
반응형

공부하면서 기록한 내용

Spring Boot와 Resilienct4j에 대한 내용만 살펴봄


Hystrix -> Resilience4j로 변경되는 이유

Netflix OSS 제품군의 다양한 프로젝트들을 Spring에서 사용하기 위해 Spring 진영에서는 Spring Cloud Netflix 프로젝트를 제공하였다.

Netflix는 Hystrix, Ribbon, Turbine, Zuul과 같은 다양한 라이브러리를 공개하여 웹서비스의 장애 대응, 서비스 분산에 대한 좋은 대안들을 제시하였고 많이 쓰였다.

하지만 2018년에 Netflix가 ribbon, hytrix를 유지관리 모드 (maintenance mode, 새로운 기능을 추가하지 않고 버그 및 보안 문제만 수정)로 더 이상 개발하지 않는다고 발표하면서 자연스레 Spring Cloud Netflix 프로젝트도 동일한 상태가 되었다.

https://github.com/Netflix/Hystrix#hystrix-status

대상 라이브러리는 다음과 같다.

  1. spring-cloud-netflix-archaius
  2. spring-cloud-netflix-hystrix-contract
  3. spring-cloud-netflix-hystrix-dashboard
  4. spring-cloud-netflix-hystrix-stream
  5. spring-cloud-netflix-hystrix
  6. spring-cloud-netflix-ribbon
  7. spring-cloud-netflix-turbine-stream
  8. spring-cloud-netflix-turbine
  9. spring-cloud-netflix-zuul

이와 관련하여 Spring은 다음과 같은 대안을 권장하였다.

https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now

기존 변경
Hystrix Resilience4j
Hystrix Dashboard / Turbine Micrometer + Monitoring System
Ribbon Spring Cloud Loadbalancer
Zuul 1 Spring Cloud Gateway
Archaius1 Spring Boot external config + Spring Cloud Config

Resilience4j 라이브러리 구성

https://resilience4j.readme.io/

Core modules

resilience4j는 다음과 같은 기능을 제공하고 각 기능에 대한 모듈 라이브러리를 제공한다.

기능 설명 모듈
CircuitBreaker backend system의 상태 관리.
CLOSED, OPEN, HALF_OPEN, DISABLED, FORCED_OPEN 상태가 있음
resilience4j-circuitbreaker
Bulkhead 병렬 작업 제한 관리 resilience4j-bulkhead
RateLimiter 요청 제한 관리 resilience4j-ratelimiter
Retry 재시도 관리 resilience4j-retry
TimeLimiter 실행 시간 제한 관리 resilience4j-timelimiter
Cache 캐시 처리 resilience4j-cache

core module들은 모두 resilience4j-core modue을 참조한다.

Framework modules

resilience4j를 사용하기 위한 spring 관련 라이브러리를 spring이 제공하지 않고 resilience4j가 제공해준다.

이는 다른 framework에 대해서도 마찬가지이다.

  • resilience4j-spring-boot: Spring Boot Starter
  • resilience4j-spring-boot2: Spring Boot 2 Starter
  • resilience4j-ratpack: Ratpack Starter
  • resilience4j-vertx: Vertx Future decorator

이 모듈들은 모두 resilience4j-framework-common을 참조한다.

Spring 관련 라이브러리 구성

resilience4j-spring-boot2는 resilience4j-spring을 참조하고 resilience4j-spring은 resilience4j-framework-common을 참조한다.

설정 정보에 대한 구성도 동일하게 상위 모듈의 class를 확장해서 쓰는 형태이다.

예를 들어 CircuitBreakerProperties는 다음과 같이 구성되어 있다.

Bulkhead, RateLimiter, Retry, TimeLimiter도 동일한 구성이며 다음과 같다.

resilience4j-spring-boot2 resilience4j-spring resilience4j-framework-common resilience4j-framework-common
BulkheadProperties BulkheadConfigurationProperties BulkheadConfigurationProperties.class CommonProperties
CircuitBreakerProperties CircuitBreakerConfigurationProperties CircuitBreakerConfigurationProperties CommonProperties
RateLimiterProperties RateLimiterConfigurationProperties RateLimiterConfigurationProperties CommonProperties
RetryProperties RetryConfigurationProperties RetryConfigurationProperties CommonProperties
TimeLimiterProperties TimeLimiterConfigurationProperties TimeLimiterConfigurationProperties CommonProperties

기본적인 설정은 모두 resilience4j-framework-common의 *Propeties에 있고 이를 확장한 resilience4j-spring은 order 정보만 추가로 가지고 있으며 이를 확장한 resilience4j-spring-boot2의 *Properties는 아무 설정값이 없다.

역할로 나누어 jar가 구성되어 있는데 resilience4j-spring-boot2는 boot autoconfiguration으로 동작할 설정을 담당하고 resilience4j-spring은 이렇게 boot로 설정된 값을 *Registry에 등록 및 spring bean 생성을 담당한다.

설정하기

maven dependency 설정

spring boot를 사용하는 프로젝트에 다음과 같이 의존성을 추가한다.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

properties 설정

각 모듈의 Properties (BulkheadProperties, CircuitBreakerProperties, RetryProperties, RateLimiterProperties)는 모두 공통적으로 다음 속성을 가지고 있다.

Map<String, String> tags = new HashMap<>();
private Map<String, InstanceProperties> instances = new HashMap<>();
private Map<String, InstanceProperties> configs = new HashMap<>();

InstanceProperties는 각 type별로 다른 설정 값을 가지고 있으며 resilience4j 문서에서 각각의 type에 대한 설정 정보를 확인할 수 있다.

문서의 설명과 해당 class의 값을 직접 확인하면서 설정을 하면 된다.

이렇게 각 properties들이 구성되어 있으며 이를 통해 spring properties 설정을 다음과 같이 할 수 있다.

resilience4j.circuitbreaker:
    configs:
        default:
            slidingWindowSize: 100
            permittedNumberOfCallsInHalfOpenState: 10
            waitDurationInOpenState: 10000
            failureRateThreshold: 60
            eventConsumerBufferSize: 10
            registerHealthIndicator: true
        someShared:
            slidingWindowSize: 50
            permittedNumberOfCallsInHalfOpenState: 10
    instances:
        backendA:
            baseConfig: default
            waitDurationInOpenState: 5000
        backendB:
            baseConfig: someShared

configs는 기본적인 설정들을 가지고 있다.

가장 최상위 설정은 "default"라는 이름의 설정이다.

별도로 설정하지 않은 경우 기본 설정값으로 "default" config가 생성된다.

instances는 각 instance에서 개별로 적용될 수 있는 설정을 관리한다.

base config를 지정하여 속성을 상속받아 설정할 수 있다.

이렇게 설정된 값을 기반으로 BulkheadRegistry, CircuitBreakerRegistry, RateLimiterRegistry, RetryRegistry, TimeLimiterRegistry가 생성이 되며 설정은 각 객체의 configurations 속성으로 위치한다.

registry는 각각 Type 별로 resilience4j-bulkhead, resilience4j-circuitbreaker, resilience4j-ratelimiter, resilience4j-retry, resilience4j-timelimiter jar에서 제공된다.

resilience4j가 제공하는 Registry 구현 객체는 다음과 같은 InMemory 객체들이다.

Resilience4j Type 구현체
Bulkhead InMemoryBulkheadRegistry
CircuitBreaker InMemoryCircuitBreakerRegistry
RateLimiter InMemoryRateLimiterRegistry
Retry InMemoryRetryRegistry
TimeLimiter InMemoryTimeLimiterRegistry

InMemoryRegistry는 공통적으로 다음의 값을 가지고 있다.

protected final RegistryStore<E> entryMap;
protected final ConcurrentMap<String, C> configurations;

configuration엔 설정 정보가 담기고 entryMap은 실제 사용하는 객체 정보가 담기게 된다.

ConfigCustomizer 설정 (optional)

resilience4j-spring을 사용할 경우 *Registry는 별도로 bean을 생성하지 못하며 만약 custom 하게 다른 설정을 하고자 하는 경우 *ConfigCustomizer를 따로 생성하여 사용한다.

Resilienc4j Type Instance Customizer class
Circuit breaker CircuitBreakerConfigCustomizer
Retry RetryConfigCustomizer
Rate limiter RateLimiterConfigCustomizer
Bulkhead BulkheadConfigCustomizer
ThreadPoolBulkhead ThreadPoolBulkheadConfigCustomizer
Time Limiter TimeLimiterConfigCustomizer

실제 사용 시 각 사용한 호출에 대해 *Registry의 configuration의 값을 기준으로 실제 사용할 설정 값이 생성되며 해당 정보는 *Registry의 entryMap에 저장된다.

lambda를 사용하여 호출 시 생성되도록 구성되어 있기 때문에 최초 registry에는 entryMap의 값이 없다.

따라서 사용하면서 해당 entryMap의 값을 확인하여야 한다.

여기까지의 내용을 정리하면

  1. Spring Boot의 *Properties 속성으로 지정된 값을 기준으로 resilience4j의 config 정보를 생성된다.
  2. *Registry의 configuration에 해당 config 정보가 등록된다.
  3. 사용 시 등록된 configuration 정보를 기준으로 해당 호출에 대한 실제 처리에 생성되어 *Registry의 entryMap에 저장을 되며 이후 재사용한다.

사용하기

대강의 설정은 위와 같이 하였다면 실제 사용을 하는 방법은 2가지가 있다.

  1. registry에서 직접 호출하여 사용
  2. spring aop로 제공되는 annotation 사용

직접 호출하여 사용하기

https://resilience4j.readme.io/docs/examples

registry에서 사용할 name을 기준으로 설정을 호출하고 실행한다.

boot와 상관없이 resilience4j를 사용한 가장 기본적인 호출 방법이다.

// create or get circuitBreaker
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("testName");

// execute 
String result = circuitBreaker.executeSupplier(backendService::doSomething);

위 예제의 경우 circuitBreaker를 호출하고 직접 사용한 경우이다.

하지만 resilience4j는 circuitBreaker뿐만 아니라 bulkhead, retry와 같이 여러 type을 제공하기 때문에 이 type들을 대상에 대하여 같이 사용할 수 있도록 decorator 처리를 지원한다.

Supplier<String> supplier = () -> {
    // 실행할 내용
    return "String 반환의 경우";
};

// create or get circuitBreaker
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("testName");

// create or get TimeLimiter
RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("testName");

// decorate circuitBreaker
Supplier<String> circuitBreakerSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, supplier);

// decorate timeLimiter
Supplier<String> rateLimiterSupplier = RateLimiter.decorateSupplier(rateLimiter, circuitBreakerSupplier);

// decorate 된 내용을 모두 실행
rateLimiterSupplier.get();

위와 같이 실행할 내용을 supplier 매개 변수로 받아 순차적으로 각 type들을 실행할 수 있도록 decorate를 할 수 있다.

위의 경우 Supplier FunctionalInterface를 사용한 경우인데 이외에도 여러 FunctionalInterface를 사용하기 위한 static method를 제공하고 있다.

예를 들면 decorateRunnable, decorateFunction, decorateCallable, decorateConsumer와 같은 것들과 이런 FunctionalInterface에 throwable 처리를 하여 재 선언한 CheckedRunnable, CheckedFunction, CheckedConsumer에 대한 decorateCheckedRunnable, decorateCheckedFunction, decorateCheckedConsumer 등의 static method도 제공된다.

(각 type 별로 비슷하나 timeLimiter는 조금 다른 부분이 있어 확인이 필요)

spring aop 사용하기

https://resilience4j.readme.io/docs/getting-started-3

resilience4j를 적용할 대상 method에 다음처럼 설정한다.

@Bulkhead(name = BACKEND, type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> doSomethingAsync() throws InterruptedException {
        Thread.sleep(500);
        return CompletableFuture.completedFuture("Test");
}

제공하는 type을 모두 지정하여 사용할 수 있다.

@CircuitBreaker(name = BACKEND, fallbackMethod = "fallback")
@RateLimiter(name = BACKEND)
@Bulkhead(name = BACKEND)
@Retry(name = BACKEND, fallbackMethod = "fallback")
@TimeLimiter(name = BACKEND)
public Mono<String> method(String param1) {
    return Mono.error(new NumberFormatException());
}

private Mono<String> fallback(String param1, IllegalArgumentException e) {
    return Mono.just("test");
}

private Mono<String> fallback(String param1, RuntimeException e) {
    return Mono.just("test");
}

boot를 사용하면 각 type들에 대한 AOP 순서에 대한 기본 설정은 다음과 같다.

Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )

각 AOP에 대한 order는 다음과 같이 설정되어 있다.

type order default value
Retry Ordered.LOWEST_PRECEDENCE - 4
CircuitBreaker Ordered.LOWEST_PRECEDENCE - 3
RateLimiter Ordered.LOWEST_PRECEDENCE - 2
TimeLimiter Ordered.LOWEST_PRECEDENCE - 1
Bulkhead Ordered.LOWEST_PRECEDENCE

참고. Ordered.LOWEST_PRECEDENCE = Integer.MAX_VALUE

만약 순서를 변경하고 싶은 경우 다음 properties값을 설정하면 된다.

resilience4j.retry.retryAspectOrder
resilience4j.circuitbreaker.circuitBreakerAspectOrder
resilience4j.ratelimiter.rateLimiterAspectOrder
resilience4j.timelimiter.timeLimiterAspectOrder
resilience4j.bulkhead.bulkheadAspectOrder

spring metric 사용하기

actuator에 resilience4j 관련 metric endpoint 및 health endpoint를 제공한다.

metric endpoint는 자동 추가되며 '/actuator/metrics'에 다음과 같은 값들이 추가된다.

{
    "names": [
        "resilience4j.bulkhead.available.concurrent.calls",
        "resilience4j.bulkhead.max.allowed.concurrent.calls",
        "resilience4j.circuitbreaker.buffered.calls",
        "resilience4j.circuitbreaker.calls",
        "resilience4j.circuitbreaker.failure.rate",
        "resilience4j.circuitbreaker.not.permitted.calls",
        "resilience4j.circuitbreaker.slow.call.rate",
        "resilience4j.circuitbreaker.slow.calls",
        "resilience4j.circuitbreaker.state",
        "resilience4j.ratelimiter.available.permissions",
        "resilience4j.ratelimiter.waiting_threads",
        "resilience4j.retry.calls",
        "resilience4j.timelimiter.calls"
        ]
}

 

health endpoint의 경우 다음과 같이 설정 값을 추가하여 사용하면 된다.

management.health.circuitbreakers.enabled: true
management.health.ratelimiters.enabled: true

resilience4j.circuitbreaker:
  configs:
    default:
      registerHealthIndicator: true


resilience4j.ratelimiter:
  configs:
    default:
      registerHealthIndicator: true

resilience4j를 사용한 호출이 발생하기 전에는 health endpoint에서는 관련한 정보 노출이 되지 않는다.

호출이 발생한 이후엔 다음과 같이 내용을 확인할 수 있게 된다.

{
  "status": "UP",
  "details": {
    "circuitBreakers": {
      "status": "UP",
      "details": {
        "backendB": {
          "status": "UP",
          "details": {
            "failureRate": "-1.0%",
            "failureRateThreshold": "50.0%",
            "slowCallRate": "-1.0%",
            "slowCallRateThreshold": "100.0%",
            "bufferedCalls": 0,
            "slowCalls": 0,
            "slowFailedCalls": 0,
            "failedCalls": 0,
            "notPermittedCalls": 0,
            "state": "CLOSED"
          }
        },
        "backendA": {
          "status": "UP",
          "details": {
            "failureRate": "-1.0%",
            "failureRateThreshold": "50.0%",
            "slowCallRate": "-1.0%",
            "slowCallRateThreshold": "100.0%",
            "bufferedCalls": 0,
            "slowCalls": 0,
            "slowFailedCalls": 0,
            "failedCalls": 0,
            "notPermittedCalls": 0,
            "state": "CLOSED"
          }
        }
      }
    }
  }
}

또한 이벤트 발생에 대한 endpoint도 제공된다.

대략 다음과 같은 다양한 endpoint가 '/actuator'에 추가된다.

{
    "_links": {
        "bulkheads": {
            "href": "http://localhost:8084/actuator/bulkheads",
            "templated": false
        },
        "bulkheadevents-bulkheadName-eventType": {
            "href": "http://localhost:8084/actuator/bulkheadevents/{bulkheadName}/{eventType}",
            "templated": true
        },
        "bulkheadevents-bulkheadName": {
            "href": "http://localhost:8084/actuator/bulkheadevents/{bulkheadName}",
            "templated": true
        },
        "bulkheadevents": {
            "href": "http://localhost:8084/actuator/bulkheadevents",
            "templated": false
        },
        "circuitbreakers-name": {
            "href": "http://localhost:8084/actuator/circuitbreakers/{name}",
            "templated": true
        },
        "circuitbreakers": {
            "href": "http://localhost:8084/actuator/circuitbreakers",
            "templated": false
        },
        "circuitbreakerevents-name": {
            "href": "http://localhost:8084/actuator/circuitbreakerevents/{name}",
            "templated": true
        },
        "circuitbreakerevents-name-eventType": {
            "href": "http://localhost:8084/actuator/circuitbreakerevents/{name}/{eventType}",
            "templated": true
        },
        "circuitbreakerevents": {
            "href": "http://localhost:8084/actuator/circuitbreakerevents",
            "templated": false
        },
        "ratelimiters": {
            "href": "http://localhost:8084/actuator/ratelimiters",
            "templated": false
        },
        "ratelimiterevents-name-eventType": {
            "href": "http://localhost:8084/actuator/ratelimiterevents/{name}/{eventType}",
            "templated": true
        },
        "ratelimiterevents": {
            "href": "http://localhost:8084/actuator/ratelimiterevents",
            "templated": false
        },
        "ratelimiterevents-name": {
            "href": "http://localhost:8084/actuator/ratelimiterevents/{name}",
            "templated": true
        },
        "retries": {
            "href": "http://localhost:8084/actuator/retries",
            "templated": false
        },
        "retryevents-name-eventType": {
            "href": "http://localhost:8084/actuator/retryevents/{name}/{eventType}",
            "templated": true
        },
        "retryevents": {
            "href": "http://localhost:8084/actuator/retryevents",
            "templated": false
        },
        "retryevents-name": {
            "href": "http://localhost:8084/actuator/retryevents/{name}",
            "templated": true
        },
        "timelimiters": {
            "href": "http://localhost:8084/actuator/timelimiters",
            "templated": false
        },
        "timelimiterevents-name-eventType": {
            "href": "http://localhost:8084/actuator/timelimiterevents/{name}/{eventType}",
            "templated": true
        },
        "timelimiterevents": {
            "href": "http://localhost:8084/actuator/timelimiterevents",
            "templated": false
        },
        "timelimiterevents-name": {
            "href": "http://localhost:8084/actuator/timelimiterevents/{name}",
            "templated": true
        }
    }
}

 

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요