재시도 폭풍
고친 사람 github-actions[bot]
재시도 폭풍은 실패를 넘기려고 넣은 재시도가 되레 그 실패를 키우는 고장입니다. 요청을 받는 쪽이 느려져 실패가 나면 보내는 쪽이 같은 요청을 다시 보냅니다. 그 재시도가 받는 쪽을 더 누릅니다. 이 고리가 한번 닫히면 밖에서 들어오는 요청이 줄어든 뒤에도 시스템이 저 혼자서는 못 일어납니다.
쉽고 빠른 이해
재시도 폭풍은 실패한 요청을 다시 보내는 일이 쌓여 그 실패를 더 키우는 고장입니다. 데이터베이스가 느려져 요청 하나가 실패했다고 합시다. 그 앞에 선 세 계층이 저마다 세 번까지 보내면 데이터베이스에는 요청이 스물일곱 번 닿습니다.
이 고장에 따로 이름이 붙은 까닭은 부하를 만든 쪽이 사용자가 아니라 우리 시스템 자신이기 때문입니다. 들어오는 요청을 막아 세워도 재시도는 안에서 계속 돌아서 안 걷힙니다.
어떻게 도나:
- 받는 쪽이 느려져 요청이 정해 둔 시간 안에 안 끝나고 실패로 처리됩니다
- 보내는 쪽이 그 실패를 잠깐 지나가는 실패로 보고 다시 보냅니다
- 늘어난 요청이 받는 쪽을 더 느리게 만들어 1번으로 돌아갑니다
무엇이 나빠지나 — 성공하는 요청은 줄어듭니다. 받는 쪽이 하는 일은 오히려 늘어납니다. 대부분이 답을 못 받고 버려질 요청이라 장비는 헛일로 가득 차고, 부하가 걷힌 뒤에도 회복이 늦습니다.
상세
재시도 폭풍은 원인과 결과가 서로를 키우는 고리입니다. 재시도가 만든 부하가 받는 쪽을 더 느리게 만듭니다. 느려진 만큼 실패가 늘어 재시도가 또 생깁니다. 이 편에서 「보내는 쪽」은 요청을 내는 서비스를, 「받는 쪽」은 그 요청을 처리하는 서비스나 데이터베이스를 뜻합니다.
고리가 닫히는 과정을 먼저 봅니다. 그다음 폭풍이 되는 조건에서 고리를 끊는 수단까지 내려갑니다.
고리가 닫히는 순간
재시도 하나하나는 잘못이 없습니다. 재시도는 잠깐 지나가는 실패를 사람 손 안 거치고 넘기려고 넣는 장치입니다. 받는 쪽이 멀쩡할 때는 제 일을 합니다.
문제는 받는 쪽이 느려졌을 때입니다. 요청이 타임아웃에 걸려 실패로 끝납니다. 타임아웃은 정해 둔 시간 안에 답이 안 오면 기다리기를 그만두는 규칙입니다. 보내는 쪽은 이 실패를 잠깐 지나가는 실패로 읽고 같은 요청을 다시 보냅니다.
flowchart TD
A["받는 쪽이 느려진다"] --> B["요청이 정해 둔 시간 안에 안 끝난다"]
B --> C["보내는 쪽이 같은 요청을 다시 보낸다"]
C --> D["받는 쪽에 닿는 요청 수가 늘어난다"]
D --> A
마지막 화살표가 첫 칸으로 돌아가는 것이 이 고리입니다. 늘어난 요청이 원인을 다시 키웁니다. 한 바퀴 돌 때마다 부하가 커지므로 밖에서 끊어 주지 않으면 저절로 잦아들지 않습니다.
여기에 하나가 더 얹힙니다. 시간이 다 돼 버린 요청도 받는 쪽에서는 이미 돌고 있는 일입니다. 보내는 쪽이 기다리기를 그만둬도 받는 쪽은 그 계산을 끝까지 합니다. 그렇게 아무도 안 받아 갈 답이 하나 만들어집니다.
버려진 요청과 새 재시도가 받는 쪽에서 어떻게 겹치는지를 시간 순으로 보면 이렇습니다.
sequenceDiagram
participant S as 보내는 쪽
participant R as 받는 쪽
S->>R: 요청
Note over R: 계산을 시작한다
Note over S: 정해 둔 시간이 지나 기다리기를 그만둔다
S->>R: 같은 요청을 다시 보낸다
Note over R: 앞 요청과 새 요청이 함께 돈다
Note over R: 앞 요청이 만든 답은 아무도 안 받아 간다
보내는 쪽이 세는 요청은 한 번에 하나입니다. 받는 쪽이 지고 있는 일은 그보다 많습니다.
계층마다 곱해지는 시도
요청은 대개 서비스 하나만 지나지 않습니다. 밖에서 들어온 요청을 맨 앞에서 받아 안쪽으로 넘기는 서버를 게이트웨이라고 합니다. 게이트웨이가 주문 서비스를 부르고, 주문 서비스가 결제 서비스를 부르고, 결제 서비스가 데이터베이스를 봅니다. 계층마다 재시도를 따로 넣어 두면 시도 횟수가 더해지지 않고 곱해집니다.
flowchart TD
U["사용자 요청 1"] --> A
subgraph A["게이트웨이 · 내보내는 요청 3"]
A1["주문 서비스 호출"]
A2["나머지 2갈래"]
end
A1 --> B
subgraph B["주문 서비스 · 한 갈래가 다시 3 · 합쳐 9"]
B1["결제 서비스 호출"]
B2["나머지 2갈래"]
end
B1 --> C
subgraph C["결제 서비스 · 한 갈래가 다시 3 · 합쳐 27"]
C1["데이터베이스 호출"]
C2["나머지 2갈래"]
end
그림은 층마다 한 갈래만 펼친 것입니다. 접어 둔 두 갈래도 펼친 갈래와 똑같은 모양으로 아래가 갈라집니다.
| 계층 | 이 계층이 정한 시도 횟수 | 이 계층이 내보내는 요청 수 |
|---|---|---|
| 게이트웨이 | 3 | 3 |
| 주문 서비스 | 3 | 9 |
| 결제 서비스 | 3 | 27 |
사용자가 보낸 요청은 하나입니다. 데이터베이스에는 그 요청이 스물일곱 번 닿습니다. 계층이 하나 늘 때마다 이 수가 다시 세 배가 됩니다.
이 배수는 부하가 가장 높은 때에 걸립니다. 평소에는 실패가 드물어 한 번에 끝나므로 배수가 안 보입니다. 받는 쪽이 흔들리기 시작해야 모든 계층이 동시에 재시도에 들어갑니다. 그때 부하가 한꺼번에 스물일곱 배가 됩니다.
폭풍이 되는 조건
넷이 다 서면 재시도 폭풍이 됩니다. 앞의 셋은 지금까지 본 고리 그대로입니다. 새로 더해지는 것은 넷째 하나입니다.
- 받는 쪽이 감당할 양보다 많은 요청이 들어와 응답이 느려집니다
- 보내는 쪽이 그 느려짐을 다시 하면 나아질 실패로 보고 재시도합니다
- 늘어난 재시도가 받는 쪽을 더 느리게 만듭니다
- 재시도 총량을 초당 몇 번까지로 묶는 총량 상한이 없습니다
둘째가 이 고장과 그냥 과부하를 가릅니다. 받는 쪽이 늦게 답하는 것을 보내는 쪽이 「지금 바쁘니 나중에」로 읽으면 다시 안 보내고 실패를 위로 올립니다. 「잠깐 끊긴 것」으로 읽을 때만 재시도가 나갑니다.
총량 상한이 있으면 고리가 안 닫힙니다. 다만 한 요청의 시도 횟수 상한은 총량 상한이 아닙니다. 한 요청을 세 번으로 묶어 두어도 요청 수가 열 배로 늘면 재시도도 열 배로 늘어 총량은 그대로 안 막힙니다.
재시도가 한때에 겹치는 까닭
폭풍은 고르게 퍼지지 않습니다. 재시도 시각이 서로 겹쳐서 봉우리가 생깁니다. 같은 사고로 한꺼번에 실패한 요청들은 같은 규칙으로 같은 시간을 기다렸다가 같은 때 돌아옵니다.
지수 백오프를 넣어도 이 겹침은 안 풀립니다. 지수 백오프는 실패가 거듭될수록 기다리는 시간을 배로 늘려 재시도 사이를 벌리는 방법입니다. 늘리는 규칙이 모두 같으면 기다리는 시간도 모두 같아집니다.
기다리는 시간을 배로 늘리는 흔한 재시도 반복문입니다.
for i in range(3):
ok = call() # 거짓 · 또 실패
if ok: break
sleep(2 ** i) # 1 · 2 · 4초
오른쪽 주석은 실패가 이어질 때 이 반복문이 쉬는 시간입니다. 같은 순간에 실패한 요청이 천 건이라면 천 건이 모두 1초 뒤에, 다시 2초 뒤에, 다시 4초 뒤에 함께 돌아옵니다. 천 건이 같은 세 시각에 겹칩니다.
겹침을 푸는 것이 지터입니다. 지터는 기다리는 시간에 무작위 값을 섞어 서로 다른 때 돌아오게 만드는 방법입니다. 봉우리를 낮추기는 하지만 재시도 총량을 줄이지는 않으므로, 지터만으로 폭풍이 그치지는 않습니다.
부하가 걷힌 뒤에도 남는 고리
보통의 과부하는 원인이 걷히면 풀립니다. 요청이 몰려 느려진 것이면 그 몰림이 빠질 때 같이 잦아듭니다.
재시도 폭풍은 다릅니다. 부하를 만드는 쪽이 밖이 아니라 시스템 안이라, 들어오는 요청을 다 막아 세워도 이미 쌓인 재시도가 계속 돕니다. 재시도가 실패하면 또 재시도가 되므로 고리가 스스로 먹이를 댑니다.
시스템이 어느 상태에서 어느 상태로 옮겨 가는지를 그리면 이렇습니다.
stateDiagram-v2
state "재시도 폭풍" as 폭풍
[*] --> 정상
정상 --> 포화: 요청이 감당할 양을 넘는다
포화 --> 폭풍: 실패를 본 쪽이 다시 보낸다
폭풍 --> 폭풍: 재시도가 실패해 또 재시도가 된다
폭풍 --> 폭풍: 들어오는 요청을 막아 세워도 그대로다
폭풍 --> 정상: 재시도를 멈춰 세운다
폭풍에서 빠져나가는 화살표는 하나뿐입니다. 들어오는 요청을 막는 화살표는 폭풍으로 되돌아옵니다.
그래서 복구가 처음 무너질 때보다 어렵습니다. 부하를 원래 수준으로 되돌리는 것으로는 모자랍니다. 재시도를 밖에서 멈춰 세워 고리를 한 번 끊어야 합니다. 트래픽을 끊었는데도 안 살아나는 장애라면 이 고리를 의심할 대목입니다.
썬더링 허드·연쇄 장애와 갈리는 선
이름이 나란히 불리는 고장 셋은 「무엇이 몰리나」에서 갈립니다.
| 고장 | 몰리는 것 | 몰리게 만드는 것 |
|---|---|---|
| 재시도 폭풍 | 같은 요청의 되풀이 | 실패를 본 뒤의 다시 보내기 |
| 썬더링 허드 | 한꺼번에 깨어난 대기자 | 사건 하나가 낸 깨우기 |
| 연쇄 장애 | 옆으로 번지는 실패 | 무너진 쪽이 옆에 넘긴 부하 |
가운데 칸이 셋을 가릅니다. 썬더링 허드는 깨우기가 몰리는 것이고 재시도 폭풍은 다시 보내기가 몰리는 것입니다. 깨어난 대기자는 헛걸음한 뒤 다시 잠들면 끝나지만, 재시도는 실패할 때마다 다음 시도를 낳습니다.
연쇄 장애는 범위가 더 넓습니다. 재시도 폭풍은 연쇄 장애가 번지는 통로 가운데 하나입니다. 한 서비스가 느려진 것을 앞 계층의 재시도가 부하로 바꿔 돌려주면, 그 부하가 앞 계층까지 무너뜨리며 위로 올라갑니다.
폭풍을 알아채는 신호
한 지점의 수치만 보면 응답이 늘어진 서비스로만 보입니다. 보내는 쪽과 받는 쪽에서 센 요청 수를 나란히 놓아야 드러납니다.
| 보는 것 | 재시도 폭풍일 때 |
|---|---|
| 사용자가 보낸 요청 수 | 거의 안 변한다 |
| 받는 쪽이 받은 요청 수 | 몇 배로 뛴다 |
| 성공한 요청의 비율 | 떨어진다 |
| 답을 못 받고 버려지는 요청 | 늘어난다 |
첫 두 줄이 가장 또렷한 신호입니다. 밖에서 들어온 요청은 안 늘었습니다. 안에서 도는 요청만 몇 배가 됐다면 그 차이를 만든 것은 재시도입니다.
받는 쪽 장비만 보면 놓칩니다. 들어온 요청을 쉬지 않고 처리하고 있어서 장비 사용률 그래프는 오히려 건강해 보입니다. 그 일의 대부분이 아무도 안 받아 갈 답을 만드는 헛일입니다.
고리를 끊는 수단
고치는 길은 재시도에 총량 상한을 씌우거나 재시도를 아예 멈춰 세우는 것입니다. 이름 있는 수단만 짚고 넘어갑니다.
| 수단 | 무엇을 막나 |
|---|---|
| 재시도를 한 계층에만 둔다 | 계층마다 곱해지는 시도 |
| 재시도 예산 | 재시도가 전체 요청에서 차지하는 몫이 커지는 것 |
| 서킷 브레이커 | 받는 쪽이 무너진 뒤에도 계속 보내는 것 |
| 지터를 섞은 지수 백오프 | 재시도가 한때에 겹치는 것 |
| 부하 차단 | 어차피 못 끝낼 요청을 받아 장비를 쓰는 것 |
| 멱등성 보장 | 겹쳐 닿은 요청이 일을 두 번 하는 것 |
재시도 예산은 전체 요청 가운데 재시도가 차지할 몫을 미리 정해 두는 방법입니다. 그 몫을 다 쓰면 다시 보내지 않고 실패를 위로 올립니다. 한 요청의 시도 횟수 상한이 아니라 총량 상한이라는 것이 다른 점입니다.
서킷 브레이커는 실패가 잦아지면 잠깐 호출을 아예 안 보내고 바로 실패를 돌려주는 장치입니다. 받는 쪽이 이미 무너진 뒤에도 계속 보내는 것을 막습니다.
부하 차단은 감당할 양을 넘는 요청을 큐에 안 넣고 즉시 거절하는 방법입니다. 어차피 못 끝낼 요청에 장비를 쓰지 않게 합니다. 둘 다 고리에 들어가는 요청 수를 줄이는 쪽이지 실패를 없애는 수단이 아닙니다.
마지막 줄만 성격이 다릅니다. 멱등성은 같은 요청이 여러 번 닿아도 결과가 한 번 한 것과 같아지는 성질입니다. 앞의 다섯은 폭풍이 이는 것을 막고, 멱등성은 폭풍이 지나가도 데이터가 안 망가지게 합니다.
어느 수단을 고르든 재는 잣대는 하나입니다. 받는 쪽이 느려졌을 때 그 쪽에 닿는 요청 수가 줄어드느냐입니다.
관련 항목
재시도 폭풍을 일으키는 재시도 장치
재시도 · 백오프 · 지수 백오프 · 지터 · 타임아웃 · 재전송
재시도 폭풍의 고리를 끊는 수단
서킷 브레이커 · 재시도 예산 · 부하 차단 · 속도 제한 · 스로틀링 · 벌크헤드 · 데드 레터 큐
재시도 폭풍과 함께 불리는 몰림 고장
썬더링 허드 · 캐시 스탬피드 · 연쇄 장애 · 라이브락 · 락 경합 · 헤드 오브 라인 블로킹
재시도가 두 번 닿아도 안전하게 만드는 성질
멱등성 · 멱등 키 · 중복 처리 · 최소 한 번 전달 · 정확히 한 번 실행
재시도 폭풍이 번지는 시스템 구조
분산 시스템 · 마이크로서비스 · 게이트웨이 · 로드 밸런서 · 커넥션 풀 · 큐
재시도 폭풍을 알아채는 데 보는 지표
처리량 · 오류율 · 꼬리 지연 · 포화 · 골든 시그널 · 관측성 · 모니터링
받는 쪽이 감당할 양을 정하는 활동
용량 계획 · 부하 테스트 · 성능 테스트 · 서비스 수준 목표 · 오류 예산
장애가 끝난 뒤 밟는 복구·검토 절차
다른 이름: retry storm