1. 개요
이 사용방법(예제)에서는 지수 백오프와 지터라는 두 가지 전략을 사용하여 클라이언트 재시도를 개선하는 방법을 살펴봅니다.
2. 재시도
분산 시스템에서 수많은 구성 요소 간의 네트워크 통신은 언제든지 실패할 수 있습니다. 클라이언트 응용 프로그램은 재시도를 구현하여 이러한 실패를 처리합니다 .
원격 서비스인 PingPongService를 호출하는 클라이언트 애플리케이션이 있다고 가정해 보겠습니다 .
interface PingPongService {
String call(String ping) throws PingPongServiceException;
}
클라이언트 애플리케이션은 PingPongService 가 PingPongServiceException 을 반환하는 경우 재시도해야 합니다 . 다음 섹션에서는 클라이언트 재시도를 구현하는 방법을 살펴보겠습니다.
3. Resilience4j 재시도
이 예에서는 Resilience4j 라이브러리, 특히 재시도 모듈을 사용합니다 . pom.xml 에 resilience4j-retry 모듈을 추가해야 합니다 .
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
</dependency>
재시도 사용에 대한 복습을 위해 Resilience4j 사용방법(예제)를 확인하는 것을 잊지 마십시오 .
4. 지수 백오프
클라이언트 애플리케이션은 책임감 있게 재시도를 구현해야 합니다. 클라이언트가 기다리지 않고 실패한 호출을 재시도하면 시스템에 과부하가 걸리고 이미 어려움을 겪고 있는 서비스의 추가 저하에 기여할 수 있습니다.
지수 백오프는 실패한 네트워크 호출의 재시도를 처리하기 위한 일반적인 전략입니다. 간단히 말해서 클라이언트는 연속적인 재시도 사이에 점진적으로 더 긴 간격을 기다립니다 .
wait_interval = base * multiplier^n
어디,
- base 는 초기 간격입니다. 즉, 첫 번째 재시도를 기다립니다.
- n 은 발생한 실패 수입니다.
- multiplier 는 적절한 값으로 대체할 수 있는 랜덤의 승수입니다.
이 접근 방식을 통해 간헐적인 오류 또는 더 심각한 문제에서 복구할 수 있도록 시스템에 숨 쉴 공간을 제공합니다.
initialInterval 및 승수를 허용하는 IntervalFunction을 구성하여 Resilience4j 재시도에서 지수 백오프 알고리즘을 사용할 수 있습니다 .
IntervalFunction은 재시 도 메커니즘에서 절전 기능으로 사용됩니다.
IntervalFunction intervalFn =
IntervalFunction.ofExponentialBackoff(INITIAL_INTERVAL, MULTIPLIER);
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(MAX_RETRIES)
.intervalFunction(intervalFn)
.build();
Retry retry = Retry.of("pingpong", retryConfig);
Function<String, String> pingPongFn = Retry
.decorateFunction(retry, ping -> service.call(ping));
pingPongFn.apply("Hello");
실제 시나리오를 시뮬레이션하고 PingPongService를 동시에 호출하는 여러 클라이언트가 있다고 가정해 보겠습니다.
ExecutorService executors = newFixedThreadPool(NUM_CONCURRENT_CLIENTS);
List<Callable> tasks = nCopies(NUM_CONCURRENT_CLIENTS, () -> pingPongFn.apply("Hello"));
executors.invokeAll(tasks);
4와 동일한 NUM_CONCURRENT_CLIENTS 에 대한 원격 호출 로그를 살펴보겠습니다 .
[thread-1] At 00:37:42.756
[thread-2] At 00:37:42.756
[thread-3] At 00:37:42.756
[thread-4] At 00:37:42.756
[thread-2] At 00:37:43.802
[thread-4] At 00:37:43.802
[thread-1] At 00:37:43.802
[thread-3] At 00:37:43.802
[thread-2] At 00:37:45.803
[thread-1] At 00:37:45.803
[thread-4] At 00:37:45.803
[thread-3] At 00:37:45.803
[thread-2] At 00:37:49.808
[thread-3] At 00:37:49.808
[thread-4] At 00:37:49.808
[thread-1] At 00:37:49.808
여기에서 명확한 패턴을 볼 수 있습니다. 클라이언트는 기하급수적으로 증가하는 간격을 기다리지만 모든 클라이언트는 각 재시도(충돌)에서 정확히 동시에 원격 서비스를 호출합니다.
우리는 문제의 일부만 해결했습니다. 더 이상 재시도로 원격 서비스를 망치지 않고 시간이 지남에 따라 워크로드를 분산시키는 대신 더 많은 유휴 시간으로 작업 기간을 분산시켰습니다. 이 동작은 Thundering Herd Problem 과 유사합니다 .
5. 지터 소개
이전 접근 방식에서는 클라이언트 대기 시간이 점진적으로 길어지지만 여전히 동기화됩니다. 지터를 추가하면 클라이언트 간의 동기화를 중단하여 충돌을 방지할 수 있습니다 . 이 접근 방식에서는 대기 간격에 임의성을 추가합니다.
wait_interval = (base * 2^n) +/- (random_interval)
여기서 random_interval을 더하거나 빼서 클라이언트 간 동기화를 중단합니다.
무작위 간격을 계산하는 메커니즘에 대해서는 다루지 않겠지만 무작위화는 클라이언트 호출의 훨씬 더 부드러운 분포를 위해 스파이크를 간격을 두고 배치해야 합니다.
randomizationFactor 도 허용하는 지수 임의 백오프 IntervalFunction 을 구성하여 Resilience4j 재시도에서 지터가 있는 지수 백오프를 사용할 수 있습니다 .
IntervalFunction intervalFn =
IntervalFunction.ofExponentialRandomBackoff(INITIAL_INTERVAL, MULTIPLIER, RANDOMIZATION_FACTOR);
실제 시나리오로 돌아가 지터가 있는 원격 호출 로그를 살펴보겠습니다.
[thread-2] At 39:21.297
[thread-4] At 39:21.297
[thread-3] At 39:21.297
[thread-1] At 39:21.297
[thread-2] At 39:21.918
[thread-3] At 39:21.868
[thread-4] At 39:22.011
[thread-1] At 39:22.184
[thread-1] At 39:23.086
[thread-5] At 39:23.939
[thread-3] At 39:24.152
[thread-4] At 39:24.977
[thread-3] At 39:26.861
[thread-1] At 39:28.617
[thread-4] At 39:28.942
[thread-2] At 39:31.039
이제 우리는 훨씬 더 나은 확산을 가지고 있습니다. 우리는 충돌과 유휴 시간을 모두 제거했으며 초기 급증을 제외하고 거의 일정한 클라이언트 호출 속도로 끝납니다 .
참고: 설명을 위해 간격을 과장했으며 실제 시나리오에서는 간격이 더 작을 것입니다.
6. 결론
이 사용방법(예제)에서는 지터로 지수 백오프를 강화하여 클라이언트 애플리케이션이 실패한 호출을 재시도하는 방법을 개선할 수 있는 방법을 살펴보았습니다.
사용방법(예제)에 사용된 샘플의 소스 코드는 GitHub 에서 사용할 수 있습니다 .