공부하면서 기록한 내용
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
대상 라이브러리는 다음과 같다.
- spring-cloud-netflix-archaius
- spring-cloud-netflix-hystrix-contract
- spring-cloud-netflix-hystrix-dashboard
- spring-cloud-netflix-hystrix-stream
- spring-cloud-netflix-hystrix
- spring-cloud-netflix-ribbon
- spring-cloud-netflix-turbine-stream
- spring-cloud-netflix-turbine
- 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의 값을 직접 확인하면서 설정을 하면 된다.
- bulkhead : https://resilience4j.readme.io/docs/bulkhead#create-and-configure-a-threadpoolbulkhead
- circuitBreaker : https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker
- rateLimiter : https://resilience4j.readme.io/docs/ratelimiter#create-and-configure-a-ratelimiter
- retry : https://resilience4j.readme.io/docs/retry#create-and-configure-retry
이렇게 각 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의 값을 확인하여야 한다.
여기까지의 내용을 정리하면
- Spring Boot의 *Properties 속성으로 지정된 값을 기준으로 resilience4j의 config 정보를 생성된다.
- *Registry의 configuration에 해당 config 정보가 등록된다.
- 사용 시 등록된 configuration 정보를 기준으로 해당 호출에 대한 실제 처리에 생성되어 *Registry의 entryMap에 저장을 되며 이후 재사용한다.
사용하기
대강의 설정은 위와 같이 하였다면 실제 사용을 하는 방법은 2가지가 있다.
- registry에서 직접 호출하여 사용
- 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
}
}
}
'Study > Java' 카테고리의 다른 글
Spring Boot 2.6 Release Notes (0) | 2021.11.23 |
---|---|
[troubleshooting] eclipse (STS)에서 빌드 시 throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module 에러 발생 (0) | 2021.11.01 |
[troubleshooting] eclipse(STS)에서 sources and javadoc downdoad 무한 반복 현상 (0) | 2021.10.22 |
JDK 12 ~ JDK 17 사이 추가된 language specification feature (0) | 2021.09.15 |
JDK 17 New Features (0) | 2021.09.15 |
Java lambda expression(람다 표현식)을 사용한 지연 연산(Lazy Evaluation) (0) | 2021.08.27 |
maven central repository에 라이브러리 배포하기 (0) | 2021.07.20 |
PropertyEditor를 사용한 Data Binding (in Spring Data Redis) (0) | 2021.06.14 |
Spring Boot Data Redis 사용해보기 (0) | 2021.06.09 |
Spring Boot 2.5 Release Notes (0) | 2021.05.26 |