[Spring](수정예정) 비동기 프로그래밍 (N) - Backoff (feat.RejectedExecutionHandler)

백오프란?

백오프는 요청 또는 재시도 비율을 일시적으로 줄여 오류 또는 리소스 포화를 처리하는데 사용되는 전략을 의미한다. 

일시적인 과부하가 발생하거나 리소스를 사용할 수 없게 되는 시스템에서 자주 사용되며, 

이미 많은 스트레스를 받고 있는 시스템이나 구성 요소에 부담을 주지 않고 회보갈 수 있는 기회를 주는 것이 핵심 아이디어이다. 

백오프 전략 유형

백오프 전략에는 2가지 주요 유형이 있다.

 

Exponential Backoff

지수 백오프 전략은 재시도 간의 대기 시간을 기하급수적으로 늘리는 전략이다.

 

예를 들어 작업 제출이 실패하면 시스템은 재시도하기 전에 1초 동안 기다릴 수 있다고 가정했을 때,

다시 실패하면 2초, 4초 등 최대 지연 시간까지 기다리게 하는 전략이다.

혼잡 수준이나 장애율에 따라 동적으로 적응하기에 대상 시스템의 로드를  빠르게 줄여 복구할 기회를 더 많이 제공하므로 특히 효과적이다.

 

다만, 반복적으로 실패할 경우 대기 시간이 매우 길어질 수 있으며 잠재적으로 복구에 실제로 필요한 것보다 훨씬 더 작업 재시도가 지연될 수 있다.

또한 백오프를 계산하고 대기 시간에 대한 최대 한도나 적절한 백오프 요소를 구현하면 시스템이 더 복잡해질 수 있다.

마지막으로 상대적으로 덜 중요한 적업의 경우나 리소스 경합이 심한 시스템에선 초기 재시도 시도가 대기 시간을 빠르게 기하급수적으로 증가시키기에 상당한 지연이나 리소스 부족이 발생할 수 있다.

 

Linear Backoff 

선형 백오프 전략은 재시도 간의 대기 시간을 일정한 양만큼 늘리는 전략이다. 

 

예를 들어 실패할 때마다 시스템은 이전 시도보다 2초 더 기다릴 수 있다고 지정하는 것이다. 

대기 시간이 지속적으로 증가하므로 예측 및 관리가 수월하며 로드 및 장애율이 일관된 시스템에서는 쉽게 관리할 수 있다.

또한 지수 백오프 전략에 비해 구현이 간단하다.

 

오류나 과부하가 높은 상황에서는 대기 시간의 증가가 점진적이기 때문에 지수 백오프만큼 효과적으로 부하를 줄이지 못할 수 있다. 

특히 혼잡도가 높은 상황에서 시스템이 복구될 수 있을 만큼 빠르게 재시도 간의 지연을 증가시키지 못하기에 문제를 더 악화시킬 가능성이 있다.

마지막으로 다양한 시스템 부하 조건에 잘 적응하지 못한다. 대기 시간의 지속적인 증가로 인해 신속한 복구 요구에 비해 느릴 수 있고 가벼운 부하에서 재시도를 불필요하게 지연시킬 수 있다.

 

전략 선택

지수 백오프는 확장성과 시스템 로드에 대한 적응형 응답으로 주로 분산 시스템 및 웹 서비스에 널리 사용된다. 

만약 운영 단순성과 에측 가능성 보다 더 중요하거나 부하 조건이 상대적으로 안정적인 시스템인 경우에는 간단하고 예측 가능성이 높은 선형 백오프을 선택하여 사용할 수 있다.

 

끄적이는 중 (아래부분은 수정할 것)

백오프 관련

지수 백오프에 대한 간단한 의사 코드 예는 다음과 같습니다.

javaCopy code
int attempt = 0;
boolean taskSubmitted = false;
while(!taskSubmitted && attempt < MAX_ATTEMPTS) {
    try {
        executor.execute(task);
        taskSubmitted = true; // If submission succeeds, exit loop
    } catch (RejectedExecutionException e) {
        // Handle the rejection by backing off
        attempt++;
        long waitTime = calculateExponentialBackoff(attempt);
        Thread.sleep(waitTime);
    }
}

long calculateExponentialBackoff(int attempt) {
    return INITIAL_WAIT * (1 << attempt); // 1 << attempt is 2 to the power of attempt
}

고려사항

  • 최대 시도 횟수: 무한 루프를 방지하려면 최대 시도 횟수를 설정하는 것이 중요합니다.
  • 최대 대기 시간: 최대 대기 시간을 구현하면 백오프 기간이 지나치게 길어지는 것을 방지할 수 있습니다.
  • 지터: 대기 시간에 임의의 양의 지터(무작위 변형)를 추가하면 분산 시스템에서 동기화된 재시도를 방지하여 로드 급증을 유발할 수 있습니다.

백오프 전략을 구현하려면 복구 속도, 예상 로드, 실행 중인 작업의 중요성 등 시스템의 특정 요구 사항과 제약 조건을 신중하게 고려해야 합니다.

https://velog.io/@jazz_avenue/좀-더-우아한-Retry-Expenential-Backoff-with-Jitter

맥락 이해하기

Java에서 'ThreadPoolExecutor' 클래스는 작업자 스레드 풀을 관리하는 데 사용되며 실행 대기 중인 작업을 보관하는 대기열을 포함합니다. 새 작업을 제출하려고 하는데 스레드 풀과 큐가 모두 가득 차면 실행기의 동작은 **RejectedExecutionHandler**에 의해 결정됩니다. 핸들러가 기본 'AbortPolicy'로 설정된 경우 'RejectedExecutionException'이 발생합니다.

지수 백오프 전략

지수 백오프 전략은 실행기의 용량이 부족하여 작업을 제출할 수 없는 경우 연속 재시도 사이의 지연을 점진적으로 늘리는 것을 목표로 합니다. 이는 대기 중인 작업을 처리할 수 있는 시간을 시스템에 더 많이 제공하고 새로운 제출을 위한 공간을 확보하여 전반적인 시스템 안정성을 향상시키는 것입니다.

구현 세부정보

  1. 초기 대기 시간 및 최대 시도 횟수:
    • 초기 대기 시간(INITIAL_WAIT): 첫 번째 재시도 전 기본 지연 시간입니다. 이는 100밀리초 정도의 작은 것일 수 있습니다.
    • 최대 시도 횟수(MAX_ATTEMPTS): 작업 제출을 포기하기 전까지의 최대 재시도 횟수입니다. 이렇게 하면 무한 재시도 루프가 방지됩니다.
  2. 지수 백오프 계산: 'calculateExponentialBackoff' 메서드는 다음 재시도 전의 지연 시간을 계산합니다. 다음 공식을 사용합니다.여기서 '시도'는 현재 재시도 횟수이며 0부터 시작합니다.
  3. \text{지연} = \text{INITIAL_WAIT} \times 2^{\text{시도}}
  4. 지터 추가: 지터는 대기 시간에 임의성을 도입하여 많은 인스턴스가 동일한 간격으로 재시도를 시도하여 동기화된 재시도 및 잠재적인 로드 급증으로 이어지는 시나리오를 방지합니다. 지터링된 백오프를 다음과 같이 계산할 수 있습니다.여기서 **JITTER**는 상수(예: 0.1배 delay)이고 **random(-JITTER, +JITTER)**는 **-JITTER**와 +JITTER 사이의 난수를 생성합니다.
  5. 지터 지연=지연+무작위(−JITTER,+JITTER)\text{지터 지연} = \text{지연} + \text{무작위}(-\text{JITTER}, +\text{JITTER })지터링 지연=지연+랜덤(−JITTER,+JITTER)
  6. 최대 지연 처리: 백오프가 지나치게 길어지는 것을 방지하려면 지연을 최대값(MAX_DELAY)으로 제한할 수 있습니다.

지터를 사용한 유사 코드 예

지터 및 최대 지연을 포함하여 이 전략을 구현하는 방법은 다음과 같습니다.

javaCopy code
int attempt = 0;
boolean taskSubmitted = false;
while(!taskSubmitted && attempt < MAX_ATTEMPTS) {
    try {
        executor.execute(task);
        taskSubmitted = true; // If submission succeeds, exit loop
    } catch (RejectedExecutionException e) {
        // Handle the rejection by backing off
        attempt++;
        long waitTime = calculateExponentialBackoffWithJitter(attempt);
        Thread.sleep(Math.min(waitTime, MAX_DELAY)); // Ensure wait time does not exceed MAX_DELAY
    }
}

long calculateExponentialBackoffWithJitter(int attempt) {
    long delay = INITIAL_WAIT * (1L << attempt); // 1L << attempt is 2 to the power of attempt
    long jitter = ThreadLocalRandom.current().nextLong(-JITTER * delay, JITTER * delay);
    return delay + jitter;
}

이 예에서 'JITTER'는 랜덤 지터 값을 계산하는 데 사용하는 지연의 백분율입니다. **ThreadLocalRandom.current().nextLong(min, max)**는 지터에 대한 **min**과 max 사이의 임의의 long을 생성하는 데 사용됩니다.

고려사항

  • 테스트: 백오프 전략의 효과와 영향은 실제 조건에서 테스트하여 예상대로 작동하고 시스템 탄력성을 향상하는지 확인해야 합니다.
  • 모니터링: 작업 제출 실패에 대한 모니터링을 구현하고 관찰된 시스템 동작을 기반으로 매개변수를 조정하기 위해 재시도합니다.
  • 동적 조정: 고급 구현에서는 시스템 성능 및 로드의 실시간 피드백을 기반으로 매개변수를 동적으로 조정하는 것을 고려합니다.