GSLB 뒤 DNS resolution 재현 랩 — "세션이 끊길 수도 있다"를 눈으로 보기
ServiceEntry resolution 정본은 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 정본(STRICT_DNS/LOGICAL_DNS, DNS refresh ≠ health check), TLS origination vs passthrough, outlier detection 다루는 것: ① 왜 자체 GSLB 시뮬레이터인가 ② 멘탈모델·부품표 ③ 실제 구성(전체 YAML + 이유) ④ 3가지 재현 모드 ⑤ 함정
1. 배경 — 이론은 있는데 왜 "재현"이 따로 필요한가
기존 runbook은 resolution 필드가 Envoy cluster type을 가른다는 것, DNS refresh가 liveness가 아니라는 것,
GSLB 뒤엔 LOGICAL_DNS가 낫다는 것까지 전부 정리했다. 그런데 이 문서들은 전부 정적 조사다 — "endpoint가
몇 개 잡히는가"는 dig+proxy-config로 스냅샷을 찍으면 끝나지만, "GSLB가 IP를 바꾸는 그 순간 이미 맺혀 있던
연결이 끊기는가/유지되는가"는 시간축이 있는 이벤트라 스냅샷으로는 못 본다. 이걸 보려면:
- GSLB를 내가 통제해야 한다 — 실제 GSLB는 언제 IP를 바꿀지 내가 못 정한다.
- 롱커넥션이 있어야 한다 — 매 요청 새 커넥션이면 애초에 "끊길 기존 세션"이 없어 STRICT든 LOGICAL이든 차이가 안 보인다.
- 공유 인프라를 안 건드려야 한다 — 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 시뮬레이터 (여기가 이 랩의 심장)
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 — 관측 지점
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 변형 — 이 랩의 유일한 독립변수
# [변형 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
# [변형 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로 승격
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) |
관측 지표(모드 공통):
# 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 <strict|logical> <mode1|mode2|mode3>다 (📎 첨부 참조). 부트스트랩은scripts/dns-lab-setup.sh.
7. 함정 (메커니즘까지)
- cluster domain 하드코딩 = sidecar 안 뜸 (이번에 실제로 밟은 함정). 이 클러스터의 cluster domain은
기본값
cluster.local이 아니라homelab.local이다. clientdnsConfig.searches에cluster.local을 하드코딩했더니istiod.istio-system.svc가 어떤 search 조합으로도 실존 이름이 안 되어 전부 NXDOMAIN → sidecar가 XDS/CA에 못 붙음 → Envoy 미기동 → pod rollout timeout(istio-proxy15021 startup probeconnection refused). 증상만 보면 sidecar 버그 같지만 실체는 DNS 도메인 불일치다. 수정:dns-lab-setup.sh가kubectl -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' | grep -oE 'kubernetes<span class="wikilink" title=":space:">🔗 :space:</span>+[^ ]+'로 도메인을 발견해__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로 확증했다:
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) — 그 경로에서 재현하려면 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 정본 — 이 랩이 재현하는 이론(STRICT_DNS/LOGICAL_DNS, dead IP, GSLB 권장) - circuit-breaking 메커니즘 (outlier detection 정본) · Cluster 해부 - Egress 4-CRD 멘탈모델 — 이 랩의 VS/DR TLS origination 패턴이 기대는 4-CRD 골격 - 클러스터 콜드부팅 복구 — 이 랩을 설계하던 중 겪은 인프라 장애
관련 IaC (실제 manifest·스크립트) - 📎 README (설계 내러티브 전문) - 📎 00-namespace · 📎 10-lab-dns · 📎 20-backends · 📎 30-client - 📎 40-serviceentry-strict · 📎 41-serviceentry-logical - 📎 42-virtualservice-80to443 · 📎 43-destinationrule-tls - 📎 44-destinationrule-tls-outlier · 📎 45-virtualservice-80to443-retry - 📎 dns-lab-setup.sh · 📎 dns-flip-test.sh