---
title: SE resolution(STRICT/LOGICAL) × TLS passthrough(L4) — in-flight 요청의 운명
date: 2026-07-09
type: note
domain: istio
tags: [istio, egress, dns, gslb, service-entry, tls-passthrough, envoy, resolution]
---
> [!abstract]
> `ServiceEntry.resolution`(`DNS`→STRICT_DNS vs `DNS_ROUND_ROBIN`→LOGICAL_DNS)과 트래픽 경로(L7 TLS
> Origination vs L4 TLS Passthrough)를 조합하면 2×2 매트릭스가 나온다. GSLB가 flip을 걸었을 때 이미
> 진행 중(in-flight)인 요청은 **네 칸 모두**에서 완주한다 — 이건 이번 실측으로 확정됐다. 이 문서가 답하는
> 진짜 질문은 "안 끊긴다"가 아니라 **"안 끊기는 이유가 칸마다 다른가, 그리고 요청이 끝난 다음엔 무슨 일이
> 벌어지는가"**다. 네 칸의 실측 근거와 그 사이의 메커니즘 차이를 정리한다.
---
## 1. 질문
**SE의 `resolution` 설정(`DNS`=STRICT_DNS vs `DNS_ROUND_ROBIN`=LOGICAL_DNS)에 따라, TLS
passthrough(L4) 경로에서 GSLB flip이 일어날 때 진행 중인(in-flight) 요청은 어떻게 처리되는가?**
이 질문은 두 축의 조합이다 — **resolution**(STRICT/LOGICAL)과 **경로**(L7 origination/L4
passthrough). 답은 이 문서 전체에 걸쳐 2×2 매트릭스 하나로 완결한다(§3). "L7이 L4보다 안전한가"
같은 넓은 보호-경계론은 배경(§2)으로만 다루고 본론으로 확장하지 않는다 — 그 주제의 정본은 이미
[TLS passthrough(L4) 대조 리포트](#참조)에 있다. dead-IP 유실 회복(D-1/D-3의 retry 이야기)도
"L4에는 무엇이 없는지"를 보여주는 배경으로만 짧게 인용한다(§4-4).
> **실측/추론 표기 규칙**: 이 문서의 모든 주장은 클러스터 실측(홈랩, Istio 1.30.0)이거나 그 실측에서
> 논리적으로 따라오는 메커니즘 추론이다. 실측하지 못한 부분은 문장 끝에 **(메커니즘 추론 — 미실측)**을
> 명시한다. 표기가 없으면 실측 근거가 있는 문장이다.
---
## 2. 배경 — sidecar egress의 두 경로
`resolution`이 Envoy cluster type을 STRICT_DNS/LOGICAL_DNS 중 무엇으로 바꾸는지의 변환 규칙 자체는
이 문서의 범위가 아니다 — [ServiceEntry `resolution` 정본](/docs/istio/egress/report-dns-resolution/)
§3이 이미 정리했다. 여기서 필요한 건 그 위에 얹히는 두 번째 축, **경로**다.
client가 sidecar에 평문 HTTP로 요청을 보내고 sidecar가 TLS를 **origination**하면(40/41 SE + 42 VS +
43 DR 조합), Envoy는 HTTP 요청/응답의 경계를 안다. 그래서 RDS route가 붙고, 그 route 위에 connection
pool이 얹히고, pool 안에서 keepalive 연결이 여러 요청에 재사용된다. 반대로 client가 처음부터 TLS를
직접 맺고 sidecar는 SNI만 보고 통과시키면(48/50 SE, `protocol: TLS`, VS/DR 없음), Envoy에게는 그냥
바이트 스트림이다 — SNI 매치로 filter chain을 고른 다음 바로 `tcp_proxy`가 raw 소켓을 1:1로 잇는다.
route도, pool도 없다.
그림 1. sidecar egress의 두 경로. L7은 HTTP filter chain → RDS route → connection pool을 거쳐 "요청 경계"가 있고, L4는 SNI 매치 직후 곧바로 tcp_proxy가 raw 소켓을 1:1로 잇는다 — 중간에 pool 단계 자체가 없다.
"요청 경계가 있다/없다"는 이 문서 전체를 관통하는 유일한 구조적 차이다. pool이 있으면 그 위에 drain
유예·재사용·stale pin 같은 **커넥션 수명 정책**이 얹힐 자리가 생긴다. pool이 없으면 그 자리 자체가
없다 — 있고 없고의 문제가 아니라 **개념이 성립하지 않는다.** §4가 이 차이를 4칸에 대입해서 푼다.
---
## 3. 실측 매트릭스
그림 2. resolution × 경로 2×2 매트릭스 요약. 색은 "완료 후 다음 연결의 예측 가능성"을 뜻한다 — 기본(회색 테두리)은 다음 IP가 구조적으로 결정되고, 호박색(LOGICAL×L7)은 옛 IP로 재사용될 위험(stale pin)이 실측으로 확인된 칸이다.
네 칸의 근거를 표로 펼치면 다음과 같다. 모두 40초 in-flight 스트림(`/slow`, payload 409610바이트) +
flip(+5s) + 4점 스냅샷(BEFORE/MID1 +12s/MID2 +30s/AFTER +43s) 하니스로 측정했다.
| | **L7 origination**(HTTP, pool) | **L4 passthrough**(SNI, raw socket) |
|---|---|---|
| **STRICT_DNS** `resolution: DNS` | 완주(200·409610B·body=backend-a). `membership_change`는 flip 반영과 함께 **+1**(MID1=+12s 시점에 이미 host A→B 교체 완료). `upstream_cx_destroy_local`은 **요청 완료 직후에만** +1 — host가 이미 목록에서 빠졌어도 in-flight 연결은 능동적으로 drain 유예된다. 다음 신규 연결은 반드시 새 IP(backend-b)로 붙는다. *(2026-07-02_112113_dns-inflight-part2.md §1)* | 완주(200·409610B·최초 접속 pod 유지). `membership_change`는 **지연 반영**(MID1=+12s엔 아직 pod-A만 보임, MID2=+30s에야 pod-B로 교체·membership_change +1). destroy도 요청 자연 종료 시에만 +1 — 단 이건 "유예"가 아니라 raw 소켓이 애초에 CDS/EDS 갱신과 무관하게 살아있었을 뿐이다. *(2026-07-09_134250_dns-passthrough-tcp.md §5, D-2)* |
| **LOGICAL_DNS** `resolution: DNS_ROUND_ROBIN` | 완주(200·409610B·body=backend-a, 2/2 결정론적). `membership_change`는 BEFORE→AFTER 전 구간 **Δ0**(논리 host 1개 유지, flip을 이벤트로 취급 안 함). `upstream_cx_active=1`이 AFTER(+43s)에도 그대로 남는다 — destroy 자체가 관측 창 안에서 발생하지 않는다(연결이 pool에 잔존, 재사용 대기 → **stale pin 지속 가능**). *(2026-07-02_112113_dns-inflight-part2.md §2)* | 완주(200·409610B·최초 접속 pod 유지, 2/2 결정론적). `membership_change`는 BEFORE→AFTER 전 구간 **11→11 고정** — flip이 반영돼도 "논리적으로는 여전히 host 1개"라 membership 갱신 개념 자체가 없다. admin의 endpoint 표시 IP는 MID1(+12s)에 이미 새 IP로 갱신되지만, 이건 **다음 연결이 참조할 해석 캐시**가 바뀐 것이지 membership 이벤트가 아니다. *(2026-07-09_144251_dns-passthrough-logical-inflight.md §3~4, F-0)* |
---
## 4. 칸 사이 차이의 메커니즘
그림 3. 40초 in-flight 스트림 동안의 membership/destroy 이벤트 타임라인. 점(●)은 실제로 카운터가 틱한 지점이다 — L7 STRICT만 flip 직후(+12s 이내)에 membership이 갱신되고, 나머지 세 레인은 그보다 늦거나(L4 STRICT) 아예 갱신되지 않는다(LOGICAL 둘 다). destroy는 네 레인 중 셋에서 정확히 스트림이 끝나는 시점(+43s)에만 틱하고, LOGICAL×L7 레인만 관측 창 안에서 전혀 틱하지 않은 채 연결이 이어진다.
### 4.1 네 칸 모두 생존하지만, 보증 수준은 다르다
in-flight 생존은 네 칸 전부에서 성립한다(§3). 하지만 그 생존이 **무엇에 의한 것인지**가 다르다.
- **L7 origination(둘 다)**: Envoy가 요청 경계를 안다. connection pool이 "이 연결 위에 아직 활성
요청이 있다"를 추적하고, host가 제거돼도 그 요청이 끝날 때까지 destroy를 미룬다. **이건 설계된
동작이다** — HTTP1.1 keepalive·HTTP2 GOAWAY류의 graceful shutdown과 같은 계열.
- **L4 passthrough(둘 다)**: `tcp_proxy`는 한 번 맺은 커널 소켓을 그냥 릴레이할 뿐이다. CDS/EDS가
나중에 그 host를 지워도 이미 진행 중인 연결과는 **애초에 무관하다** — 정리할 지점 자체가 없다.
D report는 이 차이를 정확히 짚었다: "우연히 안전한 것이지 설계로 안전한 게 아니다"
(2026-07-09_134250_dns-passthrough-tcp.md §5.3).
결과의 유사성(둘 다 완주)에 속아 "L4도 L7만큼 안전하다"고 일반화하면 안 된다. **완료까지 살아남는
것**과 **완료 후에도 신뢰할 수 있는 장애 복구 체계를 갖추는 것**은 별개 질문이고, 후자는 §4.4에서
갈린다.
### 4.2 L4엔 재사용 pool이 없다 — LOGICAL의 stale pin이 구조적으로 희석된다
L7×LOGICAL 칸의 진짜 위험은 "이번 요청이 안 끊긴다"가 아니라 "**다음 요청도 같은(옛) 커넥션을
재사용해서 계속 backend-a로 간다**"는 stale pin이다 — 이건 connection pool이 있기 때문에 생기는
문제다. D report가 명시적으로 확인한 사실은 "TCP passthrough엔 connection pool 자체가 없다"
(2026-07-09_134250_dns-passthrough-tcp.md §5.3)는 것이다. `tcp_proxy`는 다운스트림 연결마다 새
업스트림 소켓을 1:1로 여는 구조라, HTTP처럼 "여러 요청이 하나의 커넥션을 keepalive로 나눠 쓰는"
개념 자체가 없다.
이 사실에서 따라오는 결론 — **L4에서는 매 신규 연결마다 그 시점의 DNS/EDS 해석을 새로 타므로,
L7×LOGICAL에서 본 stale pin 문제가 구조적으로 희석된다(메커니즘 추론 — 미실측)**. 이번 실측은
전부 "이미 맺은 하나의 연결"의 생존만 봤을 뿐, flip 이후 **새로 여는** L4 연결이 실제로 몇 초 만에
새 IP를 따라가는지는 별도로 측정하지 않았다. STRICT×L4에서 관측한 "membership 지연 반영"(§3)이
새 연결의 목적지 선택에 얼마나 영향을 주는지, LOGICAL×L4에서 admin 표시가 MID1(+12s)에 이미
갱신된다는 사실이 새 연결에도 그대로 적용되는지는 이 문서의 실측 범위 밖이다.
### 4.3 이 매트릭스가 성립하는 전제 — LOGICAL + multi-IP는 CDS NACK
네 칸 모두 flip은 "단일 IP → 단일 IP"로만 걸었다. LOGICAL_DNS cluster는 `lb_endpoint`가 1개여야
하는데, A record가 다중 IP가 되는 순간 이 SE를 받는 프록시가 CDS push를 통째로 NACK할 수 있다는
사고를 이전 세션에서 이미 겪었다(같은 push의 listener까지 드롭 —
[DNS/GSLB 재현 랩](/docs/istio/egress/dns-gslb-repro-lab/) §7). 그래서 LOGICAL이 들어간 두 칸
(L7×LOGICAL, L4×LOGICAL)은 이 제약 위에서만 유효하다 — GSLB가 실제로 멀티 IP A record를 돌려주는
환경이라면 LOGICAL_DNS 자체를 쓸 수 없는 경우가 생긴다는 뜻이고, 이건 이 매트릭스의 전제 조건이지
매트릭스 내부의 발견은 아니다.
### 4.4 dead-IP 구제(L7 기본 retry)는 L4에 없다 — 배경으로 짧게
이 문서의 중심은 in-flight(§3~4.3)이지만, "요청이 끝난 뒤"의 이야기를 완전히 빼면 왜 이 차이가
프로덕션에서 중요한지 감이 안 온다. D-1(같은 40초 창, 같은 dead-IP 조건)에서 L7 경로는 connect-fail
자동 재시도로 46/46 성공(유실 0%)했지만, L4 passthrough는 route 자체가 없어 이 재시도가 걸릴 자리가
없어 18/41(43.9%)이 client에게 그대로 실패로 전달됐다(2026-07-09_134250_dns-passthrough-tcp.md
§3). outlier detection을 걸면 43.9%→0.21%까지 떨어지지만 정확히 0엔 안 닿는다(D-3, 같은 문서 §6).
in-flight는 4칸 모두 보호되지만, **연결이 끊어진 뒤 새로 맺으려는 시도**는 L4에서 훨씬 더 무방비다 —
자세한 수치와 원인은 원본 리포트를 참조.
### 4.5 공식 문서와의 대조 — drain 의미론 확인
§3~4.4가 실측으로 확정한 "STRICT는 host가 빠져도 in-flight는 안 끊고 완료 후에만 폐쇄한다", "LOGICAL은
아예 개입하지 않는다"는 결과가 우연이 아니라 [Envoy의 service discovery 아키텍처
문서](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/service_discovery)에
그대로 명문화돼 있다는 것을 이 절에서 확인한다.
> **Strict DNS** — "Envoy will continuously and asynchronously resolve the specified DNS targets.
> Each returned IP address in the DNS result will be considered an explicit host in the upstream
> cluster. … If a host is removed from the result Envoy assumes it no longer exists and **will
> drain traffic from any existing connection pools**. … Note that Envoy never synchronously
> resolves DNS in the forwarding path."
>
> → DNS 대상을 지속적·비동기적으로 해석한다. 반환된 각 IP는 cluster의 명시적 host로 간주된다. host가
> 결과에서 사라지면 더 이상 존재하지 않는 것으로 간주하고 기존 connection pool로부터 트래픽을
> **drain한다**. Envoy는 포워딩 경로에서 절대 동기적으로 DNS를 해석하지 않는다.
> **Logical DNS** — "…a logical DNS cluster only uses the first IP address returned **when a new
> connection needs to be initiated**. Thus, a single logical connection pool may contain physical
> connections to a variety of different upstream hosts. **Connections are never drained**,
> including on a successful DNS resolution that returns 0 hosts. With logical DNS, connections
> stay alive until they get cycled."
>
> → **새 연결을 시작해야 하는 시점에** 반환돼 있는 첫 번째 IP만 사용한다. 그래서 하나의 논리적
> connection pool 안에 서로 다른 실제 host로의 물리 연결들이 공존할 수 있다. 연결은 **절대
> drain되지 않는다** — DNS가 host 0개를 반환한 경우조차. 연결은 순환(교체)될 때까지 살아있다.
**동사가 "close"가 아니라 "drain"이다.** §3의 STRICT×L7 칸에서 `destroy_local`이 flip 시점이 아니라
요청 자연 종료 직후에만 틱한 것(§14-4/part2 §1 원표)이 바로 이 "drain" 동작 그 자체다 — 새 트래픽
배정은 즉시 멈추고(membership에서 제거), 이미 진행 중인 사용은 끝날 때까지 기다렸다가 닫는다. 문서
문장 하나에 실제로는 **시점이 다른 두 사건**이 들어 있는 셈이다: ① host 목록(라우팅 후보) 갱신은
**즉시**, ② 기존 연결의 처분(drain)은 **정중하게**. 이 구분을 놓치면 "host 제거 = 그 자리에서 연결
강제 종료"로 오독하기 쉽다.
**`close_connections_on_host_set_change`가 opt-in이라는 증거**: 기본 동작이 "host 변경 시 즉시 강제
정리"였다면 이 필드가 존재할 이유가 없다. [cluster.proto의
`CommonLbConfig`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto)는
이렇게 정의한다:
> `close_connections_on_host_set_change` — "If set to `true`, the cluster manager will drain all
> existing connections to upstream hosts whenever hosts are added or removed from the cluster."
> (기본값 **false**)
>
> → `true`로 설정하면, host가 cluster에 추가되거나 제거될 때마다 cluster manager가 기존 연결을
> 전부 drain한다. (기본값은 `false`.)
이 랩의 SE(40/41/48/50)는 전부 이 필드를 건드리지 않았다 — 즉 §3의 모든 관측은 **기본값(false)에서
나온 결과**다. 기본값이 이미 "즉시 강제 정리"라면 `true`로 바꿀 때 새로 생기는 동작이 없어야 하는데,
필드 설명 자체가 "drain all existing connections"를 `true`일 때의 효과로 명시한다 — 이는 기본값에서는
이 즉시-drain이 **일어나지 않는다**는 뜻이고, §3에서 실측한 "완료까지 유예"와 정확히 들어맞는다.
**"질의(query) vs 소비(consume)" 구분 — §4.2의 근거**: 공식 문서는 STRICT/LOGICAL 모두 "continuously
and asynchronously resolve"한다고 말한다 — **질의 주기는 두 타입이 같다.** 갈리는 지점은 그 결과를
**언제 반영하는가**다.
- STRICT: 백그라운드 질의 결과가 도착하는 즉시 host 목록 자체를 교체한다 → §3에서 관측한
`membership_change`가 이 반영이다.
- LOGICAL: 결과를 캐시해 두고, **"새 연결을 시작해야 하는 시점"에만** 그 캐시의 첫 IP를 참조한다 —
연결을 맺는 순간 DNS를 동기적으로 다시 묻는 게 아니다("Envoy never synchronously resolves DNS in
the forwarding path"는 STRICT/LOGICAL 공통 제약이다). §3의 LOGICAL×L7/L4 두 칸에서
`membership_change`가 각각 Δ0·11→11 고정으로 나온 것은 "질의를 안 해서"가 아니라 **"질의 결과를
소비할 새 연결이 이 관측 창 안에서 발생하지 않아서"** 다 — 이미 맺어진 연결은 질의 결과와 무관하게
그대로 유지된다.
**"cycled" 해석**: "connections stay alive until they get cycled"의 cycled는 Envoy가 능동적으로
갈아타는 이벤트가 아니라, **연결의 자연스러운 수명 종료**(원격 keepalive timeout, idle timeout,
클라이언트 종료, 연결당 요청 수 상한 등)로 죽은 다음에 **다음 새 연결이 그 시점의 최신 캐시를
참조**하는 것을 말한다. §3-LOGICAL×L7 런 사이에서 관측한 "run1이 pool에 남긴 backend-a 연결을 8분
뒤 원격 nginx keepalive timeout이 닫은" 사례(part2 §2 "런 간 노이즈")가 바로 이 cycling이다 — Envoy가
GSLB 신호를 보고 갈아탄 게 아니라, 연결이 다른 이유로 죽자 그 다음 연결이 마침 새 IP를 물었을 뿐이다.
---
## 5. 실측 함정 콜아웃 — ClusterIP 섀도잉
> [!warn] DNS가 Service ClusterIP를 가리키면 이 매트릭스 전체가 조용히 무효화된다
> L4 passthrough SE(48/50)의 host를 backend Service의 **ClusterIP**로 두면 curl은 200을 반환하는데
> passthrough cluster의 카운터(`upstream_cx_total`)는 전혀 늘지 않는다 — Istio sidecar가 in-mesh
> Service ClusterIP마다 만들어 둔 **전용 리스너**가 `SO_ORIGINAL_DST`로 목적지를 가로채, 0.0.0.0:443의
> SNI 캐치올보다 항상 먼저 매치되기 때문이다. §3~4의 모든 L4 측정은 DNS를 backend **pod IP**로
> 돌려서 이 함정을 피한 뒤에 나온 값이다. 겉으로는 전혀 티가 안 나는 함정이라(curl 200), GSLB/외부
> 도메인을 흉내내는 랩·테스트 환경 전반에서 재발할 수 있다 — 상세 메커니즘과 해결책은
> [TLS passthrough 대조 리포트](#참조) §2, [DNS/GSLB 재현 랩](/docs/istio/egress/dns-gslb-repro-lab/)
> §8-3에 있다.
---
## 6. 프로덕션 시사점
| 조합 | 조심할 것 |
|---|---|
| **STRICT × L7** | 기본값처럼 다루기 좋다 — flip 시 in-flight는 보호되고, 완료 후엔 반드시 새 IP로 넘어간다. GSLB 추종이 빠르고 예측 가능해야 하는 경로에 적합. |
| **LOGICAL × L7** | in-flight는 안전하지만, **완료된 연결이 pool에 남아 다음 요청도 옛 IP로 갈 수 있다(stale pin)**. GSLB가 장애 failover 목적이면 이 지연이 치명적일 수 있다 — pinned backend가 죽어야만 재연결이 강제된다([재현 랩 mode3](/docs/istio/egress/dns-gslb-repro-lab/) 참조). |
| **STRICT × L4** | in-flight는 안전하지만 membership 반영 자체가 client 쪽 DNS 갱신보다 느리다(+12s에도 아직) — 운영 중 "flip이 반영 안 됐다"고 오판하기 쉽다. 신뢰할 진단은 client `dig`가 아니라 cluster 레벨 `proxy-config endpoints`/`membership_change`. |
| **LOGICAL × L4** | in-flight는 안전하지만 membership이라는 신호 자체가 없다 — flip을 관측하려면 endpoint 표시(해석 캐시)를 봐야 하고, "membership이 안 바뀌었으니 flip이 안 됐다"는 판단은 틀렸다. |
| **L4 passthrough 전반(둘 다)** | dead-IP 재시도가 sidecar 레벨에 없다(§4.4) — outlier detection + 클라이언트 자체 재시도를 반드시 별도로 갖춰야 한다. "TLS passthrough니까 사이드카가 알아서 해줄 것"이라는 가정은 이번에도 틀렸다. |
---
## 참조
**아카이브 내부**
- [Q&A — GSLB가 IP를 바꾸면 내 요청은 어떻게 되나 (in-flight의 운명, 쉬운 정리)](/docs/istio/egress/gslb-flip-inflight-qa/) — 이 문서의 매트릭스·수치를 질문 순서로 풀어 쓴 쉬운 버전, 실측 부록에 원표 재수록
- [ServiceEntry `resolution` 정본](/docs/istio/egress/report-dns-resolution/) — `resolution` → cluster type 변환 규칙, dead-IP 처리, GSLB 권장
- [DNS/GSLB resolution 재현 랩](/docs/istio/egress/dns-gslb-repro-lab/) §8-1(A, STRICT in-flight)·§8-2(B, dead-IP 지연세)·§8-3(D, TLS passthrough 대조) — 이 문서가 인용하는 실측의 랩 컨텍스트
- [circuit-breaking 메커니즘(outlier detection 정본)](/docs/istio/egress/circuit-breaking-mechanisms/)
**원시 리포트(repo)**
- `docs/test-reports/2026-07-02_112113_dns-inflight-part2.md` — L7 STRICT/LOGICAL in-flight 4점 스냅샷(§3 매트릭스 좌측 열의 출처)
- `docs/test-reports/2026-07-09_134250_dns-passthrough-tcp.md` — L4 STRICT 대조(D-1/D-2/D-3, §3 매트릭스 우측 상단 및 §4.4의 출처)
- `docs/test-reports/2026-07-09_144251_dns-passthrough-logical-inflight.md` — L4 LOGICAL in-flight(F-0, §3 매트릭스 우측 하단의 출처, 매트릭스 마지막 칸)
- `docs/test-reports/2026-07-01_dns-resolution.md` §14 — 종합 리포트(위 세 리포트를 흡수해 재정리)
**다운로드 가능 사본(fileshare)**
- [F-0 리포트](https://files.homelab89.com/istio-egress/dns-resolution/reports/2026-07-09_144251_dns-passthrough-logical-inflight.md) · [50-serviceentry-passthrough-logical.yaml](https://files.homelab89.com/istio-egress/dns-resolution/manifests/50-serviceentry-passthrough-logical.yaml) · [dns-passthrough-inflight.sh(수정본)](https://files.homelab89.com/istio-egress/dns-resolution/scripts/dns-passthrough-inflight.sh) · [종합 리포트(§14-7 반영본)](https://files.homelab89.com/istio-egress/dns-resolution/reports/2026-07-01_dns-resolution.md)