좀 더 우아한 Retry (Expenential Backoff with Jitter)

Jazz Avenue·2022년 10월 8일
2

사내에서 APNs 관련 Retry 로직을 작성하기 위해 학습한 Exponential Backoff 관련 내용을 정리한다.

Exponential Backoff 가 필요한 이유

  • 서버와 서버간의 API 호출에 대한 재시도 행위는 매우 중요한데 단순히 한번의 네트워크 호출 실패로 서비스의 비즈니스 로직을 모두 실패처리하거나 fallback 처리하는 것은 몇번 재시도하는 것보다 큰 리소스 낭비가 될 수 있다.
  • 특히 명확한 비즈니스 로직상의 실패 응답을 받는 상황이 아니라 네트워크의 일시적인 장애로 인하여 발생한 timeout 의 경우에는 충분히 재시도 해볼만하다고 할 수 있다.

의미없는 Retry 행위

  • 평범한 재시도 행위 자체는 대부분 의미 없거나 네트워크에 부담을 더 가중하는 결과로 이어지게된다.
  • 대부분의 timeout 상황의 경우에도 특정 시간동안 네트워크 이슈가 지속되는 경우가 많기 때문에 이를 즉시 재시도한다고해도 모두 실패로 끝날 가능성이 높다.
  • 재시도 자체를 일정 시간 간격을 두지 않고 시도하는 자체가 기존에 문제가 발생한 네트워크에 더 부담을 가중할 뿐이다.
    • 특정 네트워크에 트래픽이 몰리는 상황이 발생해 요청 자체가 지연되는 상황에서 모든 클라이언트가 연속으로 재시도를 시도한다면, 네트워크 트래픽을 가중시키게 된다.

Exponential Backoff 를 이용한 똑똑한 재시도

  • 일정 시간 간격을 두고 재시도하는 것은 단순히 "일정 시간의 여유"만 주었다는 것을 제외하고는 동일하게 네트워크에 부하를 줄 가능성이 크다.
    • ex. 실패시 3초마다 요청을 시도
  • 때문에 일반적인 방법은 재시도하는 시간 간격이 매 시도마다 점차 늘어나는 Exponential Backoff 전략을 사용하는 것이다.
    • Exponential Backoff 전략에서는 지수에 비례하여 Backoff 시간을 조절하는데 다음과 같은 방식으로 동작한다.
    • ex. 첫번째 시도를 위한 대기 시간을 100ms, 두번째 재시도를 위한 대기 시간을 200ms, 세번째 재시도를 위한 대기시간을 400ms
    • 위와 같이 점차 재시도를 위한 시간이 2n2^n 만큼 증가하는 방식으로 동작한다.
  • Exponential Backoff 가 이러한 방식을 사용하는 이유는 재시도 횟수가 증가할수록 Backoff 시간이 증가하기 때문에 네트워크에 갑작스럽게 트래픽을 부담시키는 것을 피할 수 있다.
  • 하지만 이러한 방법도 동시에 요청이 몰린다면 동일한 시간 간격으로 모든 재시도가 수행될 것이기 때문에 한계점이 존재한다.

개선된 Exponential Backoff 전략 with Jitter

  • Jitter는 패킷의 지연이 수시로 변하면서 그 간격이 일정하지 않는 현상 즉, 지연 변이를 의미한다.
  • Jitter를 Retry에 적용하면 API를 요청하는 클라이언트 사이의 동일한 재시도 시간에 무작위성을 부여하여 서로 요청하는 시간대의 동시성을 분산시킬 수 있다.
  • 즉, 간단하게 말하면 Exponential Backoff에 무작위성을 더하여 동일한 시간대에 재시도 횟수가 집중되는 것을 분산시켜주는 것이라고 볼 수 있다.

Exponential Backoff with Jitter 구현

  • Jitter를 이용한 Exponential Backoff의 기본 아이디어 구현은 다음과 같다.
int interval = Math.min(MAX_INTERVAL_TIME, base * Math.pow(2, attempt));
int jitter = Random.nextInt(MIN_JITTER_VALUE, MAX_JITTER_VALUE);
Thread.sleep(inteval + jitter);
  • 위와 같이 시도 횟수에 따른 지수적인 시간의 증가와 더불어 Random한 Jitter 가중치를 부여함으로써 좀 더 개선된 Exponential Backoff를 구현할 수 있다.
  • Resilience4J 라는 라이브러리에서는 ofExponentialRandomBackoff()라는 형태로 이를 지원하고 있다.
static IntervalFunction ofExponentialRandomBackoff(
  long initialIntervalMillis,
  double multiplier,
  double randomizationFactor
) {
  checkInterval(initialIntervalMillis);
  checkMultiplier(multiplier);
  checkRandomizationFactor(randomizationFactor);
  return attempt -> {
    checkAttempt(attempt);
    final long interval = 
    of(initialIntervalMillis, x -> (long) (x * multiplier))
        .apply(attempt);
    return (long) randomize(interval, randomizationFactor);
  };
}

static double randomize(final double current, final double randomizationFactor) {
  final double delta = randomizationFactor * current;
  final double min = current - delta;
  final double max = current + delta;

  return (min + (Math.random() * (max - min + 1)));
}
  • 서비스에 간단하게 구현한 형태는 다음과 같다.
public void runWithRetry(Runnable runnable) {
  runWithRetry(runnable, 0);
}

private void runWithRetry(Runnable runnable, int count) {
  try {
    runnable.run();
  } catch (Exception e) {
  	if (count >= MAX_RETRY_COUNT) {
    	throw e;
    }
    
    try {
      Thread.sleep(BASE_INTERVAL_TIME * Math.pow(2, count) + ThreadLocalRandom.current().nextInt(MIN_JITTER_VALUE, MAX_JITTER_VALUE));
    } catch(InterrupedException e) {
      
    }
    runWithRetry(runnable, count + 1);
  }
}

참고

0개의 댓글

관련 채용 정보