--- type: guide tags: [istio, egress, serviceentry, dns, gslb, resolution, envoy, mental-model, how-to] created: 2026-07-01 --- # GSLB 뒤 DNS resolution 재현 랩 — "세션이 끊길 수도 있다"를 눈으로 보기 > [!abstract] > [ServiceEntry `resolution` 정본](gw__report-2026-06-07_dns-resolution.html)은 `DNS`(STRICT_DNS)와 > `DNS_ROUND_ROBIN`(LOGICAL_DNS)의 차이를 이론으로 정리했다. 이 문서는 그 이론을 **자기완결형 랩으로 > 라이브 재현**한다 — 공유 인프라(egress gateway·cluster CoreDNS)를 한 줄도 안 건드리고, 사설 GSLB > 시뮬레이터 하나로 "IP가 매번 바뀌는 도메인"을 통제해 ① 기존 세션이 끊기는 순간 ② 죽은 IP로 트래픽이 > 새는 순간 ③ LOGICAL_DNS가 그 대가로 stale IP를 뒤늦게 붙잡고 있는 순간을 각각 만든다. > 결론 한 문장: **resolution 필드는 "GSLB의 변덕을 Envoy가 펼쳐서 매번 반영하느냐(STRICT), 접어서 > 기존 연결은 안 건드리느냐(LOGICAL)"의 선택이고, 그 선택이 곧 세션 생존 여부다.** **대상 환경:** homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio **1.30.0** **대상 독자:** STRICT_DNS/LOGICAL_DNS 차이를 *이론으로는* 아는데, "진짜 세션이 끊기는 장면"을 직접 보고 싶은 사람 **선행 개념:** [ServiceEntry resolution 정본](gw__report-2026-06-07_dns-resolution.html)(STRICT_DNS/LOGICAL_DNS, DNS refresh ≠ health check), TLS origination vs passthrough, [outlier detection](gw__note-circuit-breaking-mechanisms.html) **다루는 것:** ① 왜 자체 GSLB 시뮬레이터인가 ② 멘탈모델·부품표 ③ 실제 구성(전체 YAML + 이유) ④ 3가지 재현 모드 ⑤ 함정 --- ## 1. 배경 — 이론은 있는데 왜 "재현"이 따로 필요한가 기존 runbook은 `resolution` 필드가 Envoy cluster type을 가른다는 것, DNS refresh가 liveness가 아니라는 것, GSLB 뒤엔 LOGICAL_DNS가 낫다는 것까지 전부 정리했다. 그런데 이 문서들은 전부 **정적 조사**다 — "endpoint가 몇 개 잡히는가"는 dig+proxy-config로 스냅샷을 찍으면 끝나지만, "GSLB가 IP를 바꾸는 그 순간 이미 맺혀 있던 연결이 끊기는가/유지되는가"는 **시간축이 있는 이벤트**라 스냅샷으로는 못 본다. 이걸 보려면: 1. **GSLB를 내가 통제**해야 한다 — 실제 GSLB는 언제 IP를 바꿀지 내가 못 정한다. 2. **롱커넥션이 있어야** 한다 — 매 요청 새 커넥션이면 애초에 "끊길 기존 세션"이 없어 STRICT든 LOGICAL이든 차이가 안 보인다. 3. **공유 인프라를 안 건드려야** 한다 — egress gateway나 cluster CoreDNS를 실험용으로 고치면 다른 시나리오 (10/20/30-*)에 영향이 번진다. 이 세 요구를 동시에 만족하려고 `scenarios/50-dns-resolution/`에 **사설 GSLB 시뮬레이터 + keepalive 부하생성기**로 된 독립 ns(`dns-lab`)를 만들었다. 아래는 그 구성을 어떻게, 왜 그렇게 만들었는지다. --- ## 2. 멘탈모델 한 문장 > **GSLB는 "같은 도메인, 매번 다른 IP"를 주는 존재일 뿐이다. `resolution` 필드는 그 변화를 Envoy가 > "펼쳐서 매번 반영"(STRICT_DNS)할지 "접어서 기존 연결은 안 건드리고 신규만 반영"(LOGICAL_DNS)할지를 > 정하고, 그 선택이 곧 "GSLB가 IP를 바꾸는 순간 내 세션이 죽느냐 사느냐"다.** ``` resolution: DNS -> Envoy STRICT_DNS host set = A record 전체. DNS refresh 시 사라진 IP의 host를 cluster에서 제거 -> 그 host로 물려있던 커넥션을 정리(drain) => 기존 세션 끊김 [Mode 1] -> refresh 전 "죽었지만 아직 목록에 있는" IP를 LB가 고르면 connect 실패 => 유실 [Mode 2] resolution: DNS_ROUND_ROBIN -> Envoy LOGICAL_DNS host = 논리적 endpoint 1개. 새 커넥션만 최신 DNS로 연결 기존 커넥션은 "절대 건드리지 않음" => 기존 세션 유지 [Mode 1 대조] 대가: 물고 있는 IP가 나중에 죽어도 Envoy는 DNS로는 못 알아챔 [Mode 3] ``` 세 대괄호 표시(Mode 1/2/3)가 이 랩이 실제로 만드는 세 장면이다. 이 한 문장 + 세 장면만 머리에 있으면 나머지 구성은 전부 "이 장면을 어떻게 세팅하나"의 디테일이다. --- ## 3. 부품표 — 각 조각 = 그게 없으면 안 되는 이유 | 답해야 할 질문 | 부품 | 왜 이게 필요한가 | |---|---|---| | "GSLB를 어떻게 내 손으로 흉내내나?" | **lab-dns**(CoreDNS + writer 사이드카) | `gslb.lab.internal` 1개 이름을 authoritative(ttl 5s)로 응답. `writer`가 A record를 실시간 재작성 = "GSLB가 IP를 바꾸는 순간"을 내가 만든 버튼 | | "그 도메인을 누가 질의하나?" | **client dnsConfig** | sidecar Envoy(c-ares)가 cluster DNS 대신 lab-dns를 보게 강제 — egress gateway·cluster CoreDNS 무변경으로 GSLB를 격리 | | "펼치나 접나?" | **ServiceEntry.resolution** | `DNS`=STRICT_DNS(펼침) / `DNS_ROUND_ROBIN`=LOGICAL_DNS(접음) — 이 랩이 대조하는 유일한 독립변수 | | "drain이 눈에 보이는 층(L7)에서 일어나게 하려면?" | **VS(80→443) + DR(TLS origination)** | client가 평문 HTTP로 부르고 sidecar가 TLS로 승격해야 Envoy가 upstream을 HTTP conn pool(L7)로 관리 → host 제거 시 GOAWAY/close가 **관측 가능**해짐. https 직접 호출이면 SNI passthrough(L4)라 drain이 안 보임 | | "죽은 IP를 어떻게 밀어내나?" | **DR outlierDetection + VS retries** | Mode 2(유실)를 0으로 수렴시키는 복구 데모 — TLS origination(L7)이라 `http.retries`가 실제로 동작(기존 egress passthrough 랩의 함정과 반대) | --- ## 4. 공간 지도 — 무엇이 어디에 앉나 ``` +------------------- ns: dns-lab (istio-injection=enabled) --------------------+ | | | [client: fortio/netshoot] dnsPolicy:None | | dnsConfig.nameservers = [lab-dns ClusterIP] | | | http://gslb.lab.internal (평문 HTTP, keepalive) | | v istio-proxy: SE.resolution -> STRICT_DNS | LOGICAL_DNS | | Envoy(c-ares) --A? gslb.lab.internal--> [lab-dns: CoreDNS] | | | ^ flip = writer(busybox)가 | | | | /hosts/addn 재작성 (+reload 2s) | | | VS:80->443 + DR:TLS origination(SIMPLE) | | +--TLS--> [backend-a svc/ClusterIP-1] -> "backend-a" | | +--TLS--> [backend-b svc/ClusterIP-2] -> "backend-b" | | | | lab-dns: gslb.lab.internal 만 authoritative(ttl 5s), | | 나머지(istiod 등)는 cluster DNS로 forward | +------------------------------------------------------------------------------+ egress gateway 미경유 · cluster CoreDNS 무변경 -> blast radius = dns-lab 내부 ``` **공간 배치가 말해주는 것**: GSLB 시뮬레이터(lab-dns)와 그 flip 스위치(writer)는 인프라 쪽에, 두 backend는 "GSLB가 고르는 IP 후보"로서 대칭 배치, client만 실험 변수(resolution)에 반응한다. 이 구조라야 **변경 지점이 client의 `dnsConfig` 하나로 좁혀져** 공유 인프라에 손대지 않고 실험이 끝난다. --- ## 5. 구성 따라가기 — 실제로 이렇게 만들었다 의존 순서대로 `namespace → lab-dns → backends → client → (SE 변형 중 택1) → VS/DR`로 채운다. ### 5.1 lab-dns — GSLB 시뮬레이터 (여기가 이 랩의 심장) ```yaml apiVersion: v1 kind: ConfigMap metadata: name: coredns-corefile namespace: dns-lab data: Corefile: | .:53 { errors log # gslb.lab.internal 만 hosts 파일로 응답(ttl 5s), 매칭 없으면 fallthrough → forward hosts /hosts/addn gslb.lab.internal { ttl 5 reload 2s fallthrough } forward . __CLUSTER_DNS__ cache 5 } --- apiVersion: apps/v1 kind: Deployment metadata: { name: lab-dns, namespace: dns-lab, labels: { app: lab-dns } } spec: replicas: 1 selector: { matchLabels: { app: lab-dns } } template: metadata: labels: { app: lab-dns } annotations: { sidecar.istio.io/inject: "false" } # 인프라 컴포넌트 — 메시 불필요 spec: containers: - name: coredns image: registry.k8s.io/coredns/coredns:v1.11.3 args: ["-conf", "/etc/coredns/Corefile"] ports: [{ containerPort: 53, protocol: UDP }, { containerPort: 53, protocol: TCP }] volumeMounts: - { name: corefile, mountPath: /etc/coredns } - { name: hosts, mountPath: /hosts } # writer — busybox: shared /hosts/addn 를 재작성해 GSLB flip을 수행하는 통로. - name: writer image: busybox:1.36 command: ["sh", "-c", "touch /hosts/addn; while true; do sleep 3600; done"] volumeMounts: [{ name: hosts, mountPath: /hosts }] volumes: - { name: corefile, configMap: { name: coredns-corefile } } - { name: hosts, emptyDir: {} } ``` **왜 이렇게 (비자명한 결정 3가지):** - `hosts` 플러그인에 `gslb.lab.internal` **만** 매칭하고 `fallthrough`로 나머지는 `forward . __CLUSTER_DNS__`. 안 그러면 client sidecar가 xDS 대상인 `istiod.istio-system.svc`도 lab-dns에 물어보게 되는데 lab-dns는 그 이름을 모른다 → forward가 없으면 sidecar가 컨트롤플레인을 못 찾아 죽는다(자기완결형 랩의 함정). - CoreDNS 공식 이미지는 **distroless라 shell이 없어** `kubectl exec`로 파일을 못 고친다 → 같은 pod에 busybox `writer`를 얹어 그 컨테이너로 `/hosts/addn`을 재작성한다. GSLB flip = 이 파일 한 줄을 바꾸는 것. - `reload 2s`가 파일 변경을 자동 재적재하므로, flip 후 별도 재시작 없이 **최대 2초 내** 새 응답이 나간다. `ttl 5`는 그 응답을 client 쪽 Envoy가 5초 넘게 캐시하지 않게 강제한다. ### 5.2 client — 관측 지점 ```yaml apiVersion: apps/v1 kind: Deployment metadata: { name: fortio, namespace: dns-lab, labels: { app: fortio } } spec: replicas: 1 selector: { matchLabels: { app: fortio } } template: metadata: { labels: { app: fortio } } spec: dnsPolicy: None dnsConfig: nameservers: ["__LABDNS_IP__"] # searches 도메인은 setup.sh 가 클러스터 coredns 에서 자동 발견해 치환(__CLUSTER_DOMAIN__). # 이 클러스터는 cluster.local 이 아니라 homelab.local — 하드코딩했다가 sidecar 가 안 떴다(§7 함정). searches: [dns-lab.svc.__CLUSTER_DOMAIN__, svc.__CLUSTER_DOMAIN__, __CLUSTER_DOMAIN__] options: [{ name: ndots, value: "5" }] # (pod metadata.annotations) Istio 기본 stats matcher 가 억제하는 cluster cx 카운터를 노출: # proxy.istio.io/config: | # proxyStatsMatcher: { inclusionRegexps: [".*gslb.*"] } containers: - { name: fortio, image: fortio/fortio:latest, args: ["server"], ports: [{ containerPort: 8080 }] } ``` **왜 이렇게**: `dnsPolicy: None` + `dnsConfig.nameservers`가 이 pod의 `/etc/resolv.conf`를 통째로 lab-dns로 덮는다. app 컨테이너와 istio-proxy는 **같은 resolv.conf를 공유**하므로, Envoy(c-ares)의 `gslb.lab.internal` 해석도 자동으로 lab-dns를 탄다 — 이게 "GSLB를 내가 통제한다"의 실제 배관이다. `searches`를 남겨 둔 이유는 §5.1과 같다(istiod 등 클러스터 이름도 계속 풀려야 하므로) — **단 그 도메인이 관건**이다. 이 클러스터의 cluster domain은 기본값 `cluster.local`이 아니라 **`homelab.local`**이라, 하드코딩하면 `istiod.istio-system.svc`가 어떤 search로도 실존 이름이 안 돼 NXDOMAIN → sidecar가 XDS에 못 붙어 Envoy가 안 뜬다(§7에서 상술). 그래서 setup 스크립트가 coredns configmap에서 도메인을 발견해 `__CLUSTER_DOMAIN__`을 치환한다. ### 5.3 ServiceEntry 변형 — 이 랩의 유일한 독립변수 ```yaml # [변형 A] resolution: DNS -> STRICT_DNS apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: { name: gslb-strict, namespace: dns-lab } spec: exportTo: ["."] # dns-lab 내부로만 가시화(gateway 전역 누수 차단) hosts: ["gslb.lab.internal"] location: MESH_EXTERNAL ports: - { number: 80, name: http, protocol: HTTP } - { number: 443, name: https, protocol: HTTP } # origination 후 내부는 HTTP로 다룸 resolution: DNS ``` ```yaml # [변형 B] resolution: DNS_ROUND_ROBIN -> LOGICAL_DNS (host 동일 -> 동시 적용 불가, 하나만 apply) apiVersion: networking.istio.io/v1beta1 kind: ServiceEntry metadata: { name: gslb-logical, namespace: dns-lab } spec: exportTo: ["."] hosts: ["gslb.lab.internal"] location: MESH_EXTERNAL ports: - { number: 80, name: http, protocol: HTTP } - { number: 443, name: https, protocol: HTTP } resolution: DNS_ROUND_ROBIN ``` **왜 포트 443을 `protocol: HTTP`로 선언하나**: TLS origination 뒤의 내부 트래픽은 평문 HTTP이므로, Envoy가 이 cluster를 HTTP conn pool(L7)로 관리하게 하려면 SE 포트도 HTTP로 선언해야 한다. TLS로 두면 passthrough(L4)로 다뤄져 이 랩의 핵심(§6에서 볼 drain 가시성)이 안 보인다. ### 5.4 VS(80→443) + DR(TLS origination) — 평문 진입을 TLS로 승격 ```yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: { name: gslb-80-to-443, namespace: dns-lab } spec: exportTo: ["."] hosts: ["gslb.lab.internal"] http: - match: [{ port: 80 }] route: [{ destination: { host: gslb.lab.internal, port: { number: 443 } } }] --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: { name: gslb-tls-origination, namespace: dns-lab } spec: exportTo: ["."] host: gslb.lab.internal trafficPolicy: portLevelSettings: - port: { number: 443 } tls: { mode: SIMPLE, sni: gslb.lab.internal, insecureSkipVerify: true } ``` **왜 이렇게**: client는 `http://gslb.lab.internal`(평문, 80)로만 부른다. VS가 80→443으로 넘기고, DR이 그 443 트래픽에 `SIMPLE` TLS를 얹어 sidecar가 실제로는 TLS로 나가게 만든다 — 이게 "client는 평문으로 호출하지만 실제론 TLS origination"의 배선이다. client가 처음부터 `https://`로 직접 불렀다면 SNI passthrough(L4)가 되어 §6의 L7 draining이 관측되지 않는다. `insecureSkipVerify: true`는 랩 단순화용 (백엔드가 self-signed 인증서만 있으면 됨) — prod는 `caCertificates`/`credentialName`으로 CA를 핀할 것. --- ## 6. 세 가지 재현 모드 | 모드 | variant | 흐름 | **실측 관측 (2026-07-01)** | |---|---|---|---| | **Mode 1** 세션 드롭 | strict / logical 각각 | keepalive 부하 중 GSLB flip(A→B) | **STRICT**: `upstream_cx_destroy` 0→**2**(`destroy_local`=Envoy 능동 drain), `membership_change`+1, endpoint A→B, who= flip **+5초**에 backend-b로 전환. **LOGICAL**: 같은 flip에 `cx_destroy` 델타 **0**, endpoint 논리 1개, who=backend-a **40초 내내 유지** | | **Mode 2** 유실+복구 | strict | A,B 둘 다 등록된 채 A만 kill(죽었지만 DNS엔 잔존) | 예상과 달리 **유실 0** — client가 backend-b로 워밍된 커넥션을 재사용해 죽은 A로 새 연결을 안 엶. **STRICT dead-IP 유실은 "새 연결이 죽은 endpoint를 뽑는 순간"에만** 발생(warm connection이 마스킹). §7 참조 | | **Mode 3** LOGICAL 트레이드오프 | logical | flip(A→B) 후 세션 유지, 그 뒤 A를 kill | flip엔 who=backend-a 유지(=Mode1 LOGICAL) → **A kill 순간 who=backend-b로 전환, `code!=200` 0건(무손실)**. 예측한 "재연결 blip"은 안 나옴 — 짧은 요청이라 죽은 커넥션이 요청 **사이**에 감지됨(`destroy_with_active_rq: 0`) | 관측 지표(모드 공통): ```bash # endpoint 집합 — STRICT는 flip 따라 바뀜, LOGICAL은 논리 1개(단 라벨은 새 IP로 갱신 = §7 stale-pin 함정) istioctl proxy-config endpoints deploy/fortio.dns-lab --cluster "outbound|443||gslb.lab.internal" # 커넥션 정리 카운터. 함정: Istio 기본 stats matcher 가 cluster 단위 upstream_cx_* 를 억제하므로 # client pod 에 annotation(proxyStatsMatcher.inclusionRegexps: [".*gslb.*"])이 있어야 이 값이 노출된다. kubectl -n dns-lab exec deploy/fortio -c istio-proxy -- pilot-agent request GET \ "stats?filter=gslb" | grep -E 'upstream_cx_destroy|membership_change' # 억제와 무관하게 항상 나오는 per-endpoint cx — 어느 backend에 실제 연결이 붙어있나: kubectl -n dns-lab exec deploy/fortio -c istio-proxy -- pilot-agent request GET clusters \ | grep 'outbound|443||gslb.lab.internal::' | grep -E 'cx_active|cx_total|rq_total' ``` > 이 세 모드를 명령 한 줄씩으로 묶은 하네스가 `scripts/dns-flip-test.sh `다 > (📎 첨부 참조). 부트스트랩은 `scripts/dns-lab-setup.sh`. --- ## 7. 함정 (메커니즘까지) - **cluster domain 하드코딩 = sidecar 안 뜸 (이번에 실제로 밟은 함정).** 이 클러스터의 cluster domain은 기본값 `cluster.local`이 아니라 **`homelab.local`**이다. client `dnsConfig.searches`에 `cluster.local`을 하드코딩했더니 `istiod.istio-system.svc`가 어떤 search 조합으로도 실존 이름이 안 되어 전부 NXDOMAIN → sidecar가 XDS/CA에 못 붙음 → Envoy 미기동 → pod rollout timeout(`istio-proxy` 15021 startup probe `connection refused`). 증상만 보면 sidecar 버그 같지만 실체는 DNS 도메인 불일치다. **수정**: `dns-lab-setup.sh`가 `kubectl -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' | grep -oE 'kubernetes[[:space:]]+[^ ]+'`로 도메인을 발견해 `__CLUSTER_DOMAIN__`을 치환. 사내 클러스터가 커스텀 도메인이면 per-pod DNS를 덮는 어떤 워크로드든 동일하게 물린다 — "cluster.local 하드코딩 금지, 도메인은 발견해서 주입"이 이식 규칙. - **LOGICAL_DNS의 관측 함정 — 덤프는 새 IP, 바이트는 옛 IP.** flip 후 `proxy-config endpoints`/clusters 덤프는 endpoint 라벨을 backend-b(새 resolved IP)로 바꿔 보여주지만, `cx_total`은 안 늘고 실제 응답 body는 여전히 backend-a다(논리 host 1개라 기존 커넥션을 그대로 재사용). **관측 도구만 믿으면 "트래픽이 새 IP로 옮겨갔다"고 오판**한다 — 진실은 응답 body(또는 패킷캡처)에만 있다. 실측에서 who=backend-a인데 endpoint 덤프는 backend-b였다. - **LOGICAL_DNS + multi-IP A record = CDS NACK.** LOGICAL_DNS cluster는 `lb_endpoint`가 1개여야 하는데, A record가 다중 IP가 되는 순간 이 SE를 받는 프록시가 CDS push를 통째로 NACK할 수 있다(같은 push의 listener까지 드롭). 그래서 **Mode 2(멀티 IP)는 STRICT 전용**, Mode 1/3의 flip은 항상 "단일 IP → 단일 IP"로만 한다. 실제로 겪은 사고: `serviceentry-example-logicaldns.yaml`에서 `example.com`이 멀티 IP로 바뀌며 이 SE를 받던 모든 egress gateway가 NACK — `exportTo: ["."]`로 가시성을 좁혀야 재발을 막는다. - **두 SE 변형은 host가 같아 동시 적용 불가.** `gslb-strict`/`gslb-logical` 둘 다 `hosts: [gslb.lab.internal]`이므로 하나를 적용하기 전 다른 하나를 반드시 `delete`한다(하네스가 자동 처리). - **client가 https로 직접 호출하면 이 랩 전체가 무의미해진다.** SNI passthrough(L4)가 되어 Envoy가 upstream을 HTTP conn pool로 안 보므로, host 제거가 "커넥션 정리"로 이어지긴 해도 L7 신호(GOAWAY 등)가 없어 관측이 흐려진다. 반드시 평문 진입 + TLS origination 경로를 지킨다(§5.4). - **keepalive 없는 요청 루프는 "끊길 세션" 자체가 없다.** 매 요청 새 커넥션이면 STRICT든 LOGICAL이든 다음 요청이 최신 DNS를 따라갈 뿐이라 차이가 안 보인다 — 반드시 `fortio -keepalive`처럼 커넥션을 오래 물고 있는 부하가 있어야 한다. --- ## 8. 라이브 실측 결과 (2026-07-01) `scripts/dns-lab-setup.sh` → `dns-flip-test.sh`로 3모드를 실제로 돌렸다. 먼저 SE.resolution이 Envoy cluster type으로 1:1 매핑됨을 config_dump로 확증했다: ```bash istioctl pc cluster deploy/fortio.dns-lab --fqdn gslb.lab.internal --port 443 -o json | grep '"type"' # resolution: DNS -> "type": "STRICT_DNS" # resolution: DNS_ROUND_ROBIN -> "type": "LOGICAL_DNS" ``` **핵심 대조 (Mode 1, flip A→B 전후를 load 유지 중에 스냅샷 → cx_destroy 델타 격리):** | 관측 지표 | STRICT_DNS | LOGICAL_DNS | |---|---|---| | endpoint(덤프) | backend-a → backend-b (교체) | backend-a → backend-b (라벨만) | | `membership_change` | **+1** | 0 | | `upstream_cx_destroy` | **+2** | **0** | | `upstream_cx_destroy_local` | +2 (Envoy 능동 drain) | 0 | | `upstream_cx_total` | +2 (신규 재연결) | 0 | | **who=(실제 응답 backend)** | flip +5초에 **backend-b** | 40초 내내 **backend-a** | | fortio downstream | Sockets 2, 100% 200 | Sockets 2, 100% 200 | 읽는 법: **downstream(client↔사이드카)은 두 경우 동일**하고, 차이는 전부 **upstream(사이드카↔backend)**에 있다. STRICT은 flip에 기존 연결을 `destroy_local`로 능동 drain하고 backend-b로 재연결(트래픽 이전), LOGICAL은 `cx_destroy` 델타 0으로 기존 연결을 보존(세션 유지, 옛 IP 고정). 두 경우 fortio는 `Sockets 2 / 100% 200`으로 동일했는데, 그게 downstream 지표라 아무 차이도 안 보였다는 것 자체가 "**정본 지표는 upstream `cx_destroy`**"임을 말한다. **Mode 3 (LOGICAL 트레이드오프):** flip엔 who=backend-a 유지 → backend-a **kill** 순간 who=backend-b로 전환, `code!=200` **0건**. 예측한 "재연결 blip"은 실측에서 안 나왔다 — 요청이 1ms로 짧아 죽은 커넥션이 요청 **사이**에 감지되고(`destroy_with_active_rq: 0`) Envoy가 이미 DNS가 가리키는 backend-b로 조용히 재연결. 따라서 **LOGICAL의 진짜 비용은 "에러"가 아니라** ① GSLB 의도 무시(먼 backend에 계속 핀) ② 장수명 스트림이면 pinned backend 사망 시 그 in-flight가 절단(`destroy_with_active_rq`가 오르는 경우). **Mode 2 (STRICT dead-IP):** 의도(죽은 IP로 유실)와 달리 **유실 0**이었다 — client가 backend-b로 워밍된 커넥션을 재사용해 죽은 A로 새 연결을 안 열었기 때문. STRICT의 dead-IP 유실은 **새 연결이 죽은 endpoint를 뽑는 순간에만** 발생하고 warm connection이 그걸 마스킹한다. 이 mode는 유실을 보려면 connection churn을 강제해야 해서 미완으로 남겼다(outlier detection 영역이라 별도 시나리오가 맞다). > 원시 로그·대조표 전문: repo `docs/test-reports/2026-07-01_dns-resolution.md`(종합) + `_193631/_193800/_195325/_195854_*`(모드별). > 회사망 다운로드: `/files/istio-egress/dns-resolution/`. **프로덕션 함의 한 줄**: STRICT=최신성(GSLB 추종) 우선·세션 희생, LOGICAL=세션 연속성 우선·최신성 희생. GSLB 뒤 장수명 연결(gRPC/DB/스트리밍)은 LOGICAL, GSLB 신호를 빨리 따라가야 하는 짧은 HTTP는 STRICT. --- ## What you might be missing - **이 랩은 sidecar-direct 경로다.** DNS 조회 주체가 client sidecar라서 자기완결형(공유 인프라 무변경)이 가능했다. 하지만 KakaoPay의 실제 egress 구성처럼 **egress gateway를 경유**하면 DNS 조회 주체가 gateway의 Envoy로 바뀐다([정본 §9](gw__report-2026-06-07_dns-resolution.html)) — 그 경로에서 재현하려면 gateway의 `dnsConfig`/resolv.conf를 건드려야 해서 blast radius가 커진다. 이 랩은 "메커니즘 증명"이지 "프로덕션 토폴로지 재현"은 아니다. - **LOGICAL_DNS의 NACK 제약은 버그가 아니라 신호다.** "논리적으로 하나의 endpoint"라고 선언한 이상, Envoy 입장에선 여러 IP 중 뭘 대표로 삼을지 정할 수 없어 거부하는 게 맞는 동작이다 — 이게 바로 "GSLB 뒤에서는 LB 권한을 DNS에 위임한다"는 §9(정본)의 트레이드오프가 코드로 강제되는 지점이다. 즉 Mode 2를 LOGICAL로 못 돌리는 제약 자체가 이론의 증거다. - **fortio의 keepalive 커넥션 1개(`-c1`)는 "죽을 때까지" 그 IP를 문다.** 이게 Mode 1에서 "세션 유지"의 시각적 증거이자 Mode 3(뒤늦은 재연결)의 씨앗이다 — 같은 메커니즘이 좋은 소식과 나쁜 소식을 동시에 만든다. - **STRICT의 "무손실"은 요청이 짧아서지 STRICT가 안전해서가 아니다.** 실측에서 STRICT flip이 100% 200이었던 건 요청이 1ms라 drain 시점에 in-flight가 없었기 때문(`destroy_with_active_rq: 0`). 프로덕션의 gRPC 스트림·대용량 업로드·장수명 DB 커넥션은 flip 순간 `destroy_with_active_rq`가 오르며 **그 요청이 잘린다** — "STRICT는 짧은 요청엔 안전, 긴 연결엔 위험"이 정확한 요약이다. 이걸 직접 보려면 장수명 부하로 재현해야 한다(다음 단계). --- ## 참조 **아카이브 내부** - [ServiceEntry resolution 정본](gw__report-2026-06-07_dns-resolution.html) — 이 랩이 재현하는 이론(STRICT_DNS/LOGICAL_DNS, dead IP, GSLB 권장) - [circuit-breaking 메커니즘 (outlier detection 정본)](gw__note-circuit-breaking-mechanisms.html) · [Cluster 해부](xds__src-cluster-anatomy.html) - [Egress 4-CRD 멘탈모델](gw__guide-egress-crd-mental-model.html) — 이 랩의 VS/DR TLS origination 패턴이 기대는 4-CRD 골격 - [클러스터 콜드부팅 복구](arch__runbook-cold-boot-recovery.html) — 이 랩을 설계하던 중 겪은 인프라 장애 **관련 IaC (실제 manifest·스크립트)** - 📎 [README (설계 내러티브 전문)](attachment/scenarios/50-dns-resolution/README.md) - 📎 [00-namespace](attachment/scenarios/50-dns-resolution/00-namespace.yaml) · 📎 [10-lab-dns](attachment/scenarios/50-dns-resolution/10-lab-dns.yaml) · 📎 [20-backends](attachment/scenarios/50-dns-resolution/20-backends.yaml) · 📎 [30-client](attachment/scenarios/50-dns-resolution/30-client.yaml) - 📎 [40-serviceentry-strict](attachment/scenarios/50-dns-resolution/40-serviceentry-strict.yaml) · 📎 [41-serviceentry-logical](attachment/scenarios/50-dns-resolution/41-serviceentry-logical.yaml) - 📎 [42-virtualservice-80to443](attachment/scenarios/50-dns-resolution/42-virtualservice-80to443.yaml) · 📎 [43-destinationrule-tls](attachment/scenarios/50-dns-resolution/43-destinationrule-tls.yaml) - 📎 [44-destinationrule-tls-outlier](attachment/scenarios/50-dns-resolution/44-destinationrule-tls-outlier.yaml) · 📎 [45-virtualservice-80to443-retry](attachment/scenarios/50-dns-resolution/45-virtualservice-80to443-retry.yaml) - 📎 [dns-lab-setup.sh](attachment/scripts/dns-lab-setup.sh) · 📎 [dns-flip-test.sh](attachment/scripts/dns-flip-test.sh)