Istio 3-1편: 503과 Half-open Connection
Ambient mode 트러블슈팅: Envoy의 stale connection 재사용에 따른 503 에러
Jetty (정재홍) • DevOps Engineer
- DevOps
안녕하세요, 채널코퍼레이션 DevOps팀의 재티, 딜런입니다.
이 글은 Istio Ambient mode 도입기 시리즈의 3-1편입니다. 3편에서 프로덕션에서 만난 여러 이슈를 다루며 "가장 까다로웠던 이슈"로 잠깐 예고했던 503 에러를, 이번 글에서 따로 떼어 깊게 추적해보려 합니다.
3편: 프로덕션에서 만난 당황스러운 이슈들과 트러블슈팅
3-1편: 503과 Half-open Connection (현재 글)
3-2편: Partially Enrolled Pod와 Untaint Controller
3-3편: Ambient mode 안전하게 업그레이드하기
3-4편: 부록 — 507 status code와 istiod disconnected 탐지
이 글은 2편에서 설명한 Envoy config 구조를 전제로 합니다. 특히 waypoint가 in-mesh 목적지로 트래픽을 보낼 때 사용하는
connect_originate(ORIGINAL_DST) cluster와 internal listener, 그리고 HBONE 터널 개념을 알고 있으면, 이후 분석을 이해하기 용이합니다. 해당 내용이 생소하시다면 2편을 먼저 읽고 오시는 것을 권합니다.
1. 문제 상황
문제는 workload rollout(재시작/배포) 과정에서 간헐적으로 발생한 503 응답이었습니다. 처음에는 Istio에서 503 status code 원인으로 잘 알려져있는 idle timeout, keep-alive timeout 설정 문제나 Istio config propagation delay를 의심했습니다. 하지만 timeout 설정을 조정해도 증상은 사라지지 않았고, config propagation 지연으로 보기에도 로그 발생 시점이 맞지 않았습니다.
재현 빈도에도 특징이 있었습니다. 애플리케이션 배포와 재시작이 훨씬 더 잦은 dev 환경에서는 비교적 잘 재현되었지만 prod에서는 드문 확률로만 발생했습니다.
요청 경로와 503이 발생하는 지점을 그림으로 정리하면 다음과 같습니다.
컴포넌트별 로그를 따라가 보면 503이 만들어지는 지점이 드러납니다. 가장 바깥의 public gateway는 response_code: 503과 response_code_details: via_upstream을 남깁니다. upstream_host가 envoy://connect_originate/...:15008을 가리키는데, 이는 곧 아래 단계인 waypoint에서 받은 503을 그대로 전달했다는 뜻입니다.
그다음 waypoint의 로그를 보면 response_code: 503, response_code_details: upstream_reset_before_response_started{connection_termination}, response_flags: UC가 찍혀 있습니다. 즉 실제로 503을 만들어 낸 지점은 waypoint입니다. 여기서 UC는 UpstreamConnectionTermination의 약자로, "upstream connection이 응답이 시작되기도 전에 끊겼다"는 의미입니다.
문제는 그다음입니다. waypoint 바로 다음 hop인 ztunnel에는 아무런 이상 로그가 없었습니다. istio-cni도 마찬가지였습니다. waypoint는 "upstream이 connection을 끊었다"고 말하는데, 정작 그 upstream에 해당하는 ztunnel에서는 에러가 관찰되지 않았습니다. 그래서 우리가 처음 던진 질문은 이것이었습니다. connection은 대체 어디에서 끊어진 것일까?
2. 문제 재현
가장 먼저 부딪힌 현실적인 벽은, 이 문제를 프로덕션에서 직접 디버깅하기 어렵다는 점이었습니다. 실제 서비스 환경은 istio 컴포넌트가 쏟아내는 트래픽 볼륨이 커서, 그 안에서 문제의 흔적을 골라내는 것이 어려웠습니다. 그래서 일단 격리된 재현 환경을 따로 구성했습니다. dummy application과 전용 gateway·waypoint·ztunnel을 별도로 띄워, 노이즈 없이 문제만 관찰할 수 있는 환경을 만들었습니다.
이 환경에서 범위를 좁혀가다 waypoint만 이용한 Pod → waypoint → waypoint 통신에서도 동일하게 503이 재현되었습니다. gateway를 경로에서 제거한 상태로 문제를 관찰할 수 있었고, 이후 분석은 waypoint ↔ ztunnel ↔ Pod 구간에 집중했습니다.
다만 기존 로그만으로는 더 들어가기 어렵다고 판단해, 두 가지 정보를 추가로 수집했습니다. 하나는 waypoint의 debug level 로그였고, 다른 하나는 destination Pod 안에서의 TCP 패킷 캡처였습니다. 후자를 위해 NET_RAW/NET_ADMIN 권한을 가진 tcpdump sidecar를 주입했습니다.
그리고, 문제의 핵심이 "Pod가 종료되는 시점"에 있다고 보고 Pod 생성부터 종료까지 전체 라이프사이클을 캡처하도록 세팅하였습니다. 구체적으로는 SIGTERM 이후 남는 잔여 패킷까지 끝까지 잡고, 캡처된 pcap을 S3에 업로드하도록 구성했습니다.
3. 문제 분석
3.1 정상과 비정상의 차이를 pcap에서 찾다
"이게 진짜 문제 상황이구나"라고 확신한 순간은, destination Pod에 직접 tcpdump를 떠서 Wireshark로 열어본 시점이었습니다.
destination Pod의 모든 network interface를 캡처하면 흥미롭게도 두 종류의 패킷이 동시에 보입니다. 하나는 ztunnel을 거치기 전의 암호화된(HBONE/mTLS) 패킷으로, waypoint에서 ztunnel로 들어오는 구간입니다. 다른 하나는 ztunnel socket을 거쳐 복호화된 평문 패킷으로, ztunnel에서 application으로 전달되는 구간입니다. 덕분에 같은 Pod 안에서 "터널 안(encrypted)"과 "터널을 빠져나온(decrypted)" 트래픽을 한 화면에서 나란히 비교할 수 있었습니다.
503을 재현했을 때 포착된 장면은 결정적이었습니다. destination Pod가 새로 생성된 직후, TLS handshake도 맺지 않은 채 application data stream이 곧바로 인입되고 있었습니다. 정상이라면 암호화 구간에서 TCP와 TLS handshake가 먼저 이뤄져야 하는데, 그 과정이 통째로 생략된 상태로 data frame만 도착한 것입니다.
정상 케이스에서는 다음처럼 HBONE 터널을 새로 수립한 뒤에 data frame이 오갑니다.
반면 비정상 케이스에서는 handshake가 통째로 생략된 채 data frame이 먼저 도착합니다. 새 Pod의 network namespace에는 이 TCP connection 상태가 없기 때문에, kernel TCP stack이 RST로 응답합니다.
여기서 자연스럽게 한 가지 의심이 떠올랐습니다. 새 Pod가 막 떴는데도 application data가 handshake 없이 "이어서" 들어왔다는 것은, 보내는 쪽인 waypoint가 이 Pod를 "이미 connection을 맺어 둔 예전의, 혹은 다른 Pod"로 착각하고 있다는 뜻일 수 있습니다. 즉, waypoint가 connection을 재사용(re-use) 하고 있는 것은 아닐까 하는 의심이었습니다.
정리하면 핵심 질문은 이렇게 좁혀집니다. Envoy(waypoint)는 왜, 그리고 어떻게 handshake도 이루어지지 않은 connection에 Application data를 실어 보냈는가? "handshake 없이 data frame"이라는 현상은 곧 "이미 존재한다고 믿는 connection을 재사용했다"는 신호라고 판단했습니다.
3.2 Pod 상태 분석
비정상 응답을 한 Pod 자체에는 문제가 없었습니다. probe 설정도, running state도 모두 정상이었습니다. 대신 결정적인 단서가 따로 있었습니다. 비정상 응답을 받은 Pod의 IP가 짧은 시간 안에 재사용되고 있었다는 점입니다. 다시 말해, 직전에 삭제된 다른 Pod가 쓰던 IP를 새 Pod가 그대로 물려받은 상황이었습니다.
4. 원인 진단
4.1 가설: "IP 겹침"이 아니라 "stale connection"이 진짜 원인
"AWS VPC CNI가 Pod IP를 재할당해서 IP가 겹치는 게 문제다"라고 결론 내리기 쉽습니다. 하지만 IP 겹침은 근본 원인은 아닙니다. 근본 원인은 waypoint(Envoy)가 IP:Port를 이름(key)으로 들고 있던 HTTP/2 connection을, 목적지 Pod가 이미 terminate되었는데도 그 사실을 인지하지 못하고 계속 붙들고 있는 것입니다.
즉 이 문제는 "IP가 겹쳐서 생긴 문제"가 아니라 "stale connection을 폐기하지 못하는 connection 생명주기 관리의 문제"입니다. IP 재사용은 그 stale connection이 하필 새 Pod로 잘못 연결되도록 만드는 조건일 뿐입니다.
그래서 IP 재사용은 "원인"이라기보다 "확률을 높이는 조건"에 가깝습니다. 만약 IP가 매번 새것으로만 할당된다면, stale connection은 그냥 죽은 채로 남고 다음 요청에서는 새 connection이 맺힙니다. 그런데 하필 같은 IP가 새 Pod에 재할당되면, Envoy는 그 stale connection을 "아직 살아 있는, 같은 목적지로의 connection"으로 착각하고 재사용하게 됩니다. 그 결과가 바로 503입니다.
connection이 stale로 남는 데에는 두 컴포넌트의 역할이 맞물려 있습니다. 먼저 waypoint(Envoy)는 connection pool을 IP:Port 기준으로 관리하기 때문에, 동일한 IP:Port라면 기존 connection을 재사용합니다. 한편 ztunnel은 Pod가 종료될 때 그 connection을 graceful하게(GOAWAY/FIN으로) 닫아주지 않습니다. 그래서 upstream인 waypoint는 connection이 죽었다는 사실을 알 방법이 없습니다.
이 지점에서 두 통신 경로를 구분할 필요가 있습니다. 이는 Istio 메인테이너인 howardjohn의 의견(#1637 comment)이기도 합니다. ztunnel → ztunnel 경로는 connection을 단순 IP가 아니라 dst-ip + Service Account로 키잉하고, RST를 받으면 HBONE connection을 폐기하므로 비교적 안전하다고 보고 있습니다. 반면 Envoy(waypoint) → ztunnel 경로는 Envoy가 이 규칙을 잘 따르는지 확신하기 어렵다("less sure")고 했습니다. 그리고 우리가 겪은 문제가 정확히 이 경로였습니다.
정리하자면, 이 글에서 말하는 half-open(stale) connection이란 새로운 Pod와 그 ztunnel은 인지하지 못하는 채로 Waypoint는 아직 살아 있다고 믿는 connection입니다.
4.2 가설 검증
waypoint는 downstream과 upstream을 "직접" connection을 맺지 않는다
검증에 들어가기 전에 2편의 내용을 한 번 더 떠올려야 합니다. waypoint Envoy는 client(downstream)와 목적지 Pod(upstream)를 하나의 직접 connection으로 연결해서 관리하지 않습니다. 내부적으로 두 영역이 분리되어 있습니다.
하나는 client의 요청을 받는 downstream listener이고, 다른 하나는 HBONE 터널, 즉 upstream connection을 별도로 맺어 connection pool로 관리하는 internal listener connect_originate와 그에 연결된 connect_originate(ORIGINAL_DST) cluster입니다. 이 두 영역은 Envoy 내부 user-space의 internal listener 경계로 단절되어 있고, upstream HBONE connection은 connect_originate cluster의 pool이 IP:Port를 키로 따로 보관·재사용합니다. 이 구조의 상세는 Istio 2편: Envoy config로 해부하는 Ambient mode에서의 3. Internal Listener와 HBONE 터널링 부분에서 다뤘습니다.
이 구조가 시사하는 바가 이 글의 핵심입니다. downstream과 upstream이 internal listener로 분리되어 있기 때문에, downstream(client) 입장에서는 upstream(HBONE→Pod) connection의 상세한 상태를 명확히 알기 어렵습니다. 즉 upstream connection이 stale인지, 다시 말해 목적지 Pod가 이미 죽었는지가 downstream 요청을 처리하는 시점에 곧바로 드러나지 않습니다. 그래서 pool에 "아직 연결되어 있다"고 표시된 connection을 그대로 재사용하게 됩니다.
이제 세 가지 각도(로그 · pcap · socket)에서 가설을 검증해 보겠습니다.
(1) waypoint Envoy debug 로그로 connection 재사용을 추적
먼저 waypoint의 debug 로그로 connection 재사용 여부를 직접 확인했습니다. 실험은 두 단계로 나눴습니다. Phase 1에서는 새 IP를 쓰는 Pod-aaa로 요청을 보내 정상적으로 신규 HBONE connection이 생성되게 했고, Phase 2에서는 같은 IP를 재사용하는 Pod-bbb로 요청을 보냈습니다.
결과는 명확했습니다. 로그상 Phase 1에서 만들어진 connection ID가 Phase 2에서 동일하게 다시 등장했습니다. 새 Pod로의 요청인데도 새 connection을 맺지 않고 옛 connection을 그대로 재사용했다는 직접적인 증거였습니다.
새 IP를 받은 Pod-aaa로 요청이 들어오며 connect_originate cluster에 신규 HBONE connection이 생성되는 로그입니다. 동일한 ConnectionId: 79097이 이후 재사용 여부를 추적하는 기준이 됩니다.
Pod-aaa가 삭제된 뒤 같은 IP를 받은 Pod-bbb로 요청이 들어왔지만, waypoint는 새 connection을 만들지 않고 기존 connection을 using existing fully connected connection으로 재사용합니다.
로그에 남은 using existing fully connected connection 메시지가 상징적입니다. 같은 IP:Port를 근거로 Phase 1의 connection을 그대로 재사용하고 있었음이 명확히 확인되었습니다.
(2) ztunnel은 connection을 어떻게 닫는가
그렇다면 ztunnel은 Pod가 죽을 때 connection을 제대로 닫고 있었을까요? pcap을 application의 라이프사이클 전체에 걸쳐 살펴봤지만, HTTP/2 GOAWAY나 FIN 패킷이 관찰되지 않았습니다. 즉 ztunnel은 Pod가 종료될 때 connection을 graceful하게 닫지 않았고, 그 결과 upstream인 waypoint는 connection이 죽었다는 사실을 알 길이 없었던 것입니다.
이 현상은 Istio 팀에 직접 리포트했습니다. 저희는 재현 과정과 waypoint 로그를 정리해 istio/ztunnel#1637 이슈로 등록했습니다. 같은 증상이 AWS VPC CNI/EKS 환경뿐만 아니라, waypoint 없이 ingress gateway만 쓰는(즉 Envoy → ztunnel) 환경에서도 보고되었습니다. 그리고, 이는 Istio Ambient mode의 공통된 문제라는 것을 확인할 수 있었습니다. connection 생명주기와 상태 관리 자체에 대한 더 넓은 논의는 별도 이슈인 istio/ztunnel#1191에서 진행되고 있습니다.
(3) waypoint의 socket 상태
마지막으로 가설이 맞다면 반드시 보여야 할 현상을 검증했습니다. in-mesh Pod가 삭제되더라도, waypoint 안에는 해당 Pod IP를 peer로 하는 socket(:15008)이 한동안 ESTABLISHED 상태로 남아 있어야 합니다. 실제로 임의의 Pod를 삭제한 뒤 socket 상태를 관찰하자, 일정 시간 동안 해당 socket이 ESTABLISHED로 남아있는 것을 확인할 수 있었습니다. 로그·pcap·socket 세 각도의 관찰이 모두 같은 결론을 가리켰고, 이로써 가설이 맞다고 판단했습니다.
5. 문제 대응
대응의 목적은 분명합니다. network 컴포넌트에서 에러가 나더라도, 그것이 애플리케이션 응답의 5xx로 전파되지 않도록 하는 것입니다. 먼저 이상적인 근본 해결책을 살펴보고, 그다음은 당장 적용할 수 있는 현실적인 방안을 이야기하겠습니다.
5.1 근본 해결책 (Istio upstream 개선이 필요한 영역)
1) Connection의 "이름"을 단순 IP:Port가 아니게 만들기
가장 깔끔한 해결은 connection을 식별하는 key 자체를 바꾸는 것입니다. Envoy ORIGINAL_DST cluster의 connection pool key에 IP:Port 외에 메타데이터(예: Pod UID)를 더하면, 같은 IP라도 다른 Pod라면 별개의 connection으로 취급됩니다. 그러면 stale connection을 재사용하는 일 자체가 원천적으로 불가능해집니다.
저희가 #1637에서 제안한 것도 이 방향입니다. 실제로 ztunnel의 HBONE connection pool은 단순히 IP만 보지 않습니다. ztunnel의 WorkloadKey는 대략 source identity + destination identity + destination address + source IP를 함께 묶고, 이 값을 hash해서 pool key로 사용합니다. 여기서 destination identity는 spiffe://<trust-domain>/ns/<namespace>/sa/<service-account> 형태입니다.
AS-IS(Envoy
ORIGINAL_DSTcluster):connection pool key ~=
10.90.142.96:15008Pod-aaa가 삭제된 뒤Pod-bbb가 같은 IP를 다시 받으면, Envoy 입장에서는 둘 다 같은 upstream으로 보일 수 있습니다.그래서
Pod-aaa로 만들었던 기존 HBONE connection을Pod-bbb요청에도 재사용할 수 있습니다.
TO-BE(ztunnel에서 볼 수 있는 방향성에 가까운 예시):
connection pool key ~=
dst=10.90.142.96:15008 + dst_id=spiffe://cluster.local/ns/default/sa/api즉 목적지 주소뿐 아니라 목적지 workload identity까지 pool key에 포함합니다.
다만 여기서 한 가지 주의할 점이 있습니다. 같은 Deployment의 새 Pod는 보통 같은 ServiceAccount를 사용하므로, dst_id만으로는 Pod-aaa와 Pod-bbb를 항상 구분할 수 없습니다. 이 문제를 원천적으로 막으려면 Pod UID처럼 Pod 인스턴스마다 달라지는 메타데이터까지 key에 포함해야 합니다.
2) Connection 상태를 더 잘 관리하기
또 다른 방향은 ztunnel이 Pod 종료 시점에 connection을 정리(graceful close)하는 것입니다. 그러면 waypoint가 이미 죽은 connection을 계속 들고 있다가 재사용하는 일을 줄일 수 있습니다. 다만 이 접근은 생각보다 까다롭습니다. 이 주제는 istio/ztunnel#1191 (improved draining)에서 논의되고 있습니다.
어려움은 크게 두 가지입니다.
첫째, 정리 신호를 보낼 타이밍과 주체가 애매합니다. 우리가 원하는 것은 Pod가 내려가기 전에 waypoint가 들고 있는 HBONE connection을 미리 정리시키는 것입니다. 그러려면 누군가 waypoint(Envoy)에게 GOAWAY 같은 신호를 보내야 합니다.
하지만 Pod(application)가 직접 보낼 수는 없습니다. ambient mode는 application에 투명해야 하므로, application은 자신의 client가 waypoint(Envoy)인지 일반 client인지 알 필요가 없어야 합니다. "종료 전에 waypoint에게 GOAWAY를 보내라"고 application 코드를 고치는 것도 ambient mode의 전제와 맞지 않습니다.
그렇다면 ztunnel이 보내야 하는데, 여기에도 문제가 있습니다. Pod가 이미 종료된 뒤에는 CNI가 veth와 network namespace를 정리하면서, ztunnel이 해당 connection을 통해 GOAWAY를 보낼 통로도 사라질 수 있습니다. 즉 "Pod가 죽었다"는 사실을 사후에 감지하더라도, 그때는 이미 정리 신호를 보낼 수 없는 상황이 됩니다.
그래서 ztunnel이 이 문제를 안정적으로 해결하려면 둘 중 하나가 필요합니다. waypoint에게 connection 정리를 알릴 별도의 control path를 갖거나, Pod가 완전히 종료되기 전에 종료 예정 상태를 미리 통지받아야 합니다. 예를 들어 ShutdownStarting 신호나 CNI DEL hook 같은 방식이 논의되는 이유가 여기에 있습니다.
둘째, GOAWAY 자체도 모든 연결을 즉시 닫는 만능 신호가 아닙니다. HBONE은 HTTP/2 CONNECT로 만든 outer connection(터널) 안에 실제 TCP 스트림인 inner connection을 실어 나르는 구조입니다. 따라서 GOAWAY를 보낸다고 해서 이미 열려 있는 inner connection들이 즉시 닫히는 것은 아닙니다.
Istio 메인테이너의 comment(#1191)도 이 지점을 설명합니다. GOAWAY는 "더 이상 active inner connection이 없어지면, 그 outer connection을 pool에 저장(재사용)하지 마라"는 신호에 가깝습니다. 즉 새 요청이 기존 outer connection을 재사용하지 않도록 막는 데에는 도움이 되지만, 이미 active한 inner connection을 즉시 정리하는 문제는 별도로 남습니다.
참고로 Ztunnel 깃헙 이슈 comment(#1191)에서 논의 중인 구체적인 방식으로는 ShutdownStarting 시 GOAWAY를 전송하는 방법, CNI DEL hook으로 "네트워크 제거 직전"에 정리하는 방법, client(ztunnel)가 Pod 삭제를 감지해 pool에서 제거하는 방법, keepalive로 timeout 정리를 유도하는 방법 등이 있습니다. 각각 타이밍과 복잡도에서 서로 다른 trade-off를 가집니다.
종합하면, 위 1)과 2)는 모두 upstream(istio/envoy) 차원의 개선이 필요한 영역이라 우리가 당장 직접 적용하기는 어렵습니다. 그래서 단기 대응이 별도로 필요했습니다.
5.2 즉시 적용할 수 있는 방안: RST에 대한 retry
지금 당장 적용할 수 있는 현실적인 대응은 RST(reset)에 대한 retry입니다.
기존 설정은 reset-before-request에 대해서만 retry하도록 되어 있었습니다. 이를 reset까지 포함하도록 확장하면, stale connection으로 인해 reset이 발생했을 때 waypoint가 자동으로 다시 시도하게 됩니다. 여기서 RST는 ztunnel application logic이 직접 "유효하지 않은 연결"이라고 판단해 보내는 것이라기보다는, 새 Pod의 network namespace 안에 기존 connection 상태가 없어서 TCP stack에서 발생하는 현상으로 보는 편이 더 정확합니다. 커뮤니티에서도 EnvoyFilter로 retry_on: reset,connect-failure,refused-stream,...을 추가하는 것이 사실상 유일하게 효과를 본 우회책으로 보고되고 있습니다.
후보로 검토했던 다른 방법으로 aggressive HTTP/2 keepalive나 HBONE idle timeout 조정도 있습니다. Istio에는 Envoy proxy가 ztunnel로 맺는 HBONE connection을 pool에 얼마나 오래 둘지 제어하는 meshConfig.hboneIdleTimeout 설정이 있고, 이 값을 짧게 잡으면 idle 상태의 stale connection을 더 빨리 정리할 수 있습니다. HTTP/2 keepalive 역시 점검 주기를 짧게 설정해 stale connection을 빠르게 감지·정리하자는 아이디어입니다. 다만 둘 다 "stale을 빨리 줄인다"는 효과일 뿐, IP 재사용 타이밍과 connection 재사용이 겹치는 상황 자체를 원천적으로 막지는 못합니다. 실제로 커뮤니티에서도 ztunnel의 KEEPALIVE_* 환경변수 조정이 이 문제 해결에는 도움이 되지 않았다는 보고가 있어, 보조 수단 정도로만 보고 있습니다.
정리하면, 근본 해결(5.1)은 upstream 개선에 의존하므로 장기 추적 과제로 남겨 두고, 단기적으로는 retry로 증상(503 전파)을 막는 전략을 택했습니다. 다만 retry는 어디까지나 증상 완화이지 근본 해결이 아니라는 점은 분명히 인지하고 있습니다.
또 하나 주의해야 할 점은, reset에 대한 retry policy를 추가하면 이번에 관찰한 waypoint → ztunnel connection 재사용 문제뿐 아니라 다른 원인으로 발생한 RST에도 retry가 trigger될 수 있다는 점입니다. 예를 들어 Pod OOM, 프로세스 크래시, 혹은 아직 원인을 모르는 unexpected RST도 같은 retry 조건에 걸릴 수 있습니다. 따라서 retry를 적용할 때는 대상 API가 멱등한지, 중복 실행이 애플리케이션 상태나 외부 시스템에 부작용을 만들지 않는지까지 함께 확인해야 합니다.
결론
결과적으로 waypoint 수준에서 reset에 대한 retry를 적용해, stale connection 재사용으로 인한 UpstreamConnectionTermination(503) 문제를 해소할 수 있었습니다.
돌이켜보면 이 문제의 핵심은 "IP가 겹쳐서 생긴 문제"가 아니었습니다. Envoy가 IP:Port로 이름 붙인 connection을, 목적지 Pod가 사라진 뒤에도 폐기하지 못한 stale connection 문제였습니다. IP 재사용은 그 stale connection을 새 Pod로 잘못 잇게 만든 방아쇠였을 뿐입니다. connection 재사용(Envoy pool, IP:Port 키)과 graceful close의 부재(ztunnel), 그리고 IP 재사용(IPAM)이라는 방아쇠가 겹치며 발생한, Ambient mode 특유의 함정이었던 셈입니다.
sidecar 시절과 달리 connection을 다루는 주체가 ztunnel과 waypoint로 분리되면서 생긴 새로운 결의 문제이기도 합니다. 근본 해결은 upstream의 개선에 기대야 하는 부분이 크지만, 적어도 로그에서 가설을 세우고 pcap과 socket으로 검증해 나간 이 추적 과정 자체는, 비슷한 문제를 만났을 때 다시 꺼내 쓸 수 있는 방법론으로 남는다고 생각합니다.
긴 글 읽어주셔서 감사합니다.
3편: 프로덕션에서 만난 당황스러운 이슈들과 트러블슈팅
3-1편: 503과 Half-open Connection (현재 글)
3-2편: Partially Enrolled Pod와 Untaint Controller
3-3편: Ambient mode 안전하게 업그레이드하기
3-4편: 부록 — 507 status code와 istiod disconnected 탐지
