새 버전을 프로덕션에 올리기 직전, “실제 트래픽에서는 어떻게 동작할까”라는 질문에 명쾌하게 답할 수 있는 팀은 많지 않습니다. 스테이징을 통과한 코드가 실 사용자 요청 패턴, 특정 헤더 조합, 예상치 못한 페이로드 앞에서 무너지는 일은 흔합니다. Istio의 트래픽 미러링(traffic mirroring)은 실제 요청을 복제해 새 버전에 흘려보내되 응답은 버려서 사용자 영향 없이 검증하고, 지연·오류 주입(fault injection)은 의도적으로 지연과 실패를 주입해 시스템의 회복력을 확인합니다.

이 글에서는 두 기능을 조합해 카오스 테스트 파이프라인을 구성하는 방법을 다룹니다. 핵심은 “프로덕션에서 실험하되 사용자에게 영향을 주지 않는다”는 원칙을 설정과 코드로 강제하는 것입니다.

트래픽 미러링이 해결하는 문제

미러링(섀도잉)은 라이브 트래픽을 그대로 복제해 미러 대상으로 보내되 그 응답은 무시합니다. 사용자는 여전히 기존 버전(primary)의 응답만 받으므로 새 버전에 버그가 있어도 사용자 경험에는 영향이 없습니다. 반면 새 버전은 실제 요청 분포 — 진짜 헤더·바디·트래픽 형태 — 를 그대로 받습니다.

이 방식이 강력한 이유는 합성 부하 테스트로 재현하기 어려운 실제 트래픽의 롱테일을 그대로 노출시키기 때문입니다. 특정 국가의 특수 문자 페이로드, 오래된 클라이언트의 이상한 헤더, 봇의 비정상 요청 등을 미러링이 공짜로 제공합니다. 다만 미러 대상이 부수 효과(side effect)를 일으키면 안 됩니다. 미러 요청이 실제 결제나 이메일 발송을 하면 재앙이므로, 미러 대상은 쓰기 작업을 격리하거나 no-op 처리해야 합니다.

VirtualService로 미러링 구성하기

Istio에서 미러링은 VirtualService의 mirror 필드로 설정하고 mirrorPercentage로 복제 비율을 조절합니다. 처음에는 1%로 시작해 미러 대상의 리소스 부담을 보며 올리는 것이 안전합니다.

# reviews-mirror.yaml
# 실 트래픽은 100% v1 이 받고, 그중 10% 를 v2 로 복제(미러)한다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: reviews
  namespace: shop
spec:
  hosts:
    - reviews
  http:
    - route:
        - destination: { host: reviews, subset: v1 }   # 응답하는 실제 버전
          weight: 100
      mirror:
        host: reviews
        subset: v2            # 응답을 버리는 그림자 버전
      mirrorPercentage:
        value: 10.0           # 실 트래픽의 10% 만 복제

여기서 subset은 DestinationRule에서 파드 라벨(version: v1/v2)로 정의한 논리적 그룹으로, 이 구분이 있어야 미러링이 올바른 대상으로 향합니다.

# reviews-destinationrule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: reviews
  namespace: shop
spec:
  host: reviews
  subsets:
    - name: v1
      labels: { version: v1 }
    - name: v2
      labels: { version: v2 }   # 미러 대상 파드

미러링된 요청은 원본 Host 헤더에 -shadow 접미사가 붙습니다(예: reviews.shop.svc.cluster.local-shadow). 미러 대상은 이 접미사로 자신이 섀도 트래픽임을 인지해 외부 부수 효과를 차단할 수 있습니다.

미러 트래픽을 코드에서 식별하기

미러 대상이 결제·발송·외부 API 호출 같은 부수 효과를 일으키지 않도록, Host 헤더의 -shadow 접미사를 확인하는 미들웨어로 섀도 요청을 판별하는 방식이 견고합니다.

# shadow_guard.py — 미러(섀도) 트래픽이면 부수 효과를 차단
async def shadow_middleware(request, call_next):
    host = request.headers.get("host", "")
    # Istio 미러링은 Host 헤더에 -shadow 접미사를 붙인다
    request.state.shadow = host.endswith("-shadow")
    return await call_next(request)

# 결제·발송 지점에서 방어
def charge_payment(request, amount: int):
    if request.state.shadow:
        # 섀도 트래픽은 실제 결제를 하지 않고 로깅만
        log.info("shadow: skip charge amount=%d", amount)
        return {"status": "shadow-skipped"}
    return payment_gateway.charge(amount)

이 방어로 “미러 대상은 읽기만 하고 쓰지 않는다”는 원칙을 코드로 강제합니다.

지연 주입으로 장애 시뮬레이션

회복력 테스트의 핵심은 “다운스트림이 느려지거나 실패할 때 우리 서비스가 어떻게 반응하는가”입니다. Istio의 fault 설정으로 특정 호출에 인위적 지연을 주입해 타임아웃·서킷 브레이커·재시도가 작동하는지 확인합니다.

# ratings-delay.yaml
# ratings 서비스 호출의 50% 에 5초 지연을 주입한다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ratings
  namespace: shop
spec:
  hosts:
    - ratings
  http:
    - fault:
        delay:
          percentage:
            value: 50.0     # 요청의 절반에만 적용
          fixedDelay: 5s    # 5초 고정 지연
      route:
        - destination: { host: ratings, subset: v1 }

상위 서비스가 3초 타임아웃 후 폴백을 반환하도록 설계했다면 실제로 폴백이 발동하는지 검증할 수 있고, 타임아웃을 빠뜨렸다면 응답 시간이 5초로 치솟으며 문제가 드러납니다. 느려진 다운스트림으로 상위 서비스의 스레드 풀이 고갈되고 여파가 위로 전파되는 캐스케이딩 장애도 지연 주입으로만 재현되곤 합니다.

오류 주입: HTTP 상태 강제

지연과 별개로 특정 비율의 요청에 HTTP 오류를 강제 반환할 수도 있습니다. 다운스트림이 503을 반환할 때 재시도 정책이 동작하는지, 무한 재시도로 폭주하지 않는지 확인합니다.

# ratings-abort.yaml
# ratings 호출의 30% 를 즉시 503 으로 실패시킨다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ratings
  namespace: shop
spec:
  hosts:
    - ratings
  http:
    - fault:
        abort:
          percentage:
            value: 30.0
          httpStatus: 503   # 서비스 불가 응답을 강제
      retries:
        attempts: 3         # 실패 시 최대 3회 재시도
        perTryTimeout: 2s
        retryOn: 5xx,reset  # 5xx 와 연결 리셋에만 재시도
      route:
        - destination: { host: ratings, subset: v1 }

여기 중요한 함정이 있습니다. abort로 주입된 오류는 프록시가 직접 만든 응답이므로 Istio의 retries에 재시도되지 않습니다. 재시도 로직을 검증하려면 오류 주입 대신 실제로 실패하는 백엔드나 애플리케이션 레벨 오류를 써야 합니다. 이 구분을 놓치면 “재시도되는 줄 알았는데 안 되는” 잘못된 안심에 빠집니다.

헤더 기반으로 특정 트래픽에만 주입

프로덕션 전체에 오류를 주입하는 것은 위험합니다. 실전에서는 특정 헤더를 가진 합성 테스트 트래픽에만 카오스를 적용하고 실 사용자는 건드리지 않는 방식이 안전하며, match 조건으로 구현합니다.

# ratings-canary-chaos.yaml
# x-chaos-test: true 헤더가 있는 요청에만 지연을 주입한다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ratings
  namespace: shop
spec:
  hosts:
    - ratings
  http:
    - match:
        - headers:
            x-chaos-test:
              exact: "true"   # 이 헤더가 있을 때만
      fault:
        delay:
          percentage:
            value: 100.0
          fixedDelay: 7s
      route:
        - destination: { host: ratings, subset: v1 }
    - route:                  # 나머지 실 트래픽은 무손실 통과
        - destination: { host: ratings, subset: v1 }

규칙 순서가 중요합니다. Istio는 http 배열을 위에서 아래로 평가하므로 헤더 매칭 규칙을 먼저, 기본 통과 규칙을 마지막에 둬야 합니다. 순서가 바뀌면 첫 무조건 규칙이 모든 트래픽을 삼켜 헤더 매칭이 도달하지 못합니다. 부하 테스트 도구에서 이 헤더를 주입하면 실 사용자와 같은 경로를 타되 테스트 요청만 카오스를 겪습니다.

# 카오스 헤더를 붙여 부하 테스트 실행
# 오직 이 요청들만 7초 지연을 겪고, 실 사용자는 영향 없음
hey -n 200 -c 10 -H "x-chaos-test: true" https://shop.int4.io/api/reviews

# 비교용: 헤더 없이 정상 경로 측정
hey -n 200 -c 10 https://shop.int4.io/api/reviews

미러링과 지연 주입 결합

두 기능의 진가는 조합할 때 나옵니다. v2를 미러링으로 그림자 배포하면서 동시에 v2가 의존하는 다운스트림에 지연을 주입하면 “실제 트래픽 + 열악한 조건”이라는 최악의 시나리오를 사용자 영향 없이 재현할 수 있습니다. 이때 미러 트래픽의 오류율·지연을 primary와 분리해 Prometheus로 관측해야 합니다.

# shadow_metrics.py — 미러(v2) 처리 결과를 primary 와 분리해 계측
shadow_requests = Counter("shadow_requests_total", "미러 처리 수", ["result"])

async def handle_review(request):
    if not request.state.shadow:
        return await process(request)
    try:
        result = await process(request)          # 응답은 버려짐
        shadow_requests.labels(result="ok").inc()
        return result
    except Exception:
        shadow_requests.labels(result="error").inc()  # 배포 전에 잡을 버그
        raise

이제 shadow_requests_total{result="error"}가 상승하면 v2에 실 트래픽에서만 드러나는 버그가 있다는 뜻이므로, 노출 전에 배포를 중단할 수 있습니다.

실험 종료와 안전장치

카오스 실험에서 가장 중요한 것은 빠른 롤백입니다. 주입 설정이 실수로 실 트래픽에 적용됐을 때 즉시 되돌릴 수 있어야 하며, 실험 리소스에 공통 라벨을 붙여 두면 한 번의 명령으로 모두 걷어낼 수 있습니다.

# 모든 카오스 실험을 chaos=enabled 라벨로 관리
kubectl get virtualservice -n shop -l chaos=enabled

# 사고 시 즉시 전체 카오스 설정 삭제 → 정상 라우팅 복원
kubectl delete virtualservice -n shop -l chaos=enabled

# 미러링만 비율 0 으로 낮춰 무력화(리소스는 유지)
kubectl patch virtualservice reviews -n shop --type=json -p 
  '[{"op":"replace","path":"/spec/http/0/mirrorPercentage/value","value":0.0}]'

운영 원칙 요약:

  • 미러 대상은 항상 부수 효과 차단: -shadow 헤더 방어를 코드에 내장하고 통과 못 하면 배포를 막는다.
  • 주입은 헤더 매칭으로 격리: 전체 주입은 승인 없이 금지, 기본은 x-chaos-test 스코프로 제한한다.
  • 미러링은 저비율부터: mirrorPercentage를 1%에서 시작해 리소스를 보며 올린다.
  • 지표 분리: 미러·주입 트래픽 지표를 실 트래픽과 분리해 오탐과 회귀를 구분한다.

마무리

Istio의 트래픽 미러링과 지연·오류 주입은 “프로덕션에서 안전하게 실험한다”는 목표를 인프라 레벨에서 실현하는 도구입니다. 미러링은 실제 트래픽 분포를 새 버전에 무해하게 노출시켜 배포 전 회귀를 잡아내고, 주입은 다운스트림 장애를 재현해 타임아웃·폴백·재시도가 작동하는지 검증합니다. 다만 미러 대상의 부수 효과 차단, 오류 주입과 재시도의 상호작용, 규칙 평가 순서 — 이 세 함정을 코드와 설정으로 명시적으로 막아 두어야 합니다.

자주 묻는 질문

Q. 미러링이 primary 서비스의 성능에 영향을 주나요?

A. Istio는 미러 요청을 fire-and-forget으로 비동기 전송하고 응답을 기다리지 않으므로, 미러 대상이 느리거나 실패해도 primary의 응답 지연에는 원칙적으로 영향이 없습니다. 다만 미러링 자체가 사이드카 프록시의 CPU·네트워크를 소모하므로 mirrorPercentage를 낮게 시작해 리소스 여유를 확인하세요.

Q. fault injection의 abort로 만든 오류가 왜 retries에 걸리지 않나요?

A. abort는 사이드카 프록시가 백엔드에 요청을 보내지 않고 직접 생성하는 응답입니다. 재시도는 실제 업스트림 호출이 실패했을 때 동작하는데, 주입된 abort는 업스트림 호출 자체가 없어 재시도 대상이 아닙니다. 재시도를 검증하려면 실제로 실패하는 백엔드나 애플리케이션 레벨 오류를 써야 합니다.