# STRICT destroy_local 타이밍 규명 + LOGICAL in-flight 대조군 (raw, part2) **Date:** 2026-07-02 · **cluster:** homelab · **ns:** dns-lab · **Istio:** 1.30.0 **선행:** `2026-07-02_104250_dns-strict-inflight.md` (STRICT flip이 in-flight 요청을 자르지 않음을 2/2 재현. 단 destroy_local +1의 발생 **시점**은 before/after 2점 스냅샷으로 구분 불가 → 이번 후속 1) · LOGICAL 대조군 미실시 → 이번 후속 2) **하니스:** part1의 단일 스크립트 패턴(`curl & → sleep → flip → wait`)을 4점 스냅샷(BEFORE/MID1+12s/MID2+30s/AFTER end+3s)으로 확장 — `run4snap.sh` (스크래치패드, 아래 §4에 경로). flip/estat 방식은 `scripts/dns-flip-test.sh` 그대로. **전제 상태(시작 시 재확인):** SE=`gslb-strict`, DNS→backend-a(10.250.147.30), backend-b=10.250.27.55, 전 파드 READY. > 초안(construction 로그). 스냅샷은 전부 **netshoot 사이드카**(실제 트래픽 지점). 카운터는 누적이므로 같은 런 내 델타로만 판정. --- ## 1. 후속 1 — STRICT destroy_local 발생 시점 (4점 스냅샷, 1런) 실행: 10:55:43 curl 시작(`/slow`, 40s) → +5s flip A→B → MID1(+12s) → MID2(+30s) → +40s 완주 → AFTER(+43s). curl 결과: `CURL_EXIT=0 SIZE=409610 TIME=40.045811 CODE=200`, body="backend-a" (part1과 동일하게 무절단 완주). ### 카운터 4점 테이블 (netshoot 사이드카, 절대값) | counter | BEFORE (t=-0) | MID1 (t=+12) | MID2 (t=+30) | AFTER (t=+43) | |---|---|---|---|---| | membership_change | 17 | **18 (+1)** | 18 | 18 | | upstream_cx_active | 0 | **1** | **1** | **0** | | upstream_cx_total | 51 | **52 (+1)** | 52 | 52 | | upstream_rq_total | 415 | **416 (+1)** | 416 | 416 | | upstream_cx_destroy | 51 | 51 | 51 | **52 (+1)** | | upstream_cx_destroy_local | 7 | 7 | 7 | **8 (+1)** | | upstream_cx_destroy_with_active_rq | 1 | 1 | 1 | 1 (Δ0) | | upstream_cx_destroy_local_with_active_rq | 1 | 1 | 1 | 1 (Δ0) | | upstream_cx_destroy_remote(_with_active_rq) | 44 (0) | 44 (0) | 44 (0) | 44 (0) | | endpoint 덤프(clusters) | 10.250.147.30(a) | **10.250.27.55(b)** cx_active=0 | 10.250.27.55(b) cx_active=0 | 10.250.27.55(b) cx_active=0 | ### 읽기 - **MID1(+12s)**: flip은 이미 반영 완료 — dig=backend-b, endpoint 덤프 A→B 교체, `membership_change +1`. 그런데 **destroy 계열은 전부 정지**. cluster 레벨 `cx_active=1`(우리의 in-flight 연결)인데 per-endpoint 덤프의 backend-b는 `cx_active=0` — 즉 **활성 연결은 "이미 제거된 host(backend-a)"에 물려 있어 endpoint 목록에서는 보이지 않지만 cluster 합계에는 잡힌다.** (STRICT에서 제거된 host의 잔존 연결이 관측 사각지대에 들어가는 모습.) - **MID2(+30s)**: MID1과 완전 동일 — host 제거 후 18초가 지나도 그 연결은 계속 살아서 스트리밍 중. - **AFTER(end+3s)**: 요청이 자연 완료된 뒤에야 `destroy +1 / destroy_local +1`, `cx_active 1→0`. `destroy_with_active_rq`는 여전히 Δ0 — 닫히는 순간 연결 위에 활성 요청이 없었다. ### 결론 (한 문장) **destroy_local +1은 flip 시점이 아니라 "요청 완료 직후"에 발생한다 — STRICT_DNS는 host 제거 시 그 host의 in-flight 연결을 즉시 끊지 않고 active stream이 끝날 때까지 유예했다가(graceful drain), 완료되자마자 Envoy가 로컬에서 연결을 폐쇄한다** (MID1/MID2에서 destroy 정지 + cx_active=1 유지, AFTER에서만 +1 — part1의 "유휴 연결 파괴" 해석은 기각, "완료까지 유예 후 폐쇄"로 확정). --- ## 2. 후속 2 — LOGICAL_DNS 대조군 (동일 시나리오, 2런) ### SE 전환 (dns-flip-test.sh 방식 그대로) ``` $ kubectl --context=homelab -n dns-lab delete serviceentry gslb-strict gslb-logical --ignore-not-found serviceentry.networking.istio.io "gslb-strict" deleted $ kubectl --context=homelab apply -f scenarios/50-dns-resolution/41-serviceentry-logical.yaml serviceentry.networking.istio.io/gslb-logical created $ istioctl --context=homelab proxy-config cluster deploy/netshoot.dns-lab --fqdn gslb.lab.internal --port 443 -o json | grep '"type"' "type": "LOGICAL_DNS", <- 테스트 시작 전 확인 완료 ``` flip은 단일 IP→단일 IP만(LOGICAL + multi-IP = CDS NACK 함정 준수). DNS는 각 런 시작 전 backend-a로 원복 확인. ### Run 1 (11:04:37 시작, flip +5s, 완주 +40s) — 4점 테이블 | counter | BEFORE | MID1 (+12) | MID2 (+30) | AFTER (+43) | |---|---|---|---|---| | membership_change | 20 | **20 (Δ0)** | 20 | 20 | | upstream_cx_active | 0 | 1 | 1 | **1 (계속 유지!)** | | upstream_cx_total | 52 | 53 (+1) | 53 | 53 | | upstream_rq_total | 416 | 417 (+1) | 417 | 417 | | upstream_cx_destroy | 52 | 52 | 52 | **52 (Δ0)** | | upstream_cx_destroy_local | 8 | 8 | 8 | **8 (Δ0)** | | upstream_cx_destroy_with_active_rq(_local/_remote) | 1/1/0 | 1/1/0 | 1/1/0 | 1/1/0 (전부 Δ0) | | endpoint 덤프 | 10.250.147.30(a) | **10.250.27.55(b)** cx_active=**1** rq_active=**1** | 동일 | 10.250.27.55(b) cx_active=1 rq_active=0 | curl: `CURL_EXIT=0 SIZE=409610 TIME=40.045900 CODE=200`, body="backend-a". ### Run 2 (11:13:45 시작, flip +5~6s, 완주 +41s) — 4점 테이블 | counter | BEFORE | MID1 (+12) | MID2 (+30) | AFTER (+44) | |---|---|---|---|---| | membership_change | 20 | 20 (Δ0) | 20 | 20 | | upstream_cx_active | 0 | 1 | 1 | **1 (유지)** | | upstream_cx_total | 53 | 54 (+1) | 54 | 54 | | upstream_rq_total | 417 | 418 (+1) | 418 | 418 | | upstream_cx_destroy | 53 | 53 | 53 | **53 (Δ0)** | | upstream_cx_destroy_local | 8 | 8 | 8 | 8 (Δ0) | | upstream_cx_destroy_with_active_rq(_local/_remote) | 1/1/0 | 1/1/0 | 1/1/0 | 전부 Δ0 | | endpoint 덤프 | 10.250.147.30(a) | 10.250.27.55(b) cx_active=1 rq_active=1 | 동일 | 10.250.27.55(b) cx_active=1 rq_active=0 | curl: `CURL_EXIT=0 SIZE=409610 TIME=40.045509 CODE=200`, body="backend-a". **2/2 런 델타 패턴 동일(결정적).** (런 간 노이즈 1건: run1 AFTER→run2 BEFORE 사이 `destroy 52→53`, `destroy_remote 44→45` — 런 사이 ~8분 유휴 동안 run1이 pool에 남겨둔 backend-a 연결을 **원격(nginx keepalive timeout)** 이 닫은 것. 런 내부 델타 판정에는 무관.) ### STRICT vs LOGICAL 차이 요약 (동일 시나리오, in-flight flip) | 관측 | STRICT_DNS | LOGICAL_DNS | |---|---|---| | curl 결과 | 완주 (200, 전량, backend-a) | 완주 (200, 전량, backend-a) — **클라이언트 체감 동일** | | membership_change | **+1** (flip 반영 시) | **0** (논리 host 1개 유지) | | upstream_cx_destroy(_local) | **+1, 단 요청 완료 직후에만** | **0 — 아예 발생 안 함** | | 요청 완료 후 연결 | **즉시 폐쇄** (cx_active 1→0) | **pool에 계속 유지** (cx_active=1 잔존, 재사용 대기) | | endpoint 덤프의 활성 연결 표시 | 제거된 host의 연결이 **목록에서 사라짐** (cluster 합계로만 보임) | 새 IP 라벨 아래 cx_active=1로 표시되지만 **실제 바이트는 옛 IP로** (라벨 함정, 2026-07-01 §4-1 재확인) | | 다음 요청의 행선지 | backend-b (새 연결) | 이 연결이 재사용되면 **backend-a 계속** | 핵심: in-flight 보호는 양쪽 동일하지만, **요청이 끝난 뒤의 운명이 갈린다** — STRICT는 "완료 즉시 폐쇄 → 다음 요청은 반드시 새 IP", LOGICAL은 "연결 존속 → 다음 요청도 옛 IP 재사용 가능". GSLB 추종 속도 차이는 in-flight 절단이 아니라 이 지점에서 발생. --- ## 3. 이상 징후 · 미해결 의문 - **STRICT의 관측 사각지대**: MID1/MID2에서 cluster `cx_active=1`인데 per-endpoint 덤프에는 그 연결이 안 보인다(host가 이미 목록에서 제거됨). 운영에서 `GET clusters`/`proxy-config endpoints`만 보면 "drain 끝났다"고 오판할 수 있음 — cluster 레벨 `upstream_cx_active`와 대조 필요. - **LOGICAL AFTER의 cx_active=1 잔존**: 요청이 끝나도 연결이 pool에 남는다(이후 원격 keepalive timeout으로 정리됨을 런 간 노이즈로 관측). 이 연결이 다음 요청에 실제 재사용되어 who=backend-a가 이어지는지는 이번 런에서 직접 검증하지 않음 (2026-07-01 mode1 LOGICAL에서 40s 루프로 이미 관측된 바 있어 중복 생략). - STRICT drain 유예의 상한(예: Envoy가 영원히 기다리는가, drain timeout이 있는가)은 미검증 — 40s보다 훨씬 긴(수분) 스트림으로 별도 확인 필요. HTTP/1.1 vs HTTP/2(GOAWAY 경로)에서 거동이 다를 수 있음도 미검증. --- ## 4. 원시 로그 / 종료 상태 스크래치패드 `/tmp/claude-1000/-mnt-homelab-kakaopay-istio/9efbaa2c-8577-4534-8d98-d97a7cc0747e/scratchpad/dns-strict-inflight/`: ``` run4snap.sh (4점 스냅샷 하니스 — curl& / sleep_until 절대시각 스케줄) strict4snap_full.txt (후속 1 전체 출력) + strict4snap_curl_out.txt logical_run1_full.txt (후속 2 run1) + logical_run1_curl_out.txt logical_run2_full.txt (후속 2 run2) + logical_run2_curl_out.txt ``` 종료 상태 (11:21 확인): ``` $ kubectl --context=homelab -n dns-lab get serviceentry gslb-strict ["gslb.lab.internal"] MESH_EXTERNAL DNS <- 원복 $ istioctl --context=homelab proxy-config cluster deploy/netshoot.dns-lab --fqdn gslb.lab.internal --port 443 -o json | grep type "type": "STRICT_DNS", <- 원복 $ kubectl --context=homelab -n dns-lab exec deploy/netshoot -- dig +short gslb.lab.internal 10.250.147.30 <- backend-a 원복 $ kubectl --context=homelab -n dns-lab get pods backend-a 1/1 · backend-b 1/1 · fortio 2/2 · lab-dns 2/2 · netshoot 2/2 <- 전부 READY $ kubectl ... exec deploy/netshoot -- curl -s -m 5 http://gslb.lab.internal/ backend-a <- mesh 경로 sanity 정상 ``` `/slow` manifest(`20-backends.yaml`) 유지.