🏠 목록 istiod 성능은 변경률·할당 리소스·워크로드 수·설정 크기 네 요인의 곱으로 결정되고, Sidecar 리소스로 설정 범위를 좁히는 것이 가장 효과적인 단일 레버다 📄 MD 원본 🌓 테마
istioperformancecontrol-planescalingsidecar-scope

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는 무한 루프를 돈다.

  1. K8s API와 Istio CRD를 watch 한다(Service/EndpointSlice/Pod/Secret + Gateway/VS/DR/SE/Sidecar/PeerAuth/AuthZ),
  2. 이 입력을 in-memory model로 reconcile 한 뒤,
  3. 각 proxy마다 그 proxy가 봐야 할 범위로 Envoy 설정(LDS/RDS/CDS/EDS/SDS)을 계산하고,
  4. 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 리소스(설정 크기·범위 축소)가 단일 최대 레버가 된다. 반대로 할당 자원만 키우는 것은 분모만 키워 "같은 곱을 더 빨리 처리"할 뿐, 곱 자체는 그대로다.

✓ number of workloads vs configuration size — 헷갈리기 쉬운 두 축

둘은 다른 축이다. workload 수는 "몇 개의 proxy에 보내는가"(fan-out)이고, config size는 "각 proxy에 얼마를 보내는가"(payload)다. 1만 개 proxy라도 각자가 받는 설정이 작으면 견딜 만하고, 100개 proxy라도 각자가 mesh 전체 설정을 받으면 istiod가 휘청한다. 이 구분이 §05의 scale out vs scale up 판단으로 그대로 이어진다.


03. 동기화 파이프라인 — debounce부터 ACK까지

부하의 총량을 봤으니, 이제 그 총량이 어느 단계를 통과하는지를 본다. 변경 하나가 Envoy에 반영되기까지의 파이프라인을 알아야 "어느 단계가 막혔는가"를 진단할 수 있다.

K8s/CRD event config·endpoint change debounce coalesce events (댐) snapshot compute per-proxy LDS/RDS/CDS/EDS/SDS push queue throttle: max concurrent push xDS push -> Envoy Envoy ACK/NACK SYNCED / STALE rate-of-change 흡수 incoming(compute) 부하 본체 outgoing(push) 병목 관측점 eventually consistent — proxy-status로 확인
그림 1. istiod push 파이프라인. 변경 1건이 Envoy에 반영되기까지의 단계 — snapshot compute(강조)가 config size에 가장 민감(incoming), push queue가 outgoing 병목 지점.

각 단계의 메커니즘과 그것이 어느 요인에 민감한지:

★ 한 문장 멘탈모델

데이터 플레인은 이 파이프라인을 통해 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.

convergence_time high queue_time high? push backed up istiod CPU saturated? compute slow outgoing bottleneck -> scale OUT add istiod replicas + shrink config size incoming bottleneck -> scale UP add istiod CPU/mem + cut change/config root: shrink config size & change rate (Sidecar scope) yes no yes no
그림 2. 병목 진단 결정 트리. queue_time이 높으면 outgoing(scale OUT), CPU 포화면 incoming(scale UP). 어느 경로든 근본 처방은 config 크기·변경률 축소(Sidecar scope).
⚠ scale out으로 incoming 병목을 못 고친다

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 scopeSidecar scope note에 위임한다.

⚠ Sidecar scoping은 보안 차단이 아니다

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