지수 백오프
고친 사람 github-actions[bot]
지수 백오프는 요청이 실패할 때마다 다음 시도까지 기다리는 시간을 배로 늘립니다. 처음에는 짧게 기다려 금방 풀리는 실패를 빨리 넘깁니다. 실패가 거듭되면 간격을 크게 벌려 서버가 회복할 틈을 줍니다. 부르는 쪽이 스스로 요청을 줄이는 셈이라 재시도가 장애를 키우는 것을 막습니다.
쉽고 빠른 이해
지수 백오프는 다시 시도하기 전에 얼마나 기다릴지 정하는 규칙입니다. 1초를 기다렸다 또 실패하면 다음에는 2초, 그다음에는 4초를 기다립니다.
간격을 안 늘리면 실패한 요청이 곧바로 되돌아옵니다. 버거워서 거절한 서버에 같은 양이 다시 밀려듭니다. 회복할 틈이 사라집니다. 기다리는 시간을 늘리는 것은 그 틈을 만들어 주는 일입니다.
어떻게 도는가:
- 첫 실패 뒤에는 짧게 기다렸다가 다시 보냅니다
- 실패가 이어질 때마다 기다리는 시간에 같은 수를 곱합니다
- 정해 둔 상한에 닿으면 더 늘리지 않고, 횟수나 전체 시간을 다 쓰면 포기합니다
다시 해서 성공할 수 있는 실패에만 씁니다. 권한이 없어 거절당한 요청은 몇 번을 보내도 같은 답을 받습니다.
대가가 있습니다. 실패가 몇 번 이어지면 부르는 쪽이 결과를 한참 뒤에 받습니다. 서버가 이미 회복한 뒤에도 다음 시도까지 계속 기다리기도 합니다.
상세
지수 백오프는 기다리는 시간을 실패 횟수로 정하는 계산 규칙입니다. 처음 기다릴 시간을 하나 정해 두고, 실패가 한 번 늘 때마다 거기에 같은 수를 곱합니다. 1초에서 시작해 두 배씩 곱하면 대기 시간이 1초, 2초, 4초, 8초로 벌어집니다.
무엇을 넣고 무엇이 나오는지를 보면 셈은 단순합니다. 들어가는 값은 지금까지 실패한 횟수입니다. 나오는 값은 다음 시도까지 기다릴 시간입니다. 시도가 하나 늘 때마다 대기 시간이 배로 커지므로 몇 번만 실패해도 간격이 크게 벌어집니다.
간격을 늘리는 이유
재시도는 실패한 요청을 한 번 더 보내는 일입니다. 연결이 순간 끊겼거나 서버가 잠시 바빠서 난 실패는 조금 뒤에 다시 하면 대개 성공합니다. 시간이 지나면 저절로 풀리는 이런 실패를 일시적 장애라고 합니다.
문제는 다시 보내는 때입니다. 실패한 직후에 곧바로 다시 보내면 서버는 아까와 같은 상태 그대로입니다. 버거워서 거절한 서버라면 두 번째 요청도 같은 이유로 거절합니다. 성공할 가망이 없는 요청이 서버를 한 번 더 두드리는 셈입니다.
부르는 쪽이 여럿이면 더 나빠집니다. 열 대가 각자 실패하고 각자 곧바로 다시 보내면 서버가 받는 양은 줄지 않습니다. 회복하려면 들어오는 양이 줄어야 합니다. 재시도는 그 감소를 없애 버립니다.
재시도 폭풍은 재시도가 장애를 키워 스스로 못 빠져나오는 상태입니다. 네 걸음이 고리로 이어져 시작한 데로 돌아옵니다.
flowchart TD
A["서버가 버겁다"] --> B["요청을 거절한다"]
B --> C["부르는 쪽이 곧바로 다시 보낸다"]
C --> D["들어오는 양이 안 준다"]
D --> A
C -->|"간격을 늘리면"| E["들어오는 양이 준다"]
E --> F["서버가 밀린 일을 처리한다"]
고리는 스스로 끊기지 않습니다. 끊는 것은 기다리는 시간입니다.
실패가 이어질수록 부르는 쪽이 보내는 양이 저절로 줄어듭니다. 서버는 그만큼 밀린 일을 처리할 틈을 얻습니다.
대기 시간이 늘어나는 모양
같은 다섯 번을 시도해도 간격을 정하는 규칙에 따라 기다리는 시간이 갈립니다.
| 실패 횟수 | 고정 간격 | 선형 증가 | 지수 백오프 |
|---|---|---|---|
| 1 | 1초 | 1초 | 1초 |
| 2 | 1초 | 2초 | 2초 |
| 3 | 1초 | 3초 | 4초 |
| 4 | 1초 | 4초 | 8초 |
| 5 | 1초 | 5초 | 16초 |
지수 백오프만 실패가 늘수록 간격이 빠르게 벌어집니다. 다섯 번 실패하는 동안 고정 간격은 다 합쳐 5초를 기다립니다. 지수 백오프는 31초를 기다립니다.
간격이 배로 커지니 시도 횟수는 잘 늘지 않습니다. 기다리는 시간을 다 합쳐 한 시간을 쓴다 해도 시도는 열몇 번뿐입니다. 기다리는 시간을 열 배로 늘려도 시도는 서너 번 느는 데 그칩니다.
코드로 본 규칙
값 셋을 먼저 정해 두면 한 번의 대기 시간은 아래 세 줄로 나옵니다.
base = 1 # 처음 기다릴 시간 1초
cap = 30 # 대기 시간 상한 30초
attempt = 10 # 열 번째 실패
wait = base * 2 ** attempt # 1024초
wait = min(wait, cap) # 30초로 잘린다
wait = uniform(0, wait) # 0~30초 사이
sleep(wait)
attempt 는 지금까지 실패한 횟수입니다. base 는 처음 기다릴 시간입니다. 첫 곱셈 줄이 간격을 벌리는 대목입니다.
cap 은 간격이 끝없이 늘어나지 않게 막는 상한입니다. 열 번쯤 실패하면 곱셈만으로는 대기 시간이 몇십 분까지 갑니다. 그때부터는 다시 시도하는 값이 없어집니다.
uniform 은 두 값 사이에서 무작위로 하나를 고르는 함수입니다. 계산한 시간을 그대로 쓰지 않고 0과 그 시간 사이의 아무 값이나 골라 기다립니다. 왜 그렇게 하는지는 아래 「같은 순간에 몰리는 것」이 받습니다.
멈추는 조건
간격만 늘리면 재시도가 끝나지 않습니다. 언제 포기할지를 함께 정해야 합니다. 멈추는 조건은 대개 셋입니다.
- 대기 시간의 상한 — 간격이 이 값에 닿으면 더 늘리지 않습니다. 그 값으로 계속 기다립니다
- 시도 횟수의 상한 — 정해 둔 횟수를 다 쓰면 그만둡니다. 마지막 실패를 그대로 부르는 쪽에 올립니다
- 전체 시간 예산 — 첫 요청부터 잰 시간이 정해 둔 값을 넘으면 그만둡니다. 몇 번을 시도했든 상관없습니다
앞의 하나는 간격이 커지는 것을 잡아 두는 조건입니다. 뒤의 둘은 재시도 자체를 끝내는 조건입니다.
셋을 한 번의 호출 흐름으로 이으면 다음과 같습니다.
flowchart TD
A["요청을 보낸다"] --> B{"성공했나"}
B -->|성공| C["끝낸다"]
B -->|실패| D{"다시 해 볼 실패인가"}
D -->|아니다| E["실패로 끝낸다"]
D -->|맞다| F{"횟수와 전체 시간이 남았나"}
F -->|아니다| E
F -->|남았다| G["대기 시간을 배로 늘린다(상한까지)"]
G --> H["그 시간만큼 기다린다"]
H --> A
갈림이 둘이라는 것이 이 그림의 요점입니다. 다시 해 볼 실패인지를 먼저 묻고, 그다음에 남은 횟수와 시간을 묻습니다. 앞의 물음을 건너뛰면 고쳐지지 않는 실패를 상한까지 되풀이하게 됩니다.
같은 순간에 몰리는 것
지수 백오프는 부르는 쪽 하나를 놓고 보면 잘 돕니다. 여럿이 같은 규칙을 쓰면 다른 문제가 생깁니다.
서버 하나가 잠깐 멈추면 그 서버를 부르던 쪽이 한꺼번에 실패합니다. 모두 같은 규칙으로 1초를 기다리면 1초 뒤에 다 같이 돌아옵니다. 2초 뒤에 또 다 같이 돌아옵니다.
간격은 벌어졌습니다. 몰리는 것은 그대로입니다. 썬더링 허드는 여럿이 같은 순간에 깨어나 한꺼번에 덤비는 것입니다.
부르는 쪽 셋을 가·나·다로 두겠습니다. 셋이 같은 규칙을 쓰면 깨어나는 때가 겹칩니다.
sequenceDiagram
participant 가
participant 나
participant 다
participant 서버
Note over 가,다: 셋 다 1.0초를 기다린다
가->>서버: 1.0초에 다시 보낸다
나->>서버: 1.0초에 다시 보낸다
다->>서버: 1.0초에 다시 보낸다
서버-->>가: 거절
서버-->>나: 거절
서버-->>다: 거절
그래서 계산한 대기 시간을 그대로 쓰지 않습니다. 지터는 거기에 섞는 무작위 값입니다. 0과 계산한 시간 사이에서 골라 쓰면 부르는 쪽마다 돌아오는 때가 달라집니다.
sequenceDiagram
participant 가
participant 나
participant 다
participant 서버
Note over 가,다: 0과 1.0초 사이에서 각자 고른다
가->>서버: 0.3초에 다시 보낸다
서버-->>가: 처리한다
나->>서버: 0.7초에 다시 보낸다
서버-->>나: 처리한다
다->>서버: 1.0초에 다시 보낸다
서버-->>다: 처리한다
Note over 서버: 한 순간에 솟던 봉우리가 낮아진다
지터는 몰리는 때를 흩을 뿐 요청의 총량을 줄이지는 않습니다. 총량을 줄이는 것은 간격을 늘리는 일입니다. 둘은 함께 써야 각자의 몫을 합니다.
써도 되는 실패와 안 되는 실패
다시 해서 성공할 수 있는 실패에만 씁니다. 필수 값이 빠졌거나 권한이 없는 요청은 백 번을 보내도 백 번 거절당합니다. 이런 실패를 되풀이하면 서버만 바빠집니다. 부르는 쪽은 오류를 늦게 봅니다.
같은 요청이 두 번 처리돼도 탈이 없어야 한다는 조건도 붙습니다. 응답을 못 받았다고 해서 서버가 일을 안 한 것은 아닙니다. 요청이 닿은 뒤에 응답만 잃어버렸다면 재시도는 같은 일을 한 번 더 시킵니다.
응답만 없어진 경우를 순서로 보면 다음과 같습니다.
sequenceDiagram
participant 호출 as 부르는 쪽
participant 서버
호출->>서버: 요청
Note over 서버: 처리를 끝냈다
서버--x호출: 응답이 유실된다
호출->>서버: 같은 요청을 다시 보낸다
Note over 서버: 같은 일을 한 번 더 한다
여러 번 해도 결과가 한 번 한 것과 같은 성질을 멱등성이라고 합니다. 멱등성이 없으면 재시도는 중복 처리를 만듭니다.
상황에 따라 값을 어떻게 잡을지는 갈립니다.
| 상황 | 어떻게 잡나 |
|---|---|
| 사람이 응답을 기다리는 요청 | 상한을 짧게, 횟수를 적게. 오래 기다리느니 실패를 빨리 보인다 |
| 뒤에서 도는 배치 처리나 큐 소비 | 상한을 길게. 오래 걸려도 끝내 성공시킨다 |
| 서버가 다시 올 때를 알려 준 응답 | 계산한 값 대신 알려 준 시간을 쓴다 |
| 이미 무너지는 중인 서버 | 백오프만으로는 부족하다. 호출을 끊는 장치를 함께 둔다 |
마지막 줄은 지수 백오프의 한계를 말합니다. 부르는 쪽이 아무리 간격을 벌려도 서버가 버틸 수 없는 양이 계속 오고 있다면 회복이 안 됩니다. 실패가 이어질 때 아예 호출을 끊어 버리는 서킷 브레이커나, 재시도에 쓸 수 있는 양을 따로 정해 두는 재시도 예산 같은 장치가 그 몫을 맡습니다.
이미 이 규칙이 들어가 있는 곳
여러 대가 선 하나를 나눠 쓰는 이더넷에서 두 대가 동시에 보내면 신호가 부딪칩니다. 부딪친 쪽은 각자 무작위로 고른 시간만큼 기다렸다 다시 보냅니다. 부딪침이 거듭될수록 그 무작위 값을 고르는 범위를 배로 넓힙니다. 지수 백오프와 지터가 함께 들어가 있는 꼴입니다.
재전송에서도 같은 규칙이 돕니다. 보낸 쪽은 받은 쪽이 잘 받았다고 알려 주는 확인 응답을 기다립니다. 정해진 시간 안에 그 응답이 안 오면 같은 데이터를 다시 보냅니다.
다시 보낼 때마다 기다리는 시간을 배로 늘립니다. 길이 막힌 채로 같은 간격으로 계속 밀어 넣으면 막힘이 풀리지 않기 때문입니다.
관련 항목
지수 백오프를 품고 도는 재시도 절차
재시도 · 백오프 · 자동 재시도 · 재시도 예산 · 타임아웃 · 데드라인 · 폴백
지수 백오프와 함께 쓰는 몰림 방지 수단
지터 · 서킷 브레이커 · 속도 제한 · 스로틀링 · 백프레셔 · 부하 흘리기 · 벌크헤드
지수 백오프로 누그러뜨리는 몰림 장애
재시도 폭풍 · 썬더링 허드 · 캐시 스탬피드 · 연쇄 장애 · 라이브락 · 혼잡 붕괴
지수 백오프를 안전하게 만드는 전제
멱등성 · 일시적 장애 · 부분 실패 · 중복 요청 · 재시도 안전 · 네트워크 분단
대기 시간을 정하는 다른 규칙
고정 간격 재시도 · 선형 백오프 · 적응형 백오프 · Retry-After · 토큰 버킷 · 누수 버킷
지수 백오프가 이미 박혀 있는 프로토콜
이더넷 · 재전송 · 재전송 타임아웃 · 확인 응답 · 혼잡 제어 · DNS
지수 백오프가 기준으로 삼는 시간 값
다른 이름: exponential backoff · 지수적 백오프