왜 스테이징만으로는 부족한가
스테이징 환경은 합성 트래픽과 소량의 시드 데이터로 돌아간다. 문제는 장애 대부분이 프로덕션 특유의 조건에서 터진다는 점이다. 실제 사용자 요청의 분포(특정 파라미터 조합, 대용량 페이로드, 만료 직전 토큰), 캐시 히트율, 커넥션 풀 포화, 특정 테넌트의 비정상적 쿼리 패턴은 합성 부하로 재현하기 어렵다. 카나리 배포도 도움이 되지만, 이미 실사용자에게 응답을 반환한 뒤라 오류가 사용자에게 노출된다는 한계가 있다.
섀도 트래픽 미러링은 프로덕션의 실제 요청을 복제해 신규 버전으로 보내되, 그 응답은 버리는 방식이다. 사용자는 기존 버전의 응답만 받으므로 영향이 없고, 우리는 실제 부하로 신규 버전을 검증할 수 있다.
미러링과 다른 전략의 차이
| 전략 | 사용자 영향 | 실제 트래픽 | 주 용도 |
|---|---|---|---|
| 스테이징 | 없음 | 합성 | 기능 검증 |
| 카나리 | 있음(소수) | 실제 | 점진 롤아웃 |
| 블루/그린 | 전환 시 전체 | 실제 | 빠른 롤백 |
| 섀도 미러링 | 없음 | 실제(복제) | 배포 전 검증 |
Envoy로 미러링 구성하기
Envoy는 request_mirror_policies로 요청을 복제한다. 미러 대상의 응답은 무시되고 지연에도 영향을 주지 않는다. runtime_fraction으로 복제 비율을 조절해 처음엔 5%부터 시작하는 것이 안전하다.
routes:
- match: { prefix: "/api/" }
route:
cluster: prod-v1
request_mirror_policies:
- cluster: shadow-v2
runtime_fraction:
default_value: { numerator: 5, denominator: HUNDRED }
runtime_key: routing.request_mirror.shadow_v2
미러 클러스터로 가는 요청의 Host 헤더에는 자동으로 -shadow 접미사가 붙는다. 이를 로그로 구분하면 실사용자 트래픽과 섀도 트래픽을 명확히 나눌 수 있다.
부작용 격리가 핵심이다
미러링의 가장 큰 함정은 쓰기 부작용이다. 복제된 요청이 결제를 두 번 처리하거나, 이메일을 중복 발송하거나, 프로덕션 DB를 오염시키면 재앙이다. 신규 버전은 반드시 섀도 모드임을 인지하고 외부 부작용을 차단해야 한다.
def is_shadow(request):
return request.headers.get("Host", "").endswith("-shadow")
def charge(request, order):
if is_shadow(request):
# 실제 결제/발송/커밋은 건너뛰고 경로만 실행
log.info("shadow: skip charge", order_id=order.id)
return DryRunResult(amount=order.total)
return payment_gateway.charge(order) # 실사용자만 실제 청구
DB는 읽기 전용 복제본이나 별도 섀도 스키마로 연결하고, 외부 API 호출은 목(mock)이나 샌드박스 엔드포인트로 돌린다. 이 격리가 완벽하지 않으면 미러링을 켜서는 안 된다.
무엇을 관측할 것인가
섀도의 목적은 응답을 반환하는 게 아니라 차이를 발견하는 것이다. 동일 요청에 대해 v1과 v2의 응답 바디, 상태 코드, 지연을 비교(diff)하면 회귀를 조기에 잡을 수 있다. 다음 지표를 대시보드로 분리해서 본다.
- v2의 5xx 비율과 예외 로그 — 신규 코드 경로의 실제 실패
- p50/p95/p99 지연 비교 — 성능 회귀 여부
- 응답 diff 불일치율 — 계약 위반, 직렬화 변경
- 커넥션 풀·메모리·GC — 실부하 하에서만 드러나는 자원 문제
운영 시 주의점
미러링은 백엔드 부하를 늘린다. 5% 복제라도 미러 대상 서비스와 그 하위 의존성(DB, 캐시)은 그만큼 추가 요청을 받는다. 공유 자원을 건드린다면 실사용자 지연에 간접 영향이 갈 수 있으니, 섀도용 인프라를 분리하는 편이 안전하다. 또한 개인정보가 담긴 요청을 복제하므로 섀도 로그의 보관 기간과 접근 권한을 프로덕션과 동일하게 관리해야 한다.
검증이 끝나면 미러링을 끄는 것을 잊지 말자. runtime_key를 0으로 내리면 즉시 중단되며, 이 토글은 배포 파이프라인에서 자동화해 두는 것이 좋다. 섀도는 상시 켜두는 기능이 아니라 배포 게이트로 쓰는 도구다.