リトライが障害を大きくする瞬間
한국어 원문으로 표시합니다.
한 줄 요약
재시도는 일시적 실패를 가린다. 그런데 백엔드가 과부하일 때 재시도를 켜 두면 부하를 두세 배로 키워 장애를 완성시킨다. 그래서 재시도에는 항상 상한과 예산이 필요하다.
왜 이게 필요했나
백엔드 하나가 느려진다 → 타임아웃 → 재시도 → 부하 2배 → 더 느려진다 → 재시도 → …
이 되먹임을 재시도 폭풍(retry storm)이라 한다. 메시가 서비스마다 사이드카를 두므로, 3단 호출 체인에서 각 단이 3번씩 재시도하면 맨 끝 서비스는 27배를 맞는다. 재시도는 곱해진다.
막는 장치는 셋이고, 각각 다른 문제를 푼다.
| 장치 | 무엇을 보나 | 무엇을 하나 |
|---|---|---|
| 재시도 예산 | 전체 요청 대비 재시도 비율 | 재시도가 일정 비율을 넘으면 더 안 한다 |
| 서킷 브레이커 | 동시 연결·대기 요청 수 | 한도를 넘으면 즉시 실패시킨다(빨리 죽는다) |
| 이상치 감지 | 엔드포인트별 연속 실패 | 나쁜 엔드포인트를 잠시 뺀다 |
어떻게 동작하나
서킷 브레이커는 이름과 달리 "차단기" 라기보다 한도다.
# DestinationRule
trafficPolicy:
connectionPool:
tcp: { maxConnections: 100 }
http:
http1MaxPendingRequests: 20 # 커넥션을 기다리는 요청 상한
maxRequestsPerConnection: 100
한도를 넘은 요청은 기다리지 않고 즉시 503 을 받는다. 잔인해 보이지만 이게 옳다 — 기다리게 하면 클라이언트의 스레드까지 묶여 장애가 위로 번진다. 빨리 실패하는 편이 전체 시스템에 낫다.
이상치 감지는 로드밸런싱 풀에서 나쁜 놈을 빼는 장치다.
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50 # 절반 넘게는 절대 빼지 않는다
maxEjectionPercent 가 중요하다. 이게 없으면 백엔드 전체가 잠깐 나빠졌을 때 엔드포인트를 전부 빼 버려 서비스가 통째로 죽는다. 감지 장치가 장애를 만드는 것이다.
흔한 착각
"재시도는 켜 두면 좋다" — 멱등하지 않은 요청(POST 로 결제, 주문)에 재시도를 걸면 중복이 생긴다. Envoy 의 retryOn 기본값은 안전한 조건만 담지만, 5xx 를 넣는 순간 서버가 처리하다 실패한 요청도 다시 보낸다.
타임아웃 없는 재시도. 재시도 3번에 각각 30초 타임아웃이면 최악의 경우 90초를 기다린다. 그 사이 사용자는 이미 떠났고 커넥션만 묶여 있다. 전체 시간 상한(timeout)을 재시도와 함께 정해야 한다.
재시도 예산이라는 안전장치
재시도를 켤 때 반드시 함께 거는 것이 예산(retry budget) 입니다. "전체 요청의 몇 %까지만 재시도로 쓴다" 를 정하면, 하류가 무너져도 재시도 트래픽이 폭주하지 않습니다.
# Envoy — 클러스터 단위 재시도 예산
circuit_breakers:
thresholds:
- priority: DEFAULT
max_retries: 3 # 동시에 진행 중인 재시도 상한
retry_policy:
retry_on: 5xx,reset,connect-failure
num_retries: 2
per_try_timeout: 1s
retry_back_off:
base_interval: 0.025s
max_interval: 1s
max_retries 는 동시 재시도 수의 상한 이라 재시도 폭풍을 물리적으로 막습니다.
이것 없이 num_retries: 3 만 두면, 하류가 느려지는 순간 요청 하나가 넷이 되어
부하가 네 배로 뜁니다. 무너지고 있는 서비스에 네 배를 보내는 것입니다.
per_try_timeout 도 중요합니다. 이것이 없으면 전체 타임아웃 안에서 첫 시도가
시간을 다 써 재시도할 여유가 없습니다. 전체 타임아웃 ≥ per_try × (재시도 + 1)
이 성립해야 합니다.
재시도해도 되는 것과 안 되는 것
| 조건 | 재시도 | 이유 |
|---|---|---|
| 연결 실패, TCP reset | ✅ | 서버가 요청을 받지도 못했다 |
| 503, 504 | ✅ | 대개 처리 전에 거절 |
| 500 | ⚠️ | 처리 중에 실패했을 수 있다 |
| 타임아웃 | ⚠️ | 서버는 처리를 마쳤을 수 있다 |
| 4xx | ❌ | 다시 보내도 같다 |
타임아웃 재시도가 가장 위험합니다. 결제 요청이 5초에 타임아웃 났는데 서버는
6초에 완료했다면, 재시도로 두 번 결제 됩니다. 그래서 비멱등 요청에는
retry_on 에서 타임아웃을 빼거나, 멱등 키를 반드시 씁니다.
Envoy 는 x-envoy-retry-on 헤더로 요청마다 다르게 지정할 수 있어, 조회는
공격적으로 쓰기는 보수적으로 둘 수 있습니다.
이상값 탐지로 아픈 인스턴스를 빼기
재시도만으로는 특정 인스턴스가 아픈 경우 를 못 고칩니다. 재시도가 같은 아픈 곳으로 갈 수 있기 때문입니다. 이상값 탐지(outlier detection)가 그것을 뺍니다.
outlier_detection:
consecutive_5xx: 5 # 연속 5번 5xx 면
base_ejection_time: 30s # 30초 뺀다
max_ejection_percent: 50 # 다만 절반 이상은 못 뺀다
interval: 10s
max_ejection_percent 가 안전장치입니다. 전체가 동시에 아플 때(공통 DB 장애)
모두 빼 버리면 서비스가 완전히 죽습니다. 절반은 남겨 두는 것 이 무너지는
것보다 낫습니다.
실무에서 진짜 중요한 것
장애 조사에서 이 셋을 구분하는 신호가 다르다.
# 서킷 브레이커에 걸렸나 — 대기열 넘침
envoy_cluster_upstream_rq_pending_overflow
# 이상치 감지가 엔드포인트를 뺐나
envoy_cluster_outlier_detection_ejections_active
# 재시도가 얼마나 도나
envoy_cluster_upstream_rq_retry / envoy_cluster_upstream_rq_total
마지막 비율이 5%를 넘으면 이미 재시도가 부하의 일부다. 그때 재시도를 늘리는 것은 불에 기름이다.