LabHub
배우기 러닝패스 코스

Envoyの内部構造

リトライが障害を大きくする瞬間

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

재시도는 일시적 실패를 가린다. 그런데 백엔드가 과부하일 때 재시도를 켜 두면 부하를 두세 배로 키워 장애를 완성시킨다. 그래서 재시도에는 항상 상한과 예산이 필요하다.

概念マップ: 일시적 실패・27배・즉시 실패・서킷 브레이커

왜 이게 필요했나

백엔드 하나가 느려진다 → 타임아웃 → 재시도 → 부하 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%를 넘으면 이미 재시도가 부하의 일부다. 그때 재시도를 늘리는 것은 불에 기름이다.