# STRICT dead-IP 유실 재현(churn 강제) → outlier+retry 수렴 — mode2 재설계 **Date:** 2026-07-02 · **cluster:** homelab · **ns:** dns-lab · **Istio:** 1.30.0 **시나리오:** `scenarios/50-dns-resolution/` · **신규/수정 manifest:** `44`(교정), `46`(신규), `47`(신규) **하네스:** `/tmp/.../scratchpad/dns-churn/run_churn_stages.sh` (단일 스크립트, 3-stage 순차 실행) **선행:** `2026-07-01_dns-resolution.md` §6(mode2 — warm connection이 유실을 마스킹), `2026-07-02_104250_*`/`_112113_*`(in-flight 절단 미재현) > 초안(construction 로그). "합격/불합격 표"가 아니라 무엇을 구성했고 그때 무엇이 보였는지, 그리고 > **예측과 다르게 나온 부분(꽤 많다)을 숨기지 않고** 그대로 남긴다. --- ## 0. 결론 먼저 (TL;DR) 이번 실험은 원래 가설("dead-IP 유실을 재현하고, outlier+retry로 0에 수렴시킨다")과 **다른, 그러나 더 중요한 결과**를 냈다. 세 가지를 먼저 요약한다. 1. **connection churn을 만드는 방법 자체가 함정이었다** — client 쪽 `-keepalive=false`/curl 반복 호출은 downstream 소켓만 churn시키고 upstream(Envoy→backend) 커넥션 풀은 그대로 재사용된다(실측: 80요청에 downstream 소켓 82개, upstream 신규 커넥션은 겨우 4개). **진짜 churn은 DestinationRule `connectionPool.http.maxRequestsPerConnection: 1`로 Envoy 자신이 요청마다 upstream 연결을 폐기·재생성하게 만들어야** 나온다(§2). 2. **그 필드를 어디 두느냐가 또 다른 함정이었다** — `trafficPolicy` top-level에 두면 **portLevelSettings가 존재하는 포트는 top-level을 상속하지 않는다**(config_dump로 확증). 기존 `44-destinationrule-tls-outlier.yaml`이 바로 이 패턴이었고, 그 결과 **이 파일은 처음부터 outlier detection이 443 트래픽에 적용된 적이 없었다** (`outlier_detection` 필드 자체가 클러스터에 없었음). 이번에 44를 교정했다(§2-1). 3. **churn을 진짜로 강제하고 나니, "유실"이 세 stage 모두에서 재현되지 않았다(0/149, 0/5221, 0/5219, 클라이언트 체감 100% 성공).** 원인은 outlier/retry가 아니라 **Envoy 라우터의 내장 동작**이다 — 커넥션이 아직 수립되지 않은 상태에서 connect가 실패하면(pending request), Envoy는 VS의 `retries` 정책 유무와 무관하게 **항상 자동으로 다른 healthy host를 다시 골라 재시도**한다(`upstream_rq_pending_failure_eject` → `upstream_rq_retry` 카운터가 이를 증명, VS에 retry 정책이 없는 stage1에서도 발생). 즉 **STRICT_DNS + L7 origination 조합에서 dead-IP의 실제 비용은 "요청 유실"이 아니라 "지연 세금"이다** — 죽은 IP를 뽑을 때마다 `connectTimeout`(1s)만큼 기다렸다가 조용히 다른 host로 넘어간다. outlier detection의 진짜 가치는 "유실을 막는 것"이 아니라 **"이 지연 세금을 없애는 것"**이었다(§4, §5). --- ## 1. 배경 — 왜 이 실험을 다시 했나 `2026-07-01_dns-resolution.md` §6(mode2)은 dead-IP 유실을 보이려다 실패했다: netshoot curl 루프가 이미 backend-b로 워밍된 **upstream** 연결을 재사용해, 죽은 backend-a로 새 연결을 열 기회가 아예 없었다. 그 리포트는 "connection churn을 강제해야 한다"는 과제를 남겼다(§9). 이번 세션의 목표는 그 churn을 실제로 만들고, outlier detection(DR44) + retry(VS45)를 단계 적용하며 유실이 줄어드는 과정을 실측하는 것이었다. --- ## 2. Churn을 실제로 만들기 — 두 번의 실측 함정 ### 시행 1: fortio `-keepalive=false` (실패) 배경 상태(SE=strict, DNS→backend-a 단일, 건강)에서 control test: ``` $ kubectl --context=homelab -n dns-lab exec deploy/fortio -c fortio -- \ fortio load -c 2 -qps 10 -t 8s -keepalive=false -quiet http://gslb.lab.internal/ Sockets used: 82 (for perfect keepalive, would be 2) Code 200 : 80 (100.0 %) ``` 같은 런의 netshoot/fortio 사이드카 cx 카운터 (BEFORE→AFTER): | counter | before | after | delta | |---|---|---|---| | upstream_rq_total | 945 | 1027 | **+82**(요청수와 일치) | | upstream_cx_total | 8 | 12 | **+4** | **client는 82개의 새 소켓을 열었는데(downstream churn 확인), upstream(Envoy→backend) 새 연결은 겨우 4개뿐.** 즉 client 쪽 keepalive 설정은 Envoy의 upstream 커넥션 풀 재사용을 전혀 건드리지 못한다 — 2026-07-01 mode2가 유실을 못 만든 이유(warm upstream 재사용)가 바로 이것이었고, client 쪽 설정 변경으로는 고쳐지지 않는다는 것을 이번에 직접 재확인했다. ### 시행 2: DestinationRule `maxRequestsPerConnection: 1` — 처음엔 이것도 실패 `46-destinationrule-tls-churn.yaml`에 `trafficPolicy.connectionPool.http.maxRequestsPerConnection: 1`을 **top-level**에 두고 재시도 → 여전히 cx_total 델타 4/82, 변화 없음. `config_dump`로 원인을 찾았다: ``` outbound|443||gslb.lab.internal (portLevelSettings[443]에 tls만 있음) → typed_extension_protocol_options 없음 (top-level의 maxRequestsPerConnection 미적용!) outbound|80||gslb.lab.internal (portLevelSettings 없음, top-level만 적용되는 포트) → typed_extension_protocol_options.common_http_protocol_options.max_requests_per_connection: 1 (적용됨!) ``` **portLevelSettings에 자기 포트 항목이 있으면 그 포트는 top-level의 다른 필드(connectionPool 등)를 전혀 상속하지 않는다.** 실제 트래픽이 흐르는 클러스터는 `outbound|443|...`인데, 443은 portLevelSettings가 있어서 top-level 설정이 무시된 것. **`connectionPool.http.maxRequestsPerConnection`을 portLevelSettings[443] 블록 "안"으로 옮기자** 비로소 반영됨을 config_dump로 확인: ```json "typed_extension_protocol_options": { "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { "common_http_protocol_options": { "max_requests_per_connection": 1 } } } ``` 수정 후 control test 재실행 (동일 8s/c=2/qps=10): | counter | before | after | delta | |---|---|---|---| | upstream_rq_total | 1109 | 1191 | **+82** | | upstream_cx_total | 16 | **98** | **+82** ← 요청수와 정확히 일치 | | upstream_cx_destroy_local | 4 | 86 | **+82** | **요청 82건에 신규 upstream 연결 82건 — 완전한 1:1 churn 확인.** 이 시점부터 진짜 실험이 가능해졌다. ### 2-1. 부수 발견 — 44(outlier)도 같은 함정에 빠져 있었다 기존 `44-destinationrule-tls-outlier.yaml`을 그대로 적용해 config_dump를 찍어보니: ``` outlier_detection present: False (443 클러스터에 아예 없음) ``` 44도 `outlierDetection`/`connectionPool.tcp`를 top-level에 두고 portLevelSettings[443]엔 tls만 있었다 — **mode2가 이 단계까지 도달하지 못해 지금까지 발견되지 않았을 뿐, 이 DR은 처음부터 443 트래픽에 outlier detection을 적용한 적이 없다.** 44를 portLevelSettings 안으로 옮겨 교정했다(파일에 함정을 주석으로 남김). 이번 실험은 **44(교정판)+churn을 합친 `47-destinationrule-tls-churn-outlier.yaml`**을 새로 만들어 stage2/3에 사용했다(44 자체는 "outlier만, churn 없음"인 채로 보존 — 다른 실험이 재사용할 수 있게). --- ## 3. 실험 설계 (최종) ``` +-------------------+ +--------------------------------------------+ | netshoot curl loop| --> | client sidecar (Envoy) | | (매 iter 새 프로세스,| | VS: 80->443, DR: TLS origination + | | -m 3, 요청 간 sleep| | | maxRequestsPerConnection:1 (churn) | | 없음 = 최대 처리량)| | [+outlier(44 교정판) stage2/3] | +-------------------+ +----------------+-----------------------------+ | STRICT_DNS: A,B 둘 다 등록 v +---------------+--------------+ | backend-a (scale 0 = dead-IP) | backend-b (healthy) +--------------------------------+ ``` - SE: `gslb-strict`(resolution:DNS) 고정. - DR/VS만 stage별 스왑: - **stage1**: DR=`46`(churn만) + VS=`42`(no-retry) - **stage2**: DR=`47`(churn+outlier: consecutive5xx/GatewayErrors=3, interval=5s, baseEjectionTime=30s, maxEjectionPercent=50, minHealthPercent=0, connectTimeout=1s) + VS=`42`(no-retry) - **stage3**: DR=`47`(동일) + VS=`45`(retry: attempts=3, perTryTimeout=2s, retryOn=connect-failure,refused-stream,gateway-error,503) - 부하: netshoot에서 `curl -s -m 3 -o /dev/null -w "%{http_code} %{time_total}"` 를 **sleep 없이** 40초간 반복 — 각 iteration이 별도 프로세스(client 쪽 churn)이며, DR의 `maxRequestsPerConnection:1`이 upstream 쪽 churn을 강제한다(양쪽 다 churn). 매 요청 결과를 `n=<순번> ` 로 타임스탬프와 함께 기록 → 실패 타임라인 재구성 가능. - dead-IP 조건: `flip_both`로 DNS에 A,B 둘 다 등록 → `kubectl scale deploy/backend-a --replicas=0` (DNS는 그대로 A를 가리키지만 실제 서비스가 없음 = STRICT_DNS host 목록에 죽은 IP가 남는 상태). - 각 stage 동일 타이밍(단일 bash 스크립트로 고정): apply → flip_both(+6s 안정화) → kill(+4s) → BEFORE 스냅샷 → 40s 부하 → AFTER 스냅샷 → backend-a 복구+READY 대기 → 단일 IP로 원복+안정화 → 다음 stage. - 스냅샷은 **netshoot 사이드카**(실제 트래픽 지점) — `pilot-agent request GET stats?filter=gslb` + `GET clusters`. --- ## 4. Stage별 결과 ### 4-1. 요약 테이블 | stage | 총요청 | 성공 | 실패(HTTP) | ≥0.9s 지연 요청 | 지연 구간(시작~끝, 40s 창 기준) | cx_connect_fail Δ | ejections_enforced_total Δ | rq_retry Δ | upstream_cx_total Δ | |---|---:|---:|---:|---:|---|---:|---:|---:|---:| | 1 baseline+churn (no outlier/no retry) | 149 | 149 | **0** | 37 (24.8%) | +2s(n=10) ~ +40s(n=149), **끝까지 지속** | +81 | N/A(미설정) | +81 | +230 | | 2 +outlier (no retry) | 5221 | 5221 | **0** | 1 (0.02%) | +33s(n=4274/5221) **1건뿐** | +10 | +2 | +10 | +5231 | | 3 +outlier+retry | 5219 | 5219 | **0** | 1 (0.02%) | +30s(n=3865/5219) **1건뿐** | +7 | +1(2→3, 기존 ejection 지속 중) | +7 | +5226 | (HTTP 실패는 세 stage 모두 0/0/0 — 아래 §4-2에서 이유를 설명한다. "실패"의 실체는 에러가 아니라 지연이었다.) ### 4-2. 왜 "실패"가 0인가 — Envoy의 내장 connect-failure 재시도 stage1(BEFORE→AFTER, netshoot 사이드카, VS42=재시도 정책 없음)의 원시 카운터: ``` BEFORE(12:12:04) AFTER(12:12:44) delta upstream_cx_total 57 287 +230 upstream_cx_connect_fail 32 113 +81 upstream_rq_pending_failure_eject 32 113 +81 upstream_rq_retry 32 113 +81 upstream_rq_retry_success 32 112 +80 upstream_rq_total 421 570 +149 <- 클라이언트 요청수와 정확히 일치 backend-a(10.250.147.30) cx_total 0 81 +81 <- 죽은 IP로의 시도 전부 backend-a rq_error 0 81 +81 backend-b(10.250.27.55) cx_total 0 149 +149 <- 클라이언트 149건 전부 여기로 "착지" backend-b rq_success 0 149 +149 ``` **VS42는 `retries` 필드가 아예 없다(재시도 정책 미설정).** 그런데도 `upstream_rq_retry` 카운터가 81 증가했고, 결과적으로 149건 전부가 backend-b로 성공했다. `upstream_cx_total`(+230) = 클라이언트 요청 149건(각 1개 신규 연결, `maxRequestsPerConnection:1` 덕에 전부 churn) **+** 죽은 backend-a로 시도했다 실패한 81건 = **230**, 정확히 일치한다. 읽는 법: LEAST_REQUEST LB가 요청마다 A/B 중 하나를 고른다(활성 요청 수가 둘 다 0이라 사실상 무작위에 가깝다). **A를 고르면** dead ClusterIP(엔드포인트 0개, kube-proxy가 매치 룰이 없어 SYN이 그냥 드롭됨)로 connect를 시도하다 `connectTimeout: 1s`에 걸려 실패한다 — 이 실패는 **connect 단계에서 일어났고 아직 어떤 바이트도 오가지 않았으므로**, Envoy 라우터는 이걸 "이미 시작된 요청의 실패"가 아니라 "아직 아무 데도 보내지지 않은 pending request"로 취급해 **VS의 retryPolicy와 무관하게** 자동으로 LB에 재요청 → 이번엔 B를 고르면(2개 host 중 나머지 하나) 성공한다. 이게 `upstream_rq_pending_failure_eject` (pending 상태에서 connection 실패로 ejected된 횟수)와 `upstream_rq_retry`가 **VS에 정책이 없는 stage1에서도** 정확히 81씩 증가한 이유다. **클라이언트가 지불한 비용은 에러가 아니라 그 실패한 connectTimeout 1초였다** — 37/149(24.8%)의 요청이 0.9~1.03초대 응답시간을 보였다(정상은 0.002~0.03초). ### 4-3. Stage2 — outlier가 "에러를 막는" 게 아니라 "지연 세금을 없앤다" stage2(+outlier, VS는 여전히 no-retry)에서 지연(≥0.9s) 요청은 **5221건 중 단 1건**(n=4274, 부하 시작 +33초 지점)뿐이었다. `outlier_detection.ejections_enforced_total`이 0→2로 늘었고, AFTER 스냅샷 시점에 `ejections_active: 1`, backend-a의 `health_flags: /failed_outlier_check`로 확인된다 — **backend-a가 초반 몇 번의 연속 connect 실패(consecutiveGatewayErrors:3) 직후 곧바로 격리(eject)되어, 이후 39초 가까이 LB 후보에서 아예 빠졌다.** `baseEjectionTime:30s`가 끝나는 시점(+30~33초) 근처에 잠깐 재편입되어 한 번 더 얻어걸린 것이 그 1건이고, 그 직후 다시 격리된 것으로 보인다(enforced_total이 2로, 즉 이 40초 안에 격리 이벤트가 두 번 있었음과 일치). **이 stage에서 요청 수 자체가 5221건으로 stage1(149건)의 약 35배로 폭증했다는 사실 자체가 강력한 방증이다.** 같은 40초 창에서 요청 처리량이 이렇게 차이 나는 이유는 단 하나 — stage1은 요청의 1/4이 매번 1초씩 날아갔고(그 시간 동안 다음 curl이 시작 못 함, 직렬 루프이므로), stage2는 그 1초 지연이 사실상 사라졌기 때문이다. **outlier detection의 진짜 효과는 "유실 방지"(원래 유실이 없었다)가 아니라 "죽은 endpoint를 반복해서 뽑는 데 드는 지연 비용의 제거"였다.** ### 4-4. Stage3 — retry는 이미 스스로 열려있던 문에 자물쇠 이름표를 붙인 것 stage3(+outlier+retry)은 stage2와 거의 동일한 프로파일(5219건, 지연 1건 @+30초)을 보였다. **VS45의 명시적 retry 정책(`retryOn: connect-failure,...`)이 추가한 값은 이 실험에서는 거의 드러나지 않았다** — §4-2에서 확인했듯 connect-failure에 대한 재시도는 VS 정책 없이도 Envoy가 이미 하고 있었기 때문이다. 다만 주의할 점: stage3 시작 시 DR47이 stage2와 **내용이 동일**해 Envoy가 클러스터를 재빌드하지 않았고, 그 결과 `ejections_active:1`/`health_flags:/failed_outlier_check` 상태가 stage2 종료 시점 그대로 stage3 BEFORE에 이어졌다(§6에 상세 기록) — 즉 stage3는 완전히 새로운 kill 사이클이 아니라 **이미 격리된 상태에서 이어달리기**했을 가능성이 있다. VS45의 순수 효과를 보려면 outlier 없이(DR=46+VS45 조합) 별도 스테이지가 필요했는데, 이번 3-stage 설계엔 없었다 — 다음 실험 후보로 남긴다(§7). --- ## 5. 종합 판정 - **Stage1 유실 재현 여부**: **HTTP 레벨 유실은 재현되지 않았다(0/149)**. 대신 **connect churn이 실제로 강제되었음을 카운터로 확증**했고(§2, cx_total 델타=요청수), dead-IP를 뽑을 때마다 Envoy가 VS 정책과 무관하게 자동 재시도하는 내장 동작 때문에 클라이언트는 지연(37/149, 24.8%가 ~1s)만 체감했다. 이건 "실패"가 아니라 **애초 가설(HTTP 레벨 유실이 관측될 것)이 이 구성(L7 origination, HTTP/1.1, 2-member cluster)에서는 성립하지 않는다는, 이번 실험으로 새로 얻은 결론**이다. - **Stage2 단축 여부**: **명확히 단축되었다** — 지연 발생 빈도가 37/149(24.8%)에서 1/5221(0.02%)로 ~1200배 감소, 그리고 그 감소 자체가 처리량을 35배 늘렸다(149→5221건/40s). outlier detection이 실제로 격리를 수행했음을 `ejections_enforced_total`(+2)과 `health_flags:/failed_outlier_check`로 직접 확인했다. - **Stage3 수렴 0**: 클라이언트 관점 성공률은 stage1부터 이미 100%였으므로 "0으로 수렴"이라는 표현은 **이 구성에서는 성립하지 않는다** — 애초에 0이 아니었던 적이 없다. `upstream_rq_retry`(+7)는 여전히 connect-failure에 대한 재시도가 일어났음을 보여주지만, 이게 VS45의 정책 때문인지 Envoy 내장 동작 때문인지는 이번 stage3 설계로는 분리되지 않는다(§4-4, §7). --- ## 6. 이상 징후 · 미해결 의문 - **stage2→stage3 전환 시 outlier 상태 이월**: DR47이 stage2/3에서 동일한 내용이라 Envoy가 클러스터를 재빌드하지 않았고, `ejections_active`/`health_flags`가 리셋 없이 이어졌다(stage3 BEFORE의 `upstream_cx_connect_fail:123`이 stage2 AFTER와 정확히 같은 값). 반면 **per-endpoint 카운터**(예: backend-b의 cx_total)는 stage 전환 중 DNS를 단일→A,B로 다시 바꾸는 과정에서 0으로 리셋됐다(호스트 목록이 갱신되며 새로 추가된 endpoint의 게이지가 초기화된 것으로 추정) — **클러스터 레벨 누적치와 엔드포인트 레벨 누적치가 같은 재구성 이벤트에 서로 다르게 반응**한다는 걸 이번에 처음 명시적으로 확인했다. 이 때문에 stage3의 "자체" 델타는 per-endpoint 값(backend-a cx_total 10→17 = +7)과 cluster 값(123→130 = +7)이 우연히 일치해서 검증 가능했지만, 항상 이렇게 깔끔하게 분리되리라는 보장은 없다. - **VS45(retry)만의 순효과 미분리**: stage3가 outlier와 함께 적용되어, "retry 정책 자체가 없어도 이미 Envoy가 connect-failure를 재시도한다"는 §4-2의 발견이 **retry 정책이 실제로 추가 가치를 내는 지점** (예: outlier 없이 retry만 있는 경우, 또는 5xx 애플리케이션 레벨 에러처럼 connect 단계를 넘어선 실패)을 가리지 못했다. DR46(churn만)+VS45(retry) 조합의 별도 stage가 필요하다. - **connectTimeout:1s가 "실패 비용"을 결정한다**: 만약 46/47에 connectTimeout을 명시하지 않았다면(기본 10s), 지연 세금이 1s가 아니라 최대 10s까지 커졌을 것이다 — 이 값 자체가 실험 결과(지연폭)를 상당 부분 결정했다는 점을 감안할 필요가 있다. 프로덕션에서 이 값을 얼마로 잡을지가 "dead-IP 지연 세금"의 크기를 직접 좌우한다. - **44의 버그가 이전 mode2 리포트에 영향을 줬는가**: 2026-07-01 mode2는 이 버그(44의 outlierDetection이 443에 적용 안 됨) 상태로 실행됐다 — 그런데 그 리포트는 애초에 churn 부재로 실패했으므로, 이 버그가 그 결과에 실질적 영향을 주지는 않았을 것으로 보인다(outlier 여부와 무관하게 유실 자체가 안 만들어졌으므로). 다만 **"44 적용 = outlier 켜짐"이라고 믿고 진행했을 모든 과거 가정은 재검토가 필요**하다. --- ## 7. 다음 작업 후보 - DR46(churn만)+VS45(retry, outlier 없음) 조합으로 별도 stage를 돌려, "outlier 없이 retry 정책만으로 connect-failure를 커버할 수 있는가"(이미 Envoy가 하고 있다는 §4-2와 어떻게 다른지)를 분리 검증. (가설: 차이가 거의 없을 것 — connect-failure 재시도는 애초에 VS 정책과 무관해 보이므로. 만약 정말 차이가 없다면 "VS retries의 connect-failure 항목은 이 경로에서 사실상 무의미"라는 강한 결론이 가능하다.) ⚠ 안전 대책 필요: `retryOn`에서 `connect-failure`를 **빼고** `5xx`/`refused-stream`만 남긴 변형으로 Envoy 내장 재시도와 VS 재시도를 실제로 분리해볼 것. - outlier 없이 connectTimeout을 기본값(10s)으로 되돌려, "지연 세금"이 실제로 최대 10초까지 벌어지는지 정량화(§6 셋째 항목). - HTTP/2(gRPC 등)에서 connect-failure에 대한 이 자동 재시도 동작이 동일한지 확인 — 이번 실험은 HTTP/1.1 로만 검증했다. - backend-a가 outlier에 의해 격리된 후 baseEjectionTime(30s) 만료 시점의 "재편입 → 재실패 → 재격리" 사이클을 더 세밀한(초 단위 촘촘한) 스냅샷으로 재현해, ejection의 실제 지속시간과 재검증 로직을 정밀 확인. --- ## 8. 종료 상태 확인 ``` $ kubectl --context=homelab -n dns-lab get virtualservice,destinationrule virtualservice.../gslb-80-to-443 ["gslb.lab.internal"] <- 42(no-retry) 재적용됨 destinationrule.../gslb-tls-origination gslb.lab.internal <- 43(no-outlier,no-churn) 재적용됨 $ kubectl --context=homelab -n dns-lab get serviceentry gslb-strict ["gslb.lab.internal"] MESH_EXTERNAL 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 exec deploy/netshoot -- curl -s -m 5 -w ' [http_code=%{http_code}]\n' http://gslb.lab.internal/ backend-a [http_code=200] <- mesh 경유 sanity 정상 $ kubectl --context=homelab -n dns-lab get pods backend-a 1/1 Running (재시작됨, scale 0→1 3회 순환 후) backend-b 1/1 Running (전 실험 동안 미변경) fortio 2/2 Running lab-dns 2/2 Running netshoot 2/2 Running $ istioctl --context=homelab proxy-config endpoints deploy/netshoot.dns-lab --cluster "outbound|443||gslb.lab.internal" 10.250.147.30:443 HEALTHY OK <- outlier 격리 상태 해제 확인(DR43 재적용으로 클러스터 재빌드) $ istioctl --context=homelab analyze -n dns-lab Warning [IST0129] ... TLS mode SIMPLE, no caCertificates ... <- 기존부터 있던 경고, 신규 아님 Info [IST0135] ... sidecar.istio.io/inject deprecated ... <- 기존부터 있던 정보성 메시지 (에러 0) ``` 한 가지 주의: stage3 종료 직후 첫 sanity curl에서 **`no healthy upstream`(503)**이 한 번 나왔다 — DNS를 단일 backend-a로 되돌린 시점에 backend-a가 여전히 outlier에 의해 격리(`/failed_outlier_check`) 중이었고, 그 상태에서 유일한 host가 격리돼 있으니 Envoy가 정상적으로 요청을 거부한 것(§4-4의 이월 현상과 같은 원인). DR을 43(no-outlier)으로 재적용해 클러스터를 재빌드하자 즉시 해소됐다 — 최종 sanity는 200/backend-a로 확인. --- ## 9. 원시 로그 경로 ``` /tmp/claude-1000/-mnt-homelab-kakaopay-istio/9efbaa2c-8577-4534-8d98-d97a7cc0747e/scratchpad/dns-churn/ ├── run_churn_stages.sh (3-stage 하네스 스크립트, §3 그대로) ├── full_run.txt (전체 stdout, 3-stage 실행 원본) ├── stage{1,2,3}_before.txt (stage별 BEFORE 스냅샷) ├── stage{1,2,3}_after.txt (stage별 AFTER 스냅샷) ├── stage{1,2,3}_loop.txt (요청별 n/code/time_total 원시 로그 — 149/5221/5219 라인) ├── fortio_config_dump.json (§2 churn 실측 과정의 config_dump — DR46 top-level 버전, 실패 증거) └── fortio_config_dump_dr44.json (§2-1 44 버그 확증용 config_dump) ``` 수정/신규 manifest (repo): ``` scenarios/50-dns-resolution/44-destinationrule-tls-outlier.yaml (교정: outlierDetection을 portLevelSettings 안으로) scenarios/50-dns-resolution/46-destinationrule-tls-churn.yaml (신규: churn 전용, connectTimeout:1s + maxRequestsPerConnection:1) scenarios/50-dns-resolution/47-destinationrule-tls-churn-outlier.yaml (신규: 44 교정판 + 46 churn 합본, stage2/3용) ```