network
exponential backoff란?

exponential backoff란?

exponential backoff는 실패한 작업을 바로 다시 시도하지 않고, 재시도 간격을 점점 크게 늘리는 방식이다

한국어로는 보통 지수 백오프 라고 부른다

예를 들면 이런 식이다

1초 뒤 재시도
2초 뒤 재시도
4초 뒤 재시도
8초 뒤 재시도
16초 뒤 재시도

매번 같은 시간만 기다리는 것이 아니라, 실패할수록 기다리는 시간을 보통 2배씩 늘린다

왜 사용할까?

서버 요청이 실패했다고 가정

실패하자마자 계속 재시도하면 문제가 더 커질 수 있다

요청 실패
바로 재시도
또 실패
바로 재시도
또 실패
바로 재시도

이런 식으로 동작하면 클라이언트는 계속 요청을 보내고, 서버는 회복할 시간 없이 계속 요청을 받게 된다

특히 장애가 났거나 네트워크가 불안정한 상황에서는 재시도 자체가 추가 부하가 될 수 있다

그래서 재시도 사이에 시간을 둔다

그리고 계속 실패할수록 이런 의미로 더 오래 기다린다

아직 문제가 해결되지 않았나보다
그러면 조금 더 기다렸다가 다시 시도하자

이게 exponential backoff의 핵심이다

지수적으로 증가한다는 뜻

여기서 exponential지수적 이라는 뜻이다

대기 시간이 단순히 1초씩 더해지는 것이 아니다

선형 증가

1초
2초
3초
4초
5초

이건 매번 1초씩 더해지는 방식이다

지수 증가

1초
2초
4초
8초
16초

이건 매번 2배씩 커지는 방식이다

수식으로 보면 대략 이렇게 표현할 수 있다

delay = baseDelay * 2^retryCount

baseDelay가 1초라면

retryCount계산대기 시간
01초 * 2^01초
11초 * 2^12초
21초 * 2^24초
31초 * 2^38초
41초 * 2^416초

간단한 코드 예시

TypeScript로 단순하게 쓰면 이런 느낌이다

const baseDelay = 1000;
 
function getBackoffDelay(retryCount: number) {
  return baseDelay * Math.pow(2, retryCount);
}
 
console.log(getBackoffDelay(0)); // 1000
console.log(getBackoffDelay(1)); // 2000
console.log(getBackoffDelay(2)); // 4000
console.log(getBackoffDelay(3)); // 8000

실제 재시도 흐름에 넣으면 이런 식이다

const baseDelay = 1000;
const maxRetryCount = 5;
 
function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
async function requestWithRetry() {
  for (let retryCount = 0; retryCount <= maxRetryCount; retryCount++) {
    try {
      return await request();
    } catch (error) {
      if (retryCount === maxRetryCount) {
        throw error;
      }
 
      const delay = baseDelay * Math.pow(2, retryCount);
 
      await sleep(delay);
    }
  }
}

이 코드는 요청이 실패할 때마다 다음 시간만큼 기다린다

1번째 실패 후 1초 대기
2번째 실패 후 2초 대기
3번째 실패 후 4초 대기
4번째 실패 후 8초 대기
5번째 실패 후 16초 대기

max delay가 필요하다

exponential backoff는 계속 2배씩 늘어나기 때문에 그대로 두면 대기 시간이 너무 커질 수 있다

1초
2초
4초
8초
16초
32초
64초
128초

그래서 보통 최대 대기 시간을 같이 둔다

const baseDelay = 1000;
const maxDelay = 30000;
 
function getBackoffDelay(retryCount: number) {
  const delay = baseDelay * Math.pow(2, retryCount);
 
  return Math.min(delay, maxDelay);
}

이렇게 하면 아무리 많이 실패해도 최대 30초까지만 기다린다

1초
2초
4초
8초
16초
30초
30초
30초

jitter를 같이 쓰는 이유

exponential backoff만 쓰면 재시도 간격은 늘어나지만, 여러 클라이언트가 같은 타이밍에 다시 몰릴 수 있다

예를 들어 서버 장애로 클라이언트 1만 개가 동시에 실패했다고 가정

그리고 모든 클라이언트가 같은 설정을 가지고 있다

1초 뒤 재시도
2초 뒤 재시도
4초 뒤 재시도
8초 뒤 재시도

그러면 1초 뒤에 1만 개가 동시에 재시도하고, 2초 뒤에도 1만 개가 동시에 재시도할 수 있다

서버 입장에서는 재시도 시간이 조금 늘어났을 뿐, 여전히 요청이 한 번에 몰린다

이 문제를 줄이려고 jitter를 같이 넣는다

jitter는 기준 delay에 랜덤한 흔들림을 추가하는 것이다

원래 delay: 4000ms

실제 재시도:
- 클라이언트 A: 3210ms 뒤 재시도
- 클라이언트 B: 4380ms 뒤 재시도
- 클라이언트 C: 5120ms 뒤 재시도

이렇게 재시도 타이밍을 조금씩 흩뿌리면 서버에 한 번에 몰리는 요청을 줄일 수 있다

jitter를 포함한 코드 예시

간단하게는 이런 식으로 만들 수 있다

const baseDelay = 1000;
const maxDelay = 30000;
 
function getBackoffDelayWithJitter(retryCount: number) {
  const exponentialDelay = baseDelay * Math.pow(2, retryCount);
  const cappedDelay = Math.min(exponentialDelay, maxDelay);
  const jitter = Math.random() * cappedDelay;
 
  return jitter;
}

이 방식은 0 ~ cappedDelay 사이에서 랜덤한 값을 사용한다

예를 들어 계산된 backoff 값이 8000ms라면 실제 delay는 0ms부터 8000ms 사이에서 랜덤하게 정해진다

이런 방식을 보통 full jitter 라고 부르기도 한다

조금 더 보수적으로 하고 싶다면 기준 delay 근처에서만 흔들 수도 있다

const baseDelay = 1000;
const maxDelay = 30000;
const jitterRatio = 0.2;
 
function getBackoffDelayWithSmallJitter(retryCount: number) {
  const exponentialDelay = baseDelay * Math.pow(2, retryCount);
  const cappedDelay = Math.min(exponentialDelay, maxDelay);
  const jitterRange = cappedDelay * jitterRatio;
  const jitter = Math.random() * jitterRange;
 
  return cappedDelay + jitter;
}

이 경우에는 계산된 delay에 최대 20% 정도의 랜덤 지연을 더한다

언제 사용하면 좋을까?

exponential backoff는 일시적인 실패가 자연스럽게 회복될 수 있는 작업에 잘 맞는다

예를 들면

  • HTTP API 요청 재시도
  • DB 연결 재시도
  • 메시지 발행 재시도
  • 외부 서비스 호출 재시도
  • rate limit 응답 이후 재시도
  • WebSocket 재연결

반대로 재시도해도 성공 가능성이 없는 실패에는 맞지 않는다

예를 들어

  • 인증 토큰이 잘못됨
  • 요청 파라미터가 잘못됨
  • 존재하지 않는 리소스를 요청함
  • 권한이 없음

이런 오류는 오래 기다렸다가 다시 보내도 성공하지 않을 가능성이 크다

그래서 재시도할 오류와 바로 실패시킬 오류를 구분해야 한다

일시적인 오류인가? -> retry 가능
요청 자체가 잘못됐는가? -> retry 하지 않는 편이 좋음

정리

exponential backoff는 실패할수록 재시도 간격을 지수적으로 늘리는 방식이다

1초 -> 2초 -> 4초 -> 8초 -> 16초

핵심은 이렇다

  • 실패 직후 바로 계속 재시도하지 않음
  • 실패가 반복될수록 더 오래 기다림
  • 서버나 네트워크가 회복할 시간을 줌
  • 보통 max delay로 최대 대기 시간을 제한함
  • 여러 클라이언트가 동시에 몰리지 않도록 jitter를 같이 쓰는 경우가 많음

한줄로 정리하면

exponential backoff = 실패할수록 재시도 대기 시간을 2배처럼 점점 크게 늘리는 전략