--- type: note tags: [istio, xds, proxy-status, eventual-consistency, diagnosis] created: 2026-06-07 --- # 데이터 플레인은 eventually consistent하게 동기화되고, proxy-status의 SYNCED/NOT SENT/STALE가 각 xDS 타입별 컨트롤 플레인 sync 상태를 드러낸다 > [!abstract] 이 문서가 다루는 것 > Istio 데이터 플레인(Envoy proxy 무리)은 컨트롤 플레인(istiod)과 **강한 일관성이 아니라 최종 일관성(eventual consistency)** 으로 맞춰진다. 그래서 "설정을 apply했다"와 "그 설정이 모든 proxy에 반영됐다"는 별개의 사건이고, 둘 사이엔 항상 전파 window가 있다. `istioctl proxy-status`는 그 window를 가시화하는 계기판으로, 각 proxy가 **CDS/LDS/EDS/RDS(/ECDS) 타입별로** istiod의 마지막 push를 ACK했는지를 `SYNCED / NOT SENT / STALE / (누락)` 으로 보고한다. 이 멘탈모델을 잡으면 트러블슈팅 1단계가 "내 의도가 Envoy까지 전달됐는가"로 고정된다. 운영 진단 명령 전체 흐름은 [xDS 5계층과 진단](xds__src-xds-layers-and-diagnosis.html)으로 위임한다. > ⚠️ **버전 skew 주의**: 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다. `proxy-status`의 VERSION 컬럼이나 `proxy-config` 일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장. --- ## 01. 배경 — apply는 "의도 등록"이지 "반영 완료"가 아니다 먼저 풀어야 할 문제부터. Istio mesh는 **하나의 istiod가 수백~수천 개 Envoy proxy에 설정을 push하는 fan-out 분산 시스템**이다. 사용자가 `kubectl apply -f virtualservice.yaml`을 실행하면 일어나는 일은 이렇다. 1. K8s API server에 객체가 저장됨 2. istiod의 watch가 변경을 감지하고, 영향받는 proxy 집합에 대해 새 Envoy 설정을 **컴파일** 3. istiod가 각 proxy의 ADS gRPC stream으로 update를 **push** 4. 각 Envoy가 설정을 적용하고 **ACK**(거부 시 NACK)를 돌려줌 이 4단계는 proxy마다 **독립적·비동기적**으로 진행된다. proxy A는 이미 ACK했는데 B는 아직 push 대기 중일 수 있고, 네트워크가 느린 C는 더 늦다. 강한 일관성을 보장하려면 모든 proxy의 ACK를 기다린 뒤에야 apply를 "완료"로 인정해야 하는데, 그건 수천 proxy 규모에서 비현실적이고 **한 proxy의 장애가 mesh 전체 변경을 막는 단일 실패점**이 된다. 그래서 Istio는 *"각자 받는 대로 수렴하되, 언젠가는 모두 같은 상태에 도달한다"* 는 eventual consistency를 택한다 — 가용성과 확장성을 위해 순간적 불일치를 의도적으로 허용하는 거래다. 이 설계의 직접적 귀결이 **운영자가 알아야 할 진실 하나**다: apply가 성공해도 트래픽은 즉시 안 바뀐다. "VirtualService를 고쳤는데 트래픽이 안 바뀐다"는 증상은 두 갈래다 — (a) 내 의도(YAML)가 틀렸거나, (b) 의도는 맞는데 **아직/영영 Envoy까지 전달이 안 됐거나**. (b)를 먼저 배제하지 않으면 멀쩡한 YAML을 붙들고 헤맨다. 다음 절의 도구가 바로 이 (b)를 1초 만에 판별한다. --- ## 02. 핵심 모델 — proxy-status는 "타입별 수렴 계기판"이다 **한 문장 앵커: `proxy-status`의 각 셀은 "이 proxy가 이 xDS 타입에 대해 istiod의 마지막 push와 합의(ACK)됐는가"라는 단 하나의 질문에 답한다.** 이 그림만 머리에 박으면 모든 상태값·모든 컬럼이 여기서 파생된다. 왜 **하나의 SYNCED가 아니라 타입별 컬럼**으로 쪼개져 있을까. xDS 자체가 LDS/RDS/CDS/EDS/SDS의 독립 resource type으로 나뉘고(계층 개념은 [xDS API 계층](xds__note-xds-api-layers.html)), ADS가 이들을 단일 gRPC stream으로 묶더라도 **각 타입은 개별적으로 versioned·ACK된다**. Envoy는 "CDS v17을 ACK", "RDS v23을 ACK"처럼 타입마다 따로 응답한다. proxy-status는 그 per-type 합의 상태를 그대로 한 줄에 펼쳐 보여주는 overview일 뿐이다. ```mermaid flowchart LR APPLY["kubectl apply
(intent)"] --> APISERVER[K8s API server] APISERVER --> ISTIOD["istiod
watch + compile"] ISTIOD -->|push CDS/LDS/EDS/RDS| A["Envoy A"] ISTIOD -->|push| B["Envoy B"] ISTIOD -.->|not connected| C["Envoy C
(missing in list)"] A -->|ACK per type| ISTIOD B -->|no ACK = STALE| ISTIOD ``` ### 상태값 4종 — 앵커에서 그대로 따라 나온다 셀의 값은 "ACK됐는가"의 결과일 뿐이다. 그러니 4종은 ACK 여부 + push 여부의 조합으로 읽힌다. | 상태 | 의미 | 대표 원인 | |---|---|---| | `SYNCED` | Envoy가 istiod의 마지막 push를 **ACK**함 (정상, 수렴 완료) | — | | `NOT SENT` | istiod가 그 타입으로 **보낼 게 없었음** | gateway에 적용할 HTTP route가 없어 RDS가 NOT SENT, extension 없는 proxy의 ECDS NOT SENT — **대개 정상** | | `STALE` | istiod가 push했지만 Envoy ACK를 **못 받음** (수렴 미완·정체) | proxy↔istiod 네트워크 문제, istiod 과부하/push 지연, Envoy가 NACK 유발하는 잘못된 설정 | | (목록에서 proxy 자체 누락) | 그 proxy가 **현재 istiod에 연결 안 됨** | sidecar injection 누락, istio-agent crash, mTLS/네트워크 차단 — **거의 항상 문제** | 핵심 구분은 `NOT SENT` vs `STALE`이다. **`NOT SENT`은 "보낼 게 없음"(정상일 수 있음)**, **`STALE`은 "보냈는데 응답이 없음"(수렴이 막힘)** 이다. ingress gateway의 RDS가 `NOT SENT`인 건 흔히 그 gateway에 묶인 route가 아직 없다는 뜻이라 장애가 아니다. 반대로 어떤 타입이든 `STALE`은 그 타입 설정이 Envoy에 안 박혔다는 신호고, **proxy가 목록에서 통째로 빠진 것**은 그 proxy가 어떤 설정도 못 받는 중이라는 가장 강한 위험 신호다. ### 타입 분리가 곧 "고장 위치 지도" per-type 보고가 진단에 주는 가치는, **어떤 타입이 STALE인지가 의심할 리소스를 좁혀준다**는 것이다. 각 컬럼이 답하는 질문이 다르기 때문이다. ```text CDS STALE → cluster(upstream pool) 설정이 안 박힘 → DestinationRule/Service/ServiceEntry 쪽 의심 LDS STALE → listener(받는 지점) 설정이 안 박힘 → Gateway/Sidecar/PeerAuth 쪽 의심 EDS STALE → endpoint(실제 Pod IP) 설정이 안 박힘 → EndpointSlice/readiness/WorkloadEntry 쪽 의심 RDS STALE → route(어느 cluster로) 설정이 안 박힘 → VirtualService/HTTPRoute 쪽 의심 ECDS → WasmPlugin/EnvoyFilter의 extension config sync (안 쓰면 NOT SENT가 정상) ``` 타입 간엔 **적용 순서 의존성**도 있다. add 시 `CDS→EDS→RDS→LDS`(참조 대상부터, bottom-up), remove 시 그 역순(top-down)으로 적용되어, **route가 아직 없거나 이미 사라진 cluster를 가리키는 순간이 생기지 않도록** ADS가 순서를 보장한다(상세는 [xDS 5계층과 진단](xds__src-xds-layers-and-diagnosis.html) §04). 그래서 여러 타입이 동시에 STALE이면 **수렴 사슬의 위쪽(CDS/EDS)부터** 풀리는지 봐야 한다 — 아래쪽(RDS/LDS)은 위쪽이 안정돼야 적용되므로. ```mermaid flowchart TD subgraph perType["per-type version & ACK"] CDS["CDS vN"] EDS["EDS vN"] RDS["RDS vN"] LDS["LDS vN"] end ISTIOD["istiod push (ADS single stream)"] --> CDS ISTIOD --> EDS ISTIOD --> RDS ISTIOD --> LDS CDS -->|ACK| OK1["SYNCED"] RDS -->|no ACK| BAD["STALE → route 설정 정체"] ``` > [!warning] 함정 — SYNCED는 전달 보장이지 정확성 보장이 아니다 > `SYNCED`는 "Envoy가 ACK했다"는 **전달(delivery)** 보장이지, "그 설정이 옳다"는 **정확성** 보장이 아니다. 잘못된 의도(틀린 YAML)도 컴파일만 되면 SYNCED로 멀쩡히 전달된다. 의도 검증은 `istioctl analyze`의 몫이다(§04). --- ## 03. 예시 — 실제 출력을 읽고 STALE을 진단하기 ### 계기판 한 번 보기 ```bash istioctl proxy-status ``` 기대 출력 (Istio 1.30): ```text NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION details-v1-558b8b4b76-qzqsg.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1 istio-ingressgateway-66c994c45c-cmb7x.istio-system Kubernetes SYNCED SYNCED SYNCED NOT SENT NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1 reviews-v1-7f99cc4496-rtsqn.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-6cf8d4f9cb-wm7x6 1.30.1 ``` 앵커로 읽으면 한눈에 들어온다. 모든 app proxy가 CDS/LDS/EDS/RDS `SYNCED` → 수렴 완료. ingress gateway의 RDS가 `NOT SENT`인 건 그 gateway에 묶인 HTTP route가 아직 없다는 뜻이라 **정상**이고, 전 proxy의 ECDS `NOT SENT`는 WasmPlugin/EnvoyFilter extension을 안 쓴다는 뜻이라 역시 정상이다. 빨간불은 여기 없다. ### STALE이 떴을 때 — ACK 지연인가, 설정 거부인가 `STALE`은 "istiod가 보냈는데 ACK가 없다"까지만 말한다. 원인은 둘로 갈리고 처치가 다르다. - **ACK 지연/실패** — push는 갔는데 Envoy의 ACK가 안 돌아옴. proxy↔istiod 연결 품질, istiod 과부하, 혹은 Envoy가 그 설정을 **NACK**(거부)하는 경우. NACK이면 설정이 Envoy에 reject된 것이라 **Envoy는 blank-out 없이 직전 good config를 계속 서빙**한다(트래픽은 옛 설정대로 — 안전하지만 의도와 어긋남). - **push 자체가 정체** — istiod가 부하/내부 문제로 push 큐가 밀림. 이 경우 보통 **여러 proxy가 동시에 STALE**로 나타난다(개별 proxy 문제는 한두 개만). 진단은 **범위(scope)부터 좁히고 → 양쪽 끝(istiod / 해당 Envoy)을 각각 확인**하는 순서다. ```bash # 1. STALE이 한두 proxy인가, mesh 전반인가 (범위 판별) istioctl proxy-status # → 다수가 STALE이면 istiod(push) 의심, 소수면 그 proxy(연결/NACK) 의심 # 2. istiod가 push를 미루는지 / NACK이 쌓이는지 (control plane 쪽) kubectl -n istio-system logs deploy/istiod | grep -E "ADS|NACK|push" # 기대: "Pushing ..." 정상 흐름. "rejected"/"NACK" 다수면 설정 거부 # 3. 해당 Envoy가 istiod와 연결돼 있고 무엇을 ACK했는지 (data plane 쪽) istioctl proxy-config bootstrap -n | grep -A3 discoveryAddress # → 어느 istiod에 붙는지 확인 (proxy-status 목록에서 빠졌으면 미연결) ``` 원인별 처치 가이드: | 관찰 | 해석 | 다음 조치 | |---|---|---| | 다수 proxy 동시 STALE | istiod push 정체/과부하 | istiod CPU/mem·replica·push debounce 확인 → [컨트롤 플레인 성능 요인](arch__note-control-plane-performance-factors.html) | | 특정 1 proxy만 STALE | 그 proxy의 연결/agent 문제 | 해당 pod 재시작, agent log, 네트워크 정책(15012 차단 여부) 확인 | | STALE + istiod log에 NACK | Envoy가 설정 거부 | 직전 apply한 리소스 rollback, `analyze`로 잘못된 의도 색출 | | proxy 목록에서 누락 | istiod 미연결 | injection/sidecar 존재, agent crash, mTLS/방화벽 확인 | > [!tip] STALE을 봤을 때의 1·2·3 > ① 범위(다수 vs 소수) → ② istiod log에 NACK 있나(설정 거부 vs 단순 지연) → ③ 해당 Envoy가 istiod에 붙어있나. 이 세 질문이면 "istiod 문제 / proxy 문제 / 내 설정 문제"로 갈린다. `proxy-status`(전달 검증)와 `analyze`(의도 검증)는 **짝**이다. SYNCED인데 동작이 이상하면 의도가 틀린 것(→ `analyze`), STALE이면 전달이 막힌 것(→ 위 흐름). Envoy 내부 **실제 적용값**까지 봐야 하면 admin API/`proxy-config`로 내려간다 — 데이터 플레인 실측 진단은 [Envoy Admin API 진단](xds__note-envoy-admin-api-diagnosis.html) 참조. ```bash # 의도(YAML)가 틀렸는지 — 전달과 무관하게 정적 검증 istioctl analyze -A # 기대: "No validation issues found" / 경고 0 ``` --- ## 정리 머리에 남길 그림 하나: **apply는 의도 등록일 뿐, 반영은 proxy별로 비동기 수렴한다. proxy-status의 각 셀은 "이 proxy가 이 xDS 타입을 ACK했는가"를 보여주는 per-type 계기판이고, 트러블슈팅은 그 계기판으로 "전달됐나?"부터 본다.** ### 핵심 정리 ```text 모델 : 데이터 플레인은 eventually consistent — apply(의도 등록) ≠ 반영 완료. 둘 사이 전파 window 존재 1단계 : 트러블슈팅은 "의도가 Envoy까지 갔나?"부터 → istioctl proxy-status 상태값 : SYNCED = ACK함(전달 완료, 정확성 보장 아님) NOT SENT= 보낼 게 없음(흔히 정상; gateway RDS, extension 없는 ECDS) STALE = 보냈는데 ACK 없음(수렴 정체 — 진짜 위험 신호) 누락 = proxy가 istiod 미연결(거의 항상 문제) 타입별 : CDS/LDS/EDS/RDS가 따로 versioned·ACK → STALE 컬럼이 고장 위치를 좁힘 (CDS=cluster, LDS=listener, EDS=endpoint, RDS=route, ECDS=extension) STALE : 다수 동시 → istiod push 정체 / 소수 → 그 proxy 연결·NACK / log에 NACK → 설정 거부 짝 : proxy-status(전달 검증) ↔ analyze(의도 검증) ↔ proxy-config(Envoy 실측) ``` ## What you might be missing - **SYNCED는 "옳다"가 아니라 "도착했다"이다.** 틀린 VirtualService도 컴파일만 되면 SYNCED로 전달된다. SYNCED를 보고 "설정이 맞다"고 결론짓는 게 가장 흔한 오진 — 전달(proxy-status)과 정확성(analyze)을 분리해서 사고할 것. - **`NOT SENT`을 빨간불로 오해하지 말 것.** gateway의 RDS·extension 없는 proxy의 ECDS가 NOT SENT인 건 정상이다. 진짜 위험은 `STALE`(ACK 실패)과 **목록에서의 누락**(istiod 미연결)이다. 컬럼이 전부 SYNCED인데 한 proxy만 목록에 없다면, 그 proxy는 어떤 설정도 못 받는 중이라 가장 먼저 봐야 한다. - **STALE의 범위가 원인을 가른다.** 한두 proxy만 STALE이면 그 proxy의 연결/NACK 문제지만, **다수가 동시에 STALE**이면 istiod push 정체일 가능성이 크다 — 이때 개별 pod를 재시작해봐야 헛수고고, 컨트롤 플레인 성능 요인(istiod CPU/replica/debounce)을 봐야 한다. - **타입별 보고는 "어느 리소스를 의심할지"의 지름길이다.** RDS만 STALE이면 VirtualService/HTTPRoute, CDS만 STALE이면 DestinationRule/ServiceEntry — apply 직후 어느 컬럼이 흔들리는지를 보면 방금 바꾼 리소스가 박혔는지 즉시 안다. 적용 전후 `proxy-config ... -o json` diff와 함께 쓰면 "YAML이 아니라 Envoy 설정으로" 사고하는 훈련이 된다. - **eventual consistency는 정상 동작 중에도 잠깐의 불일치를 허용한다.** apply 직후 수 초간 일부 proxy가 옛 route로 트래픽을 보내는 건 버그가 아니라 설계다(특히 rollout·endpoint 변동 시). 그래서 "방금 바꿨는데 안 됨"은 몇 초 기다린 뒤 다시 proxy-status를 봐야 진짜 STALE인지 일시적 window인지 구분된다.