티스토리 뷰
spirng cloud gateway에서는 리퀘스트에 대해 filter를 적용해 상황에 따라 특정한 동작을 할 수 있다.
custom filter들도 만들 수 있는데 spring cloud gateway 라이브러리에 기본으로 제공해주는 filter들이 몇 개 있다.
그 중에서 제일 많이들 사용하고 게이트웨이 서버 에러 처리를 위해 한 번쯤은 다룰 수밖에 없는 RetryGatewayFilterFactory에 대해 간단하게 정리해보려 한다.
RetryGatewayFilterFactory
spring cloud gateway에서 제공해주는 기본 filter로 게이트웨이를 거쳐간 요청에서 에러가 발생했을 시 retry하기 위해 사용하는 filter이다.
RetryGatewayFilterFactory는 아래와 같이 설정 값들을 넣어서 상황에 맞게 retry, repeat로 나뉘어서 동작한다.
retry는 특정 exceptions에 대해 재시도 하는 것이고 repeat는 특정 HTTP status, HTTP status serie에 대해 재시도 한다.
spring:
cloud:
gateway:
routes:
- id: test
uri: http://localhost:8081
predicates:
- Path=/test/**
# gateway filter
filters:
- name: Retry
args:
methods: GET,PUT,POST,DELETE
exceptions:
- java.io.IOException
backoff:
firstBackoff: 200ms
maxBackoff: 200ms
factor: 1
basedOnPreviousValue: false
retries: 3
series:
statuses:
methods
어떤 methods에 대해 retry, repeat할지 설정할 수 있다.
기본 값으로 GET이 설정되어 있다.
exceptions
어떤 exception이 발생했을 때 retry할지 설정할 수 있다.
기본 값으로 IOException, TimeOutException이 설정되어 있다.
backoff
retry, repeat 주기에 대해 설정할 수 있다.
firstBackoff, maxBackoff, factor, basedOnPreviousValue이 네 가지 값 설정을 통해 backoff 설정을 할 수 있다.
첫 번째 에러가 발생해서 retry/repeat를 하게 될 경우 firstBackoff 뒤에 다시 재시도 하게 되고 그 후에 또 에러가 발생하면 firstBackoff * (factor^n)을 계산한 값 뒤에 재시도 하게 된다. 즉 n번 째 에러에 대해 firstBackoff * (facktor^n) 뒤에 재시도 하게 된다는 것이다. (maxBackoff가 설정되어 있을 경우 maxBackoff를 넘을 경우 maxBackoff 뒤에 재시도)
basedOnPreviousValue의 경우 true일 경우 그 전 backoff * factor 값으로 백오프 값을 계산해 재시도 한다고 한다.
firstBackoff는 5ms, factor는 2, basedOnPreviousValue는 true로 기본 값이 설정 되어 있다.
retries
몇 번까지 retry/repeat 할지 설정이다.
기본 값은 3이다.
series
repeat 설정에 대한 것으로 어떤 HTTP statue series에 재시도 할지 설정할 수 있다.
기본 값으로 SERVER_ERROR가 설정되어 있다. 즉, 5xx번 대 에러가 발생하면 기본적으로 재시도를 한다는 것이다.
statuses
repeat 설정에 대한 것으로 어떤 HTTP status에 재시도 할지 설정할 수 있다.
기본값은 설정되어 있지 한다.
위 설정들을 토대로 retry, repeat 설정하는 것은 RetryGatewayFilterFactory의 apply 함수에서 확인할 수 있다.
public GatewayFilter apply(RetryConfig retryConfig) {
retryConfig.validate();
Repeat<ServerWebExchange> statusCodeRepeat = null;
if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) {
Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> {
ServerWebExchange exchange = context.applicationContext();
if (exceedsMaxIterations(exchange, retryConfig)) {
return false;
}
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
boolean retryableStatusCode = retryConfig.getStatuses().contains(statusCode);
// null status code might mean a network exception?
if (!retryableStatusCode && statusCode != null) {
// try the series
retryableStatusCode = false;
for (int i = 0; i < retryConfig.getSeries().size(); i++) {
if (statusCode instanceof HttpStatus) {
HttpStatus httpStatus = (HttpStatus) statusCode;
if (httpStatus.series().equals(retryConfig.getSeries().get(i))) {
retryableStatusCode = true;
break;
}
}
}
}
final boolean finalRetryableStatusCode = retryableStatusCode;
trace("retryableStatusCode: %b, statusCode %s, configured statuses %s, configured series %s",
() -> finalRetryableStatusCode, () -> statusCode, retryConfig::getStatuses,
retryConfig::getSeries);
HttpMethod httpMethod = exchange.getRequest().getMethod();
boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
trace("retryableMethod: %b, httpMethod %s, configured methods %s", () -> retryableMethod,
() -> httpMethod, retryConfig::getMethods);
return retryableMethod && finalRetryableStatusCode;
};
statusCodeRepeat = Repeat.onlyIf(repeatPredicate)
.doOnRepeat(context -> reset(context.applicationContext()));
BackoffConfig backoff = retryConfig.getBackoff();
if (backoff != null) {
statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff));
}
}
// TODO: support timeout, backoff, jitter, etc... in Builder
Retry<ServerWebExchange> exceptionRetry = null;
if (!retryConfig.getExceptions().isEmpty()) {
Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
ServerWebExchange exchange = context.applicationContext();
if (exceedsMaxIterations(exchange, retryConfig)) {
return false;
}
Throwable exception = context.exception();
for (Class<? extends Throwable> retryableClass : retryConfig.getExceptions()) {
if (retryableClass.isInstance(exception)
|| (exception != null && retryableClass.isInstance(exception.getCause()))) {
trace("exception or its cause is retryable %s, configured exceptions %s",
() -> getExceptionNameWithCause(exception), retryConfig::getExceptions);
HttpMethod httpMethod = exchange.getRequest().getMethod();
boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
trace("retryableMethod: %b, httpMethod %s, configured methods %s", () -> retryableMethod,
() -> httpMethod, retryConfig::getMethods);
return retryableMethod;
}
}
trace("exception or its cause is not retryable %s, configured exceptions %s",
() -> getExceptionNameWithCause(exception), retryConfig::getExceptions);
return false;
};
exceptionRetry = Retry.onlyIf(retryContextPredicate)
.doOnRetry(context -> reset(context.applicationContext())).retryMax(retryConfig.getRetries());
BackoffConfig backoff = retryConfig.getBackoff();
if (backoff != null) {
exceptionRetry = exceptionRetry.backoff(getBackoff(backoff));
}
}
GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry);
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return gatewayFilter.filter(exchange, chain);
}
@Override
public String toString() {
return filterToStringCreator(RetryGatewayFilterFactory.this).append("routeId", retryConfig.getRouteId())
.append("retries", retryConfig.getRetries()).append("series", retryConfig.getSeries())
.append("statuses", retryConfig.getStatuses()).append("methods", retryConfig.getMethods())
.append("exceptions", retryConfig.getExceptions()).toString();
}
};
}
코드를 보면 알 수 있지만 repeat는 status 또는 series 값이 비어 있지 않을 경우 설정되고 retry의 경우는 exceptions의 값이 비어 있지 않으면 설정된다.
WebFlux가 기본인 gateway 답게 filter 내의 코드들도 웹플럭스 기반으로 작성 되어 있다.
주의 사항
spring cloud gateway로 게이트웨이 서버를 구축하고 retryFilter를 사용해봤는데 예상한대로 동작하지 않아서 애를 좀 먹었었다.
내가 원한 것은 게이트웨이로 연결한 서버에서 특정 exception이 발생하면 재시도 하는 것이었다.
그래서 series, statues를 다 빈값으로 설정하고 exception에 에러 클래스 정의를 했는데 아무리 테스트를 해봐도 재시도를 안 하는 것이다. 그래서 계속 테스트 해봤음에도 재시도를 하지 않았다.
원인은 웹플럭스 동작을 내가 제대로 이해하지 못해 발생했던 것이다.
RetryGatewayFilterFactory에서 retry의 경우는 웹플럭스의 Retry.wiehThrowable을 이용해서 구현되는데 이는 doOnError에 걸리는 애들한테 동작한다.
그런데 웹플럭스의 경우 status가 400이든 500이든 응답값을 제대로 받으면 doOnError가 아닌 doOnSuccess에 걸리게 된다. 즉, 에러라고 인식하지 않는 것이다. 내가 구현했던 게이트웨이 서버에서 연결한 서버들은 다 ExceptionHandler를 구현해서 에러의 경우에도 정한 에러 응답값을 다 전달하고 있다. 그래서 아무리 테스트를 해도 exception에 대한 retryFilter가 동작하지 않았던 것이다.
Publisher<Void> publisher = chain.filter(exchange)
// .log("retry-filter", Level.INFO)
.doOnSuccess(aVoid -> updateIteration(exchange)).doOnError(throwable -> updateIteration(exchange));
if (retry != null) {
// retryWhen returns a Mono<Void>
// retry needs to go before repeat
publisher = ((Mono<Void>) publisher)
.retryWhen(reactor.util.retry.Retry.withThrowable(retry.withApplicationContext(exchange)));
}
if (repeat != null) {
// repeatWhen returns a Flux<Void>
// so this needs to be last and the variable a Publisher<Void>
publisher = ((Mono<Void>) publisher).repeatWhen(repeat.withApplicationContext(exchange));
}
즉 그와 같은 에러의 경우는 statue, series로 구분해 동작하는 repeat를 이용해야 하는것이다.
retry는 에러가 발생했을 때의 제시도를 하지만 repeat는 에러가 아니어도 재시도를 한다는 것을 이해해야 한다.
spring cloud gateway가 웹플럭스 기반인만큼 그 필터들도 웹플럭스 기반이어서 필터를 제대로 쓰기 위해서는 웹플럭스에 대한 이해가 필요한 것 같다.
✋ Retry GatewayFilter Factory
'SpringBoot' 카테고리의 다른 글
RestTemplate 대신 WebClient (0) | 2024.03.15 |
---|---|
JsonSerializer, JsonDeserializer (1) | 2024.02.04 |
FeignClient config bean 설정 시 timeout 설정이 안 먹는 이유 (2) | 2022.09.30 |
Spring AOP @Before, @After, @Around (0) | 2021.10.23 |
EhCache2로 캐싱 기능 구현해보기 (1) | 2021.08.30 |
- Total
- Today
- Yesterday
- MAC
- 메시지큐
- 어노테이션
- k8s
- eclipse
- ssh
- 이클립스
- 역직렬화
- postgres
- DATABASE
- 캐시
- Java
- 공간데이터
- Caching
- DB
- enum
- Spring
- springboot
- annotation
- PostgreSQL
- cache
- 스프링부트
- 자바
- rabbitmq
- 데이터베이스
- JPA
- Intellij
- 캐싱
- mockito
- HttpClient
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |