여러 팀이 일상적으로 혼동하는 두 가지 신뢰성 문제 사이에는 명확한 경계선이 있다. retry(재시도)는 잘못된 요청을 다룬다. 호출 하나가 실패했으니 다시 시도하는 것이다. Failover(장애 조치)는 잘못된 의존성을 다룬다. 전체 구성 요소가 작동을 멈췄으니 그것을 돌아가도록 우회하는 것이다. 로드 밸런싱과 재시도 아키텍처 게시물은 첫 번째 문제, 즉 작업이 어떻게 아이덴티티에 매핑되는지, 그리고 재시도 계층이 개별 실패를 어떻게 분류하고 복구하는지를 다뤘다. 이번 게시물은 두 번째 문제를 다루며, 이는 다른 성격의 문제다. 재시도 루프로도 해결되지 않는 상황, 즉 재시도 대상 자체가 다운된 경우 파이프라인이 무엇을 해야 하는가에 관한 것이다.
여러 시장에서 대규모로 수집을 운영하는 데이터 엔지니어링 팀에게 이는 장애 발생 시 우아하게 성능이 저하되는 파이프라인과 하루치의 누락되거나 잘못된 데이터를 조용히 생성하는 파이프라인 사이의 차이를 의미한다. 레지덴셜 프록시 게이트웨이에서는 IP를 직접 관리하지 않으므로, 여기서 말하는 장애 조치는 주소를 교체하는 문제가 아니라 개별 요청보다 상위 수준에서 발생하는 실패에 대비해 설계하는 문제다.
First, name the failure domains
실제로 무엇이 실패하는지 나열하지 않고는 장애 조치를 설계할 수 없다. 프록시 기반 파이프라인은 여섯 가지 서로 다른 수준에서 실패하며, 그중 처음 두 가지만 이미 처리되어 있다.
| Failure | Who handles it |
|---|---|
| 단일 요청 (타임아웃, 일시적 오류) | retry 계층 |
| 단일 종료 IP가 불량해지는 경우 | 게이트웨이 (서버 측 로테이션) |
| 특정 타겟 전체에 대한 차단 물결 | 당신 |
| 지역/시장 저하 (특정 국가에서 품질 또는 가용성 하락) | 당신 |
| 게이트웨이/프로바이더 (엔드포인트 접근 불가, 인증 실패, 인시던트) | 당신 |
| 자체 인프라 (리전, 워커, 큐 장애) | 당신 |
흔한 실수는 재시도가 표의 첫 행보다 더 많은 것을 처리해준다고 가정하는 것이다. 전체 트래픽 패턴을 차단 물결로 몰아넣는 타겟을 향해 재시도를 더 강하게 시도하는 것은 복구가 아니라 상황을 악화시킬 뿐이다. 인시던트를 겪고 있는 게이트웨이에 재시도를 반복하는 것은 시간만 낭비할 뿐이다. 장애 조치는 세 번째 행부터 여섯 번째 행까지를 위한 설계다.
The reliability primitives you actually control
IP 관리는 게이트웨이 안에서 이루어지므로, 장애 조치 도구 모음은 적절한 세분화 수준에서 적용되는 소수의 기본 요소로 구성된다.
Health signals, per failure domain. 성공률, 지연 시간, 차단율을 전역적으로만이 아니라 타겟별, 지역별, 프로바이더별로 추적하라. 전체 평균 성공률이 95%라도 한 시장이 20%에 머물러 있는 상황을 가릴 수 있다. 실패하는 것을 볼 수 있어야만 장애 조치를 할 수 있으며, 측정 방법은 프록시 속도, 성공률, 위치 정확도를 테스트하는 방법에 있다.
Circuit breakers, per failure domain. 로드 밸런싱 게시물에서는 호스트별 브레이커를 소개했다. 장애 조치는 이를 일반화한다. 타겟별, 지역별, 프로바이더별 브레이커를 두는 것이다. 특정 도메인의 상태가 무너지면, 이미 거부하고 있는 의존성을 계속 두드리는 대신 그것을 멈추고 대체 경로로 전환하라.
A secondary path. 실패했을 때 옮겨갈 곳이 없다면 장애 조치는 의미가 없다. 대체 지역, 대체 프로바이더, 또는 명시적인 저하 모드가 필요하다. 실패에 대한 유일한 대응이 ‘같은 것을 다시 시도하는 것’이라면, 그것은 장애 조치가 아니라 바쁜 루프(busy loop)에 불과하다.
Durable queues. 장애가 발생한 동안 진행 중이던 작업은 그 장애를 견뎌내야 한다. 인시던트로 인해 작업 항목이 손실된다면, 장애 조치는 오히려 손실을 숨기면서 상황을 더 악화시킨 것이다.
Multi-region topology: contain failures, don’t spread them
핵심 구조적 아이디어는 각 시장이 그 자체로 하나의 장애 도메인이며, 토폴로지는 그 상태를 유지해야 한다는 것이다. 독일 트래픽에 대한 차단 물결이 미국 수집 작업을 멈추게 해서는 안 된다.
이는 다음을 의미한다:
- 워커를 시장별로 분리하라. 지역별로 워커 풀(또는 최소한 별도의 큐와 리미터)을 분리해서, 한 리전의 저하가 다른 리전의 용량을 소모하지 못하도록 하라. 이는 로드 밸런싱 게시물에서 다룬 호스트별 격리 원칙을 리전 단위로 한 단계 위로 적용한 것이다.
- 리전 단위의 서킷 브레이커와 동시성. 각 시장은 고유한 브레이커와 고유한 동시성 예산을 갖는다.
de가 트립되어도us와gb는 영향 없이 계속 실행된다. - 전역적 결합을 피하라. 리전 전체에 걸쳐 공유되는 단일 레이트 리미터나 단일 재시도 예산은 독립적인 장애 도메인들을 결합시키며, 이는 피해야 할 정확한 안티 패턴이다. 한 시장의 인시던트는 다른 시장에는 보이지 않아야 한다.
그 결과: 부분적인 실패는 부분적인 상태로 머문다. ‘파이프라인이 다운됐다’가 아니라 ‘독일 수집이 저하되어 장애 조치 중이지만 나머지는 모두 정상 작동한다’는 상태가 되며, 이는 새벽 3시의 긴급 호출이 아니라 운영 가능한 상태다.
Provider-level failover, honestly
진지한 신뢰성 검토에서 제기되는 불편한 질문이 있다. 게이트웨이 자체에 인시던트가 발생하면 어떻게 되는가? 인증이 실패하기 시작하고, 엔드포인트에 접근할 수 없고, 품질이 네트워크 전체에서 무너진다. 아무리 지역을 격리해도 도움이 되지 않는다. 모든 리전이 동일한 프로바이더를 거쳐 라우팅되기 때문이다.
솔직한 답에는 두 가지 부분이 있다.
대부분의 팀은 멀티 프로바이더 장애 조치가 필요하지 않으며, 이를 조급하게 추가해서는 안 된다. 두 번째 프로바이더를 두면 통합 영역, 청구, 일관성 문제(프로바이더 간 지역 및 세션 시맨틱이 다르므로 장애 조치된 요청이 미묘하게 다르게 동작할 수 있다)가 두 배로 늘어난다. 대다수의 파이프라인에서는 품질 좋은 레지덴셜 프록시 프로바이더 하나와 견고한 내부 장애 조치, 즉 상태 기반 브레이커, 저하 모드, 내구성 있는 큐만으로 실제 위험을 충분히 커버할 수 있다. 드문 프로바이더 인시던트에 대비해 두 번째 프로바이더를 추가하면서 매일 복잡성을 감수하는 것은 대개 나쁜 거래다.
신뢰성 목표가 실제로 이를 정당화하는 경우, 즉 엄격한 신선도 SLA를 가진 고가치 파이프라인의 경우에는, 프로바이더를 얇은 인터페이스 뒤로 추상화해서 장애 조치가 재작성이 아니라 설정 변경이 되도록 제대로 구현하라.
class ProxyProvider: def proxy_url(self, geo: str, session: str | None) -> str: ... def healthy(self) -> bool: ...
class Gateway: def __init__(self, primary: ProxyProvider, secondary: ProxyProvider | None = None): self.primary, self.secondary = primary, secondary
def resolve(self, geo, session=None): # Prefer primary; fail over only when its breaker is open. if self.secondary and not self.primary.healthy(): return self.secondary.proxy_url(geo, session) return self.primary.proxy_url(geo, session)중요한 것은 코드 자체가 아니라 그 형태다. 프로바이더는 인터페이스 뒤에 있는 교체 가능한 의존성이며, 상태에 따라 게이트되고, 대체 경로는 주 프로바이더의 브레이커가 열리기 전까지는 휴면 상태로 남는다. 오늘 단일 프로바이더만 운영하고 있더라도 이 인터페이스로 구축하는 비용은 크지 않으며, 이는 나중에 보조 프로바이더를 추가하는 것이 파이프라인 재작성이 아니라 설정 변경이 되도록 만든다. 필요해지기 전에 두 번째 프로바이더를 구매하지는 말되, 그 문은 열어두라.
Graceful degradation: wrong-but-honest beats missing
신선한 데이터를 정말로 수집할 수 없을 때, 아무것도 없는 상태로 장애 조치하는 것은 최선의 선택이 되는 경우가 거의 없다. 의도적으로 성능을 저하시켜라:
- 마지막으로 확인된 정상 데이터를 stale로 표시해 제공하라. 많은 사용 사례에서,
stale: true로 표시된 어제의 가격은 다운스트림이 그것이 오래된 데이터임을 알고 있다면 데이터가 없는 것보다 더 유용하다. 오래된 데이터를 현재 데이터처럼 조용히 넘기지 마라. 이는 데이터 품질 실패(그리고 그 데이터가 사람에 관한 결정을 좌우한다면 정확성 실패)다. - 우선순위에 따라 부하를 덜어내라. 부분 장애 중 용량이 제한된다면, 모든 것을 균등하게 실패시키기보다는 고가치 타겟을 수집하고 롱테일은 미뤄라.
- 허용 범위를 넓혀라. 커버리지를 줄이는 대신 신선도 요구 사항을 일시적으로 완화하라. 약간 오래된 전체 데이터셋이 신선하지만 부분적인 데이터셋보다 나은 경우가 많다.
성능 저하는 사고가 아니라 정식으로 설계된 모드여야 한다. 각 데이터셋에 대해 ‘저하됨’이 무엇을 의미하는지 미리 결정하고, 이를 명시적이고 관찰 가능한 상태로 만들라.
Durability and backpressure
장애 조치는 진행 중인 작업이 그 장애를 견뎌낼 때만 작동한다. 두 가지 규칙이 있다:
아무것도 손실되지 않는다. 작업 항목은 내구성 있는 큐에 존재한다. 실패한 항목은 사라지는 것이 아니라 의존성이 복구되면 소진되는 재시도 큐나 데드레터로 이동한다. de가 장애 조치되면, 대기 중인 작업은 대기 상태로 있다가 브레이커가 닫히면 다시 실행된다.
재실행은 안전해야 한다. 작업 항목을 idempotent(멱등)하게 만들어서 복구 후 재실행이 중복 집계나 손상을 일으키지 않도록 하라. 이는 로드 밸런싱 게시물이 의존하는 것과 동일한 멱등성이며, 이것이 장애 조치 복구를 정합성 확인의 악몽이 아니라 깔끔한 과정으로 만드는 요소다.
Test the failover, or it doesn’t exist
실제 인시던트 중에 처음으로 작동해보는 장애 조치 경로는 장애 조치 경로가 아니라 좋은 의도로 포장된 부채일 뿐이다. 스테이징 환경에서 의도적으로 실패를 주입하라:
- 한 리전의 프로바이더를 죽은 엔드포인트로 향하게 하고, 브레이커가 트립되고 장애 조치되며 다른 리전들이 영향을 받지 않는지 확인하라.
- 타겟이 차단 응답을 반환하는 상황을 시뮬레이션하고, 타겟별 브레이커가 열리고 저하 모드가 작동하는지 확인하라.
- 실행 중에 워커나 큐를 강제 종료하고, 복구 시 작업이 손실되지 않는지 확인하라.
파이프라인이 의존성을 잃고도 균형을 유지하는 모습을 직접 지켜본 적이 없다면, 실제로 그렇게 될지는 알 수 없는 것이다.
Observability built for failure, not just health
전체 처리량만 보여주는 대시보드는 한 시장이 조용히 죽어가는 동안에도 녹색으로 보일 것이다. 장애 도메인을 위한 계측을 구축하라:
- 성공률뿐 아니라 신선도를 포함하는 SLO. 데이터 지연(data lag)은 조용히 멈춘 리전을 잡아내는 지표다.
- 도메인별 상태에 대한 알림. 타겟별, 지역별, 프로바이더별로 두어서, 하나의 시장이 실패했을 때 하루치 데이터 누락이 되기 전에 즉시 알림을 받도록 하라.
- 장애 조치 이벤트를 정식 텔레메트리로 취급하라. 브레이커가 트립되거나 리전이 저하될 때, 이는 조용한 내부 상태가 아니라 기록하고 알리고 검토해야 할 이벤트다.
FAQ
retry와 failover의 차이는 무엇인가? retry는 동일한 의존성에 대해 실패한 단일 요청을 다시 시도하는 것이다. failover는 그 자체가 실패한 의존성, 즉 차단 물결이 발생하는 타겟, 저하된 지역, 프로바이더 인시던트를 우회하는 것이다. 재시도로는 고장난 의존성을 고칠 수 없다. failover는 재시도 대상 자체가 다운되었을 때를 위한 설계다.
장애 조치를 위해 두 번째 프록시 프로바이더가 필요한가? 대개는 필요하지 않다. 품질 좋은 프로바이더 하나와 내부 장애 조치, 즉 상태 기반 서킷 브레이커, 저하 모드, 내구성 있는 큐만으로도 대부분의 위험을 커버할 수 있으며, 두 번째 프로바이더는 실제 비용과 일관성 복잡성을 추가한다. 엄격한 신뢰성/신선도 SLA가 이를 정당화할 때만 멀티 프로바이더를 추가하고, 그렇게 하더라도 인터페이스 뒤로 프로바이더를 추상화해서 설정 변경만으로 처리되도록 하라.
한 시장의 실패가 전체 파이프라인을 무너뜨리지 않도록 하려면 어떻게 해야 하는가? 각 시장을 고유한 장애 도메인으로 취급하라. 지역별로 워커 풀, 큐, 동시성 예산, 서킷 브레이커를 분리하고, 이들을 결합시키는 전역 레이트 리미터나 공유 예산을 두지 마라. 그러면 한 국가의 차단 물결은 그 국가만 저하시키고 장애 조치되며, 나머지는 정상적으로 실행된다.
신선한 데이터를 수집할 수 없을 때는 어떻게 해야 하는가? 조용히 실패하는 대신 의도적으로 성능을 저하시켜라. 마지막으로 확인된 정상 데이터를 stale로 표시해 제공하거나, 우선순위 타겟으로 부하를 줄이거나, 신선도 허용 범위를 넓혀라. 각 데이터셋에 대해 ‘저하됨’이 무엇을 의미하는지 미리 결정하고 이를 관찰 가능한 상태로 만들되, 오래된 데이터를 현재 데이터처럼 넘기지는 마라.
내 장애 조치가 실제로 작동하는지 어떻게 알 수 있는가? 테스트하라. 스테이징 환경에서 프로바이더, 타겟, 인프라 실패를 주입하고 브레이커가 트립되고, 대체 경로가 작동하고, 다른 도메인들이 정상 유지되고, 작업이 손실되지 않는지 확인하라. 실제 인시던트 중에만 실행된 장애 조치 경로는 고장났다고 가정해야 한다.
The bottom line
retry와 failover는 서로 다른 문제를 해결하며, 이 둘을 혼동하는 것이 견고해 보이는 파이프라인이 실제 인시던트에서 무너지는 이유다. retry는 개별 요청을 복구하고, failover는 타겟, 시장, 프로바이더, 또는 자체 인프라 같은 장애 도메인 전체가 다운될 때를 위한 아키텍처다. 이를 구축하려면 그런 도메인들을 명확히 규정하고, 각 시장을 격리해 실패가 그 안에 갇히도록 하고, 상태 기반 서킷 브레이커 뒤에서 의존성을 게이트하고, 조용히 실패하는 대신 의도적으로 성능을 저하시키고, 진행 중인 작업을 내구성 있고 멱등하게 유지하고, 그리고 무엇보다도 인시던트가 실패 경로를 테스트하기 전에 스스로 그것을 테스트해야 한다.
프로바이더 문제에 관해서는, 실제 SLA가 요구할 때까지 두 번째 프로바이더 추가를 자제하고, 그 선택이 저비용으로 유지되도록 얇은 프로바이더 인터페이스로 구축하라. 일관된 지역 및 세션 동작을 갖춘 품질 좋은 레지덴셜 프록시 네트워크는 흔한 실패 모드인 차단 물결과 지역 저하를 애초에 복구 가능하게 만들며, 풀 품질(IP reputation)은 이러한 경로를 얼마나 자주 실행하게 되는지를 결정한다. 가격 페이지에는 자체 멀티 리전 워크로드에 맞춰 이를 구축하고 테스트할 수 있는 GB당 플랜이 있다.