데이터 플레인은 eventually consistent하게 동기화되고, proxy-status의 SYNCED/NOT SENT/STALE가 각 xDS 타입별 컨트롤 플레인 sync 상태를 드러낸다
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계층과 진단으로 위임한다.
⚠️ 버전 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을 실행하면 일어나는 일은 이렇다.
- K8s API server에 객체가 저장됨
- istiod의 watch가 변경을 감지하고, 영향받는 proxy 집합에 대해 새 Envoy 설정을 컴파일
- istiod가 각 proxy의 ADS gRPC stream으로 update를 push
- 각 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 계층), ADS가 이들을 단일 gRPC stream으로 묶더라도 각 타입은 개별적으로 versioned·ACK된다. Envoy는 "CDS v17을 ACK", "RDS v23을 ACK"처럼 타입마다 따로 응답한다. proxy-status는 그 per-type 합의 상태를 그대로 한 줄에 펼쳐 보여주는 overview일 뿐이다.
상태값 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인지가 의심할 리소스를 좁혀준다는 것이다. 각 컬럼이 답하는 질문이 다르기 때문이다.
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계층과 진단 §04). 그래서 여러 타입이 동시에 STALE이면 수렴 사슬의 위쪽(CDS/EDS)부터 풀리는지 봐야 한다 — 아래쪽(RDS/LDS)은 위쪽이 안정돼야 적용되므로.
SYNCED는 "Envoy가 ACK했다"는 전달(delivery) 보장이지, "그 설정이 옳다"는 정확성 보장이 아니다. 잘못된 의도(틀린 YAML)도 컴파일만 되면 SYNCED로 멀쩡히 전달된다. 의도 검증은 istioctl analyze의 몫이다(§04).
03. 예시 — 실제 출력을 읽고 STALE을 진단하기
계기판 한 번 보기
istioctl proxy-status
기대 출력 (Istio 1.30):
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)을 각각 확인하는 순서다.
# 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 <pod> -n <ns> | grep -A3 discoveryAddress
# → 어느 istiod에 붙는지 확인 (proxy-status 목록에서 빠졌으면 미연결)
원인별 처치 가이드:
| 관찰 | 해석 | 다음 조치 |
|---|---|---|
| 다수 proxy 동시 STALE | istiod push 정체/과부하 | istiod CPU/mem·replica·push debounce 확인 → 컨트롤 플레인 성능 요인 |
| 특정 1 proxy만 STALE | 그 proxy의 연결/agent 문제 | 해당 pod 재시작, agent log, 네트워크 정책(15012 차단 여부) 확인 |
| STALE + istiod log에 NACK | Envoy가 설정 거부 | 직전 apply한 리소스 rollback, analyze로 잘못된 의도 색출 |
| proxy 목록에서 누락 | istiod 미연결 | injection/sidecar 존재, agent crash, mTLS/방화벽 확인 |
① 범위(다수 vs 소수) → ② istiod log에 NACK 있나(설정 거부 vs 단순 지연) → ③ 해당 Envoy가 istiod에 붙어있나. 이 세 질문이면 "istiod 문제 / proxy 문제 / 내 설정 문제"로 갈린다.
proxy-status(전달 검증)와 analyze(의도 검증)는 짝이다. SYNCED인데 동작이 이상하면 의도가 틀린 것(→ analyze), STALE이면 전달이 막힌 것(→ 위 흐름). Envoy 내부 실제 적용값까지 봐야 하면 admin API/proxy-config로 내려간다 — 데이터 플레인 실측 진단은 Envoy Admin API 진단 참조.
# 의도(YAML)가 틀렸는지 — 전달과 무관하게 정적 검증
istioctl analyze -A
# 기대: "No validation issues found" / 경고 0
정리
머리에 남길 그림 하나: apply는 의도 등록일 뿐, 반영은 proxy별로 비동기 수렴한다. proxy-status의 각 셀은 "이 proxy가 이 xDS 타입을 ACK했는가"를 보여주는 per-type 계기판이고, 트러블슈팅은 그 계기판으로 "전달됐나?"부터 본다.
핵심 정리
모델 : 데이터 플레인은 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 jsondiff와 함께 쓰면 "YAML이 아니라 Envoy 설정으로" 사고하는 훈련이 된다. - eventual consistency는 정상 동작 중에도 잠깐의 불일치를 허용한다. apply 직후 수 초간 일부 proxy가 옛 route로 트래픽을 보내는 건 버그가 아니라 설계다(특히 rollout·endpoint 변동 시). 그래서 "방금 바꿨는데 안 됨"은 몇 초 기다린 뒤 다시 proxy-status를 봐야 진짜 STALE인지 일시적 window인지 구분된다.