istiod 성능은 변경률·할당 리소스·워크로드 수·설정 크기 네 요인의 곱으로 결정되고, Sidecar 리소스로 설정 범위를 좁히는 것이 가장 효과적인 단일 레버다
istiod를 "control plane"이라는 추상명사가 아니라 설정 컴파일러 + 분배기로 보면, 부하의 출처와 튜닝 레버가 한눈에 선다. 이 문서는 (1) istiod 부하를 결정하는 네 요인이 곱셈으로 결합한다는 멘탈모델, (2) event→debounce→snapshot→push queue→xDS로 이어지는 동기화 파이프라인, (3) 메트릭으로 병목을 incoming(compute) vs outgoing(push)으로 갈라 scale up이냐 scale out이냐를 결정하는 진단법, (4) 왜 Sidecar 리소스가 네 요인을 동시에 미는 단일 최대 레버인지를 다룬다.
결론: istiod 성능 튜닝은 "CPU를 더 주자"가 아니라 "각 proxy가 받아야 하는 설정량을 줄이자"에서 출발한다.
대상환경: Istio 1.30 / Envoy · 대상독자: mesh 규모가 커지며 istiod CPU·push latency가 의심되기 시작한 SRE · 범위: 부하 모델·진단·근본 레버(운영 detail은 위임) · 선행개념: xDS(LDS/RDS/CDS/EDS/SDS), sidecar proxy. 운영 detail은 → 운영 플레이북, → Sidecar scope에 위임하고, 여기서는 성능의 멘탈모델에 집중한다.
01. 배경 — 왜 control plane이 병목이 되는가
작은 mesh에서는 istiod 성능을 신경 쓸 일이 없다. 문제는 mesh가 커질 때 비용이 선형이 아니라 곱셈으로 자란다는 데서 시작된다. 왜 그런지 보려면 istiod가 실제로 무슨 일을 하는지를 추상명사에서 끌어내려야 한다.
istiod는 무한 루프를 돈다.
- K8s API와 Istio CRD를 watch 한다(Service/EndpointSlice/Pod/Secret + Gateway/VS/DR/SE/Sidecar/PeerAuth/AuthZ),
- 이 입력을 in-memory model로 reconcile 한 뒤,
- 각 proxy마다 그 proxy가 봐야 할 범위로 Envoy 설정(LDS/RDS/CDS/EDS/SDS)을 계산하고,
- xDS로 모든 연결된 proxy에 push 한다.
즉 istiod는 "선언적 의도(CRD) → Envoy가 실행하는 구체 설정"을 끊임없이 컴파일하고, 그 결과물을 수천 개 proxy에 분배하는 기계다. 여기서 비용이 두 방향으로 갈린다 — 이 구분이 문서 전체를 관통하는 첫 번째 축이다.
incoming 부하 (watch + compute)
= K8s/CRD 이벤트를 받아 in-memory model을 갱신하고
각 proxy용 Envoy 설정 snapshot을 계산하는 비용
→ CPU·메모리 바운드, 단일 istiod 인스턴스에 집중
outgoing 부하 (push + transport)
= 계산된 설정을 모든 연결된 proxy에 xDS로 밀어내는 비용
→ 연결 수(proxy count) × 설정 크기에 비례, 인스턴스 수로 분산 가능
곱셈이 어디서 나오는가? mesh가 N개 service, M개 proxy로 자라는데 scope를 안 좁히면 각 proxy가 mesh 전체(N개 service)의 설정을 받는다. 그러면 service 하나가 바뀔 때 M개 proxy 전부에 N 크기의 설정을 다시 밀어야 한다 — 변경 1건의 비용이 M×N으로 부푼다. 이 곱셈 구조가 control plane을 병목으로 만드는 근본 원인이고, 뒤의 모든 레버는 이 곱의 항(項)을 하나씩 깎는 일이다.
02. 핵심 멘탈모델 — 네 요인의 곱
이 문서에서 딱 하나만 머리에 남긴다면 이 식이다. istiod 부하는 네 요인의 곱이고, 곱이기 때문에 가장 큰 항을 깎는 것이 압도적으로 효과적이다.
istiod 부하 ≈ (변경률) × (영향받는 proxy 수) × (proxy당 설정 크기) / (할당 자원)
rate of change number of workloads configuration size allocated resources
Istio 공식 performance 모델의 네 요인을, 각자가 incoming/outgoing 중 어디를 미는지와 함께 본다.
| 요인 | 무엇인가 | 미는 곳 | 대표 트리거 |
|---|---|---|---|
| Rate of change | 단위 시간당 config/endpoint 변경 횟수 | incoming(compute) + outgoing(push) | 잦은 deploy, HPA scale, Pod churn, endpoint flapping |
| Allocated resources | istiod에 준 CPU/메모리 | incoming 처리량 상한 | 요청·limit 부족 → push 지연 |
| Number of workloads | 연결된 proxy(sidecar+gateway) 수 | outgoing(push fan-out) | mesh 규모, namespace 수 |
| Configuration size | 각 proxy가 받는 Envoy 설정의 크기 | outgoing(push 바이트) + Envoy 메모리 | scope 미설정 → 모든 서비스가 모든 proxy에 |
곱셈이라는 사실에서 모든 결론이 따라 나온다. 설정 크기를 절반으로 줄이면 모든 push의 바이트와 Envoy 메모리가 함께 줄 뿐 아니라, "해당 proxy scope에 무관한 변경"은 아예 push 대상에서 빠지므로 변경 빈도의 체감값까지 줄어든다 — 한 항을 깎았더니 두 항이 줄어든다. 그래서 §06의 Sidecar 리소스(설정 크기·범위 축소)가 단일 최대 레버가 된다. 반대로 할당 자원만 키우는 것은 분모만 키워 "같은 곱을 더 빨리 처리"할 뿐, 곱 자체는 그대로다.
둘은 다른 축이다. workload 수는 "몇 개의 proxy에 보내는가"(fan-out)이고, config size는 "각 proxy에 얼마를 보내는가"(payload)다. 1만 개 proxy라도 각자가 받는 설정이 작으면 견딜 만하고, 100개 proxy라도 각자가 mesh 전체 설정을 받으면 istiod가 휘청한다. 이 구분이 §05의 scale out vs scale up 판단으로 그대로 이어진다.
03. 동기화 파이프라인 — debounce부터 ACK까지
부하의 총량을 봤으니, 이제 그 총량이 어느 단계를 통과하는지를 본다. 변경 하나가 Envoy에 반영되기까지의 파이프라인을 알아야 "어느 단계가 막혔는가"를 진단할 수 있다.
각 단계의 메커니즘과 그것이 어느 요인에 민감한지:
- debounce — istiod는 변경 이벤트를 즉시 push하지 않고 짧은 시간 창(
PILOT_DEBOUNCE_AFTER, 기본 100ms 부근;PILOT_DEBOUNCE_MAX로 상한) 동안 모아서 한 번에 처리한다. 변경 폭주(deploy storm, endpoint flapping) 시 push 횟수를 줄여 istiod를 보호하는 1차 방어선이다. 즉 rate of change를 흡수하는 댐이다. 단, 댐을 높이면(창을 늘리면) 그만큼 설정 반영 latency가 늘어난다(빈도 안정성 ↔ 반영 신속성 trade-off). - snapshot 계산 — debounce가 끝나면 영향받는 각 proxy에 대해 그 proxy의 scope에 맞는 Envoy 설정을 계산한다. 이 단계가 configuration size·workload 수에 가장 민감하다(incoming CPU 부하의 본체). §01의 M×N 곱이 실제 CPU로 환산되는 지점이 바로 여기다.
- push queue + throttle — 계산된 설정은 큐에 들어가고, istiod는 동시 push 수를
PILOT_PUSH_THROTTLE(기본 100 부근)로 제한해 한꺼번에 모든 proxy를 밀어내 자신과 네트워크가 죽는 것을 막는다. 큐가 길어지면 일부 proxy의 반영이 늦어진다(STALE의 한 원인). 이게 outgoing 병목의 관측 지점이다. - xDS push → ACK/NACK — Envoy는 받은 설정을 적용하고 ACK(정상) 또는 NACK(거부)한다. ACK 지연/누락은 proxy-status에
STALE/NOT SENT로 드러난다 → 상태 의미는 데이터 플레인 sync 상태에 위임.
데이터 플레인은 이 파이프라인을 통해 eventually consistent하게 동기화된다. 그래서 모든 트러블슈팅의 1단계는 "Envoy가 최신 설정을 받았는가"(istioctl proxy-status)이고, 그 다음이 "그 설정 내용이 맞는가"(proxy-config)다.
04. 진단 메트릭 — 어느 단계가 느린가
파이프라인의 각 단계는 Prometheus 메트릭으로 노출된다. 핵심은 단일 숫자가 아니라 메트릭의 조합으로 병목을 incoming(compute)과 outgoing(push)으로 가르는 것이다.
| 메트릭 | 보는 것 | 높을 때의 의미 |
|---|---|---|
pilot_proxy_convergence_time |
변경 발생 → 모든 proxy 반영까지 end-to-end 시간 | 전체 파이프라인이 느림(가장 먼저 보는 신호) |
pilot_proxy_queue_time |
push queue에서 대기한 시간 | throttle/outgoing 병목(push가 밀림) |
pilot_xds_push_time |
xDS push 1건의 처리 시간 | 설정 크기 큼 또는 transport 부하 |
pilot_xds_pushes |
push 횟수(타입별) | rate of change 높음 → debounce가 못 잡고 있음 |
pilot_xds (connected) |
연결된 proxy 수 | outgoing fan-out 규모 |
pilot_push_errors / NACK |
push 실패·거부 | 설정 오류 또는 Envoy 거부 |
| istiod container CPU/mem | 자원 압박 | incoming(compute) 병목 후보 |
읽는 법은 §02의 두 축으로 환원된다: convergence_time은 "느리다"는 신호일 뿐, 어디가 느린지는 queue_time(밀려서 못 보냄=outgoing)과 CPU 포화(계산이 느림=incoming)의 둘 중 어느 것이 동반되는지로 갈린다.
# istiod 메트릭 빠른 확인 (15014 = istiod monitoring port)
kubectl -n istio-system port-forward deploy/istiod 15014:15014 &
curl -s localhost:15014/metrics | grep -E \
'pilot_proxy_convergence_time|pilot_proxy_queue_time|pilot_xds_push_time'
# 기대 출력: 각 메트릭의 _bucket/_sum/_count histogram 라인.
# convergence p99가 수 초 이상으로 꾸준히 크면 파이프라인 어딘가가 병목.
# 동기화 상태 한눈에
istioctl proxy-status
# 기대 출력: NAME / CLUSTER / CDS·LDS·EDS·RDS 열이 모두 SYNCED.
# 다수가 STALE → push 적체(outgoing) 의심, NOT SENT 다수 → scope/debounce 확인.
05. 병목 진단 → scale out vs scale up
이제 §02(부하 = 두 방향)와 §04(메트릭)를 합쳐 결정을 내린다. 목표는 "더 큰 istiod 하나(scale up)"와 "istiod replica 증설(scale out)" 중 무엇이 답인지를 가르는 것이다. 규칙은 한 줄이다 — incoming이 막히면 scale up, outgoing이 막히면 scale out.
- outgoing(push) 병목 —
queue_time·push_time이 크고 연결 proxy 수가 많다. 한 istiod가 너무 많은 proxy fan-out을 감당하지 못하는 상태. scale out: istiod replica를 늘려 proxy 연결을 분산한다(각 proxy는 한 istiod에만 붙으므로 연결 수가 나뉜다). - incoming(watch/compute) 병목 — istiod CPU가 포화이고
convergence_time은 큰데queue_time은 상대적으로 작다. snapshot 계산이 느린 상태. scale up: istiod에 CPU/메모리를 더 준다(replica를 늘려도 각 인스턴스가 동일하게 전체를 watch·compute하므로 incoming 부하는 안 줄어든다). - 공통 근본 처방 — 둘 중 무엇이든, rate of change와 configuration size를 줄이면 양쪽이 같이 가벼워진다(곱의 항을 깎으니까). 그래서 scale out/up은 증상 완화이고, 근본은 §06이다.
istiod replica를 늘려도 각 replica는 동일한 K8s/CRD 전체를 watch하고 동일한 model을 계산한다. compute가 병목이면 replica 추가는 메모리만 더 쓰고 compute 부하는 분산되지 않는다. incoming 병목엔 scale up(인스턴스당 자원) + 변경률/설정 축소가 답이다.
06. 단일 최대 레버 — Sidecar resource (worked example)
지금까지의 분석은 한 결론으로 수렴한다: 곱의 항을 깎아라. 그 중 configuration size를 깎는 것이 가장 효과적인 이유는, 그 한 항을 줄이면 outgoing 바이트·Envoy 메모리·체감 변경률이 동시에 줄기 때문이다(§02). 이걸 실제로 하는 도구가 Sidecar 리소스다.
기본 상태에서 istiod는 모든 namespace의 설정을 읽고, 각 proxy는 mesh 전체의 서비스 설정을 받을 수 있다. out-of-box 편의의 대가가 곧 §02 네 요인을 전부 악화시키는 비용이다.
scope 미설정 시 한 proxy가 받는 설정:
mesh의 모든 Service → cluster
모든 VirtualService → route
모든 endpoint → EDS
→ proxy당 config size ↑↑, Envoy 메모리 ↑, push 바이트 ↑
→ 무관한 namespace의 변경에도 push 대상이 됨 (체감 rate of change ↑)
Sidecar 리소스로 각 workload의 egress(설정 범위)를 좁히면, istiod가 그 proxy에 계산·push하는 설정량 자체가 줄어든다. 아래는 app-a namespace의 모든 proxy에 적용되는 default Sidecar — 적용 그대로의 완전한 파일이다.
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: default
namespace: app-a # rootNamespace가 아니면 namespace 범위
spec:
egress:
- hosts:
- "./*" # 자기 namespace의 서비스
- "shared/*" # 의존하는 공용 서비스 namespace만
- "istio-system/*" # control-plane/telemetry 등 mesh 인프라
줄마다 왜: name: default + namespace 범위라서 이 namespace의 모든 proxy에 걸린다(workload별 override가 없으면). egress.hosts의 각 항목은 "이 proxy가 cluster/route를 받을 namespace의 화이트리스트"다 — 여기 없는 namespace의 service는 istiod가 이 proxy의 snapshot 계산에서 아예 빼고, 그 namespace의 변경은 이 proxy의 push 대상이 되지 않는다. 이것이 한 레버로 네 요인을 동시에 미는 메커니즘이다.
Sidecar로 scope를 좁히면:
configuration size ↓ (proxy당 cluster/route/endpoint 수 감소)
push 바이트·시간 ↓ (outgoing 부하 감소)
Envoy 메모리 ↓ (받은 설정이 작으므로)
체감 rate of change ↓ (scope 밖 변경은 push 대상에서 제외)
떴는지 한 번 확인 — scope 적용 전후로 한 proxy의 cluster 수가 줄어드는지 본다.
istioctl proxy-config cluster <pod> -n <ns> | wc -l
# Sidecar egress.hosts를 좁힌 뒤 다시 실행하면 라인 수가 눈에 띄게 감소해야 함.
# (mesh 전체 → scope 내 서비스로 cluster 목록이 줄어듦)
cluster 이름은 direction|port|subset|fqdn 규칙을 따르므로(예: outbound|8080||svc.ns.svc.cluster.local), 줄어든 목록을 보면 scope 밖 namespace의 outbound|... 항목이 사라진 것을 직접 확인할 수 있다. Sidecar/exportTo/discoverySelectors의 selection 우선순위(mesh-wide vs namespace vs workload override)와 outboundTrafficPolicy(REGISTRY_ONLY) 동작 detail은 → Sidecar scope 및 Sidecar scope note에 위임한다.
Sidecar egress.hosts는 proxy에 푸시할 설정량을 줄이는 성능 레버일 뿐, scope 밖 목적지로의 트래픽이 반드시 차단되는 것은 아니다. 외부 호출 차단은 outboundTrafficPolicy: REGISTRY_ONLY 등 별도 메커니즘이다(레버가 다르다).
07. 보조 레버 — 요인별 대조표
Sidecar가 단일 최대 레버지만, 곱의 다른 항도 깎을 수 있다. 각 레버가 §02의 어느 요인을 미는지로 정리한다.
| 요인 | 레버 | 메커니즘 |
|---|---|---|
| configuration size | Sidecar, exportTo, discoverySelectors |
proxy/istiod가 보는 설정 범위 축소 |
| rate of change | endpoint flapping 억제, deploy batching, HPA 안정화 | push 트리거 자체를 줄임 |
| rate of change | PILOT_DEBOUNCE_AFTER/MAX 조정 |
변경 합치기 창 확대(반영 latency와 trade-off) |
| number of workloads | istiod replica 증설(scale out) | proxy 연결 fan-out 분산 |
| allocated resources | istiod CPU/mem requests·limits 상향(scale up) | compute 처리량 상한 확대 |
| 전반 | phantom/stale endpoint 정리 | 죽은 endpoint가 EDS를 부풀리고 push를 늘림 |
마지막 항목 — 잘못 빠진 endpoint(phantom workload)는 EDS 설정을 부풀리고 불필요한 push와 503을 유발한다. 원인·진단은 → phantom workloads(개념은 → phantom workloads note) 참조.
자원 증설(scale up)·replica 증설(scale out)은 비용을 늘린다. 운영 순서는 항상 ① Sidecar/exportTo로 설정 크기 축소 → ② 변경률 안정화 → ③ 그래도 부족하면 scale up/out. 줄이기 전에 키우면 큰 설정을 더 빨리 밀 뿐, 곱셈 비용은 그대로다.
핵심 정리
1. istiod 부하 = (변경률) × (영향 proxy 수) × (proxy당 설정 크기) / (할당 자원).
네 요인이 곱셈으로 결합 → 가장 큰 항(보통 설정 크기)을 줄이면 모든 push가 동시에 가벼워짐.
2. 파이프라인: event → debounce → snapshot 계산 → push queue(throttle) → xDS → ACK.
데이터 플레인은 eventually consistent → 1차 진단은 항상 proxy-status.
3. 진단: convergence_time이 높을 때, 동반 신호로 방향을 가른다
- queue_time 큼 → outgoing(push) 병목 → scale OUT(replica)
- istiod CPU 포화 → incoming(compute) 병목 → scale UP(자원)
4. 가장 효과적인 단일 레버 = Sidecar resource로 설정 범위 축소.
config size·push 바이트·Envoy 메모리·체감 변경률을 한 번에 낮춤.
단, scoping은 성능 레버이지 보안 차단이 아님.
What you might be missing
- scale out이 만능이 아니다. istiod replica를 늘려도 각 replica는 전체를 watch·compute한다. compute(incoming) 병목엔 replica가 메모리만 더 먹고 부하는 안 나뉜다 — incoming은 scale up + 설정/변경률 축소로만 풀린다. queue_time vs CPU 포화로 방향을 먼저 가른 뒤 손대야 한다.
- debounce는 양날의 검. debounce 창을 늘리면 push 횟수는 줄지만 설정 반영 latency가 늘어 canary 전환·장애 대응이 느려진다. "push가 많아서 늘렸다"가 endpoint 반영 지연으로 돌아온다.
- config size는 Envoy 쪽 비용이기도 하다. 큰 설정은 istiod의 push 비용만이 아니라 각 Envoy의 메모리·warmup 시간을 늘린다. proxy가 많을수록 이 비용이 mesh 전체에 곱해진다.
Sidecarscope의 효과가 큰 진짜 이유. - phantom/stale endpoint가 조용히 부하를 키운다. 잘못 등록·미정리된 endpoint는 EDS를 부풀리고 불필요한 push와 503을 만든다. 성능 튜닝 전에 endpoint 정합성부터 확인할 것(→ phantom workloads).
- 메트릭 없이 튜닝하면 추측이다.
pilot_proxy_convergence_time/queue_time/push_time을 보지 않고 자원만 키우는 것은 어림짐작이다. 병목 방향(incoming/outgoing)을 메트릭으로 먼저 확정해야 scale up/out 선택이 맞아떨어진다. - xDS 타입별 sync는 따로 보고된다. CDS/LDS/EDS/RDS가 각각 SYNCED 상태를 갖는다 — 특정 타입만 STALE이면 그 타입(예: EDS=endpoint)에서 push가 적체된 것. 상태 의미·진단 흐름은 → 데이터 플레인 sync 상태, xDS 계층은 → xDS API 계층.