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^retryCountbaseDelay가 1초라면
| retryCount | 계산 | 대기 시간 |
|---|---|---|
| 0 | 1초 * 2^0 | 1초 |
| 1 | 1초 * 2^1 | 2초 |
| 2 | 1초 * 2^2 | 4초 |
| 3 | 1초 * 2^3 | 8초 |
| 4 | 1초 * 2^4 | 16초 |
간단한 코드 예시
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배처럼 점점 크게 늘리는 전략