Istio Ambient mode 도입기 3-1편으로, workload rollout 과정에서 간헐적으로 발생한 503을 로그·pcap·socket 세 각도로 추적한 기록이다. 처음에는 idle timeout이나 config propagation 지연을 의심했지만 설정을 조정해도 사라지지 않았다. 결론은 IP 겹침이 아니라 waypoint(Envoy)가 목적지 Pod이 죽은 뒤에도 폐기하지 못한 stale connection을 재사용하는 문제였고, IP 재사용은 그 stale connection을 새 Pod으로 잘못 잇게 만든 방아쇠일 뿐이었다.
핵심 포인트- waypoint 로그는 upstream이 응답 시작 전에 connection을 끊었다고 말하는데, 정작 그 upstream인 ztunnel에는 아무 이상 로그가 없었다.
- destination Pod에서 캡처한 pcap에서 새 Pod 생성 직후 TLS handshake 없이 application data stream이 곧바로 들어오는 장면이 결정적이었다.
- handshake 없이 data frame이 온다는 것은 이미 존재한다고 믿는 connection을 재사용했다는 신호였다.
- Envoy가 connection pool을 목적지 주소를 키로 관리하고, ztunnel은 Pod 종료 시 GOAWAY나 FIN으로 graceful하게 닫아주지 않아 waypoint가 죽음을 알 방법이 없었다.
- 근본 해결은 upstream 개선에 달려 있어, 단기적으로는 RST에 대한 retry로 503 전파를 막는 전략을 택했다.
상세 정리- 재현 특성: 애플리케이션 배포와 재시작이 훨씬 잦은 dev 환경에서는 비교적 잘 재현됐지만 prod에서는 드문 확률로만 발생했다.
- 로그 추적: 가장 바깥 public gateway는 아래 단계에서 받은 503을 그대로 전달했다는 사유를 남겼고, waypoint에는 upstream_reset_before_response_started{connection_termination}이 찍혀 있어 503을 만든 지점이 waypoint임이 드러났다.
- 재현 환경 구성: 실제 서비스는 Istio 컴포넌트가 쏟아내는 트래픽 볼륨이 커서 흔적을 골라내기 어려워, dummy application과 전용 gateway·waypoint·ztunnel을 따로 띄운 격리 환경을 만들었다.
- 범위를 좁히다 waypoint만 이용한 Pod → waypoint → waypoint 통신에서도 동일하게 재현돼, gateway를 경로에서 빼고 waypoint와 ztunnel과 Pod 구간에 집중할 수 있었다.
- 추가 수집: waypoint의 debug level 로그와 destination Pod 안의 TCP 패킷 캡처를 위해 NET_ADMIN 권한을 가진 tcpdump sidecar를 주입했고, Pod 생성부터 종료 이후 잔여 패킷까지 잡아 pcap을 S3에 업로드하도록 구성했다.
- pcap이 보여준 것: destination Pod의 모든 인터페이스를 캡처하면 waypoint에서 ztunnel로 들어오는 암호화된 HBONE 패킷과, ztunnel socket을 거쳐 복호화된 평문 패킷이 동시에 보여 터널 안과 밖을 한 화면에서 비교할 수 있었다.
- 정상 케이스는 HBONE 터널을 새로 수립한 뒤 data frame이 오가는데, 비정상 케이스는 handshake가 통째로 생략된 채 data frame이 먼저 도착했다. 새 Pod의 network namespace에는 이 TCP connection 상태가 없어 커널 TCP 스택이 RST로 응답한다.
- 결정적 단서: 비정상 응답을 받은 Pod 자체는 probe도 running state도 정상이었고, 대신 그 Pod의 IP가 짧은 시간 안에 재사용되고 있었다. 직전에 삭제된 Pod의 IP를 새 Pod이 물려받은 상황이었다.
- 원인 규정: IP 겹침은 근본 원인이 아니라 확률을 높이는 조건이다. IP가 매번 새것이면 stale connection은 죽은 채 남고 다음 요청에서 새 connection이 맺히지만, 같은 IP가 재할당되면 Envoy가 그것을 살아 있는 같은 목적지 connection으로 착각한다.
- 두 컴포넌트의 역할이 맞물린다. waypoint는 목적지 주소가 같으면 기존 connection을 재사용하고, ztunnel은 Pod 종료 시 graceful close를 하지 않는다.
- 경로별 차이: Istio 메인테이너 의견에 따르면 ztunnel과 ztunnel 사이 경로는 단순 IP가 아닌 값으로 키잉하고 RST를 받으면 HBONE connection을 폐기해 비교적 안전하지만, Envoy에서 ztunnel로 가는 경로는 이 규칙을 잘 따르는지 확신하기 어렵다고 했고 겪은 문제가 정확히 그 경로였다.
- 구조적 배경: waypoint Envoy는 client와 목적지 Pod을 하나의 직접 connection으로 연결하지 않는다. downstream listener와, HBONE 터널을 별도로 맺어 pool로 관리하는 internal listener·ORIGINAL_DST cluster가 internal listener 경계로 단절돼 있다.
- 이 분리 때문에 downstream 요청을 처리하는 시점에 upstream connection이 stale인지가 곧바로 드러나지 않고, pool에 연결돼 있다고 표시된 것을 그대로 재사용하게 된다.
- 검증 1 로그: 새 IP를 쓰는 Pod로 요청해 신규 HBONE connection을 만든 뒤(Phase 1), 같은 IP를 재사용하는 Pod로 요청하자(Phase 2) Phase 1의 connection ID가 동일하게 다시 등장했다.
- 검증 2 pcap: 애플리케이션 라이프사이클 전체를 살펴봐도 HTTP/2 GOAWAY나 FIN 패킷이 관찰되지 않아, ztunnel이 종료 시 graceful close를 하지 않는다는 것이 확인됐다.
- 검증 3 socket: in-mesh Pod을 삭제한 뒤 waypoint 안에 해당 Pod IP를 peer로 하는 socket이 일정 시간 ESTABLISHED로 남아 있는 것을 확인했다.
- Istio 팀에 재현 과정과 로그를 정리해 이슈로 등록했고, AWS VPC CNI/EKS뿐 아니라 waypoint 없이 ingress gateway만 쓰는 환경에서도 보고돼 Ambient mode의 공통 문제임이 확인됐다.
- 근본 해결안 1: connection 식별 키에 목적지 주소 외 메타데이터를 더하는 것이다. ztunnel의 HBONE pool은 source identity, destination identity, destination address, source IP를 묶어 해시한 값을 키로 쓴다.
- 다만 같은 Deployment의 새 Pod은 보통 같은 ServiceAccount를 쓰므로 identity만으로는 구분이 안 되고, Pod UID처럼 인스턴스마다 달라지는 메타데이터까지 키에 넣어야 원천 차단된다.
- 근본 해결안 2 graceful close의 어려움: application이 직접 GOAWAY를 보낼 수는 없다. Ambient mode는 application에 투명해야 하므로 자기 client가 waypoint인지 알 필요가 없어야 한다.
- ztunnel이 보내려 해도 Pod 종료 후에는 CNI가 veth와 network namespace를 정리하면서 신호를 보낼 통로 자체가 사라진다. 그래서 별도 control path를 갖거나 종료 예정 상태를 미리 통지받아야 하고, ShutdownStarting 신호나 CNI DEL hook이 논의되는 이유가 여기 있다.
- GOAWAY도 만능이 아니다. HBONE은 CONNECT로 만든 outer 터널 안에 inner TCP 스트림을 싣는 구조라, GOAWAY는 새 요청이 기존 outer connection을 재사용하지 않게 막는 데 도움이 될 뿐 이미 active한 inner connection을 즉시 정리하지는 못한다.
- 단기 대응: waypoint의 retry 조건을 reset까지 포함하도록 확장해, stale connection으로 reset이 발생하면 자동으로 다시 시도하게 했다. 커뮤니티에서도 EnvoyFilter로 이를 추가하는 것이 사실상 유일하게 효과를 본 우회책으로 보고된다.
- 여기서 RST는 ztunnel이 유효하지 않다고 판단해 보낸 것이라기보다, 새 Pod의 network namespace에 기존 connection 상태가 없어 TCP 스택에서 발생하는 현상으로 보는 편이 정확하다.
- 검토했다 보류한 대안: HBONE idle timeout을 짧게 잡거나 aggressive HTTP/2 keepalive를 쓰는 방법은 stale을 빨리 줄일 뿐 IP 재사용과 connection 재사용이 겹치는 상황 자체를 막지 못하며, 커뮤니티에서도 해당 환경변수 조정이 도움이 되지 않았다는 보고가 있어 보조 수단으로만 봤다.
- retry의 부작용: reset retry는 이번 문제뿐 아니라 Pod OOM이나 프로세스 크래시 같은 다른 원인의 RST에도 걸린다. 따라서 대상 API가 멱등한지, 중복 실행이 애플리케이션 상태나 외부 시스템에 부작용을 만들지 않는지 함께 확인해야 한다.
왜 읽나Istio Ambient mode에서 배포마다 튀는 503의 정체를 못 찾고 있는 팀에게 원인 규명과 단기 우회책을 주고, 로그에서 가설을 세워 pcap과 socket으로 검증하는 추적 방법론 자체가 남는다.