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

이제 우리는 훨씬 더 나은 확산을 가지고 있습니다. 우리는  충돌과 유휴 시간을 모두 제거했으며 초기 급증을 제외하고 거의 일정한 클라이언트 호출 속도로 끝납니다 .

차트 1.png

참고: 설명을 위해 간격을 과장했으며 실제 시나리오에서는 간격이 더 작을 것입니다.

6. 결론

이 사용방법(예제)에서는 지터로 지수 백오프를 강화하여 클라이언트 애플리케이션이 실패한 호출을 재시도하는 방법을 개선할 수 있는 방법을 살펴보았습니다.

사용방법(예제)에 사용된 샘플의 소스 코드는 GitHub 에서 사용할 수 있습니다 .

Generic footer banner