티스토리 뷰

SpringBoot

Spring Cloud Gateway Retry Filter

파미페럿 2024. 1. 28. 20:41

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

https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/retry-factory.html

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함