--- type: note tags: [istio, performance, control-plane, scaling, sidecar-scope] created: 2026-06-07 --- # istiod 성능은 변경률·할당 리소스·워크로드 수·설정 크기 네 요인의 곱으로 결정되고, Sidecar 리소스로 설정 범위를 좁히는 것이 가장 효과적인 단일 레버다 > [!abstract] 이 문서가 다루는 것 > 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은 → [운영 플레이북](arch__src-operations-playbook.html), → [Sidecar scope](gw__src-sidecar-scope.html)에 위임하고, 여기서는 성능의 멘탈모델에 집중한다. --- ## 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에 **분배**하는 기계다. 여기서 비용이 두 방향으로 갈린다 — 이 구분이 문서 전체를 관통하는 첫 번째 축이다. ```text 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 부하는 네 요인의 곱이고, 곱이기 때문에 가장 큰 항을 깎는 것이 압도적으로 효과적이다.** ```text 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` 리소스(설정 크기·범위 축소)가 단일 최대 레버가 된다. 반대로 할당 자원만 키우는 것은 분모만 키워 "같은 곱을 더 빨리 처리"할 뿐, 곱 자체는 그대로다. > [!tip] 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에 반영되기까지의 파이프라인을 알아야 "어느 단계가 막혔는가"를 진단할 수 있다. ```mermaid flowchart LR EV["K8s/CRD event\n(config·endpoint change)"] DEB["debounce\n(coalesce events)"] CALC["snapshot compute\n(per-proxy LDS/RDS/CDS/EDS/SDS)"] PQ["push queue\n(throttle: max concurrent push)"] XDS["xDS push\n-> Envoy"] ACK["Envoy ACK/NACK"] EV --> DEB --> CALC --> PQ --> XDS --> ACK ``` 각 단계의 메커니즘과 그것이 어느 요인에 민감한지: - **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 상태](xds__note-data-plane-sync-state.html)에 위임. > [!key] 한 문장 멘탈모델 > 데이터 플레인은 이 파이프라인을 통해 **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)의 둘 중 어느 것이 동반되는지로 갈린다. ```bash # 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가 수 초 이상으로 꾸준히 크면 파이프라인 어딘가가 병목. ``` ```bash # 동기화 상태 한눈에 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.** ```mermaid flowchart TD S["convergence_time high"] Q{"queue_time high?\n(push backed up)"} C{"istiod CPU saturated?\n(compute slow)"} OUT["outgoing bottleneck\n-> scale OUT\n(add istiod replicas\n+ shrink config size)"] IN["incoming bottleneck\n-> scale UP\n(add istiod CPU/mem\n+ cut rate-of-change/config)"] ROOT["root: shrink config size & change rate\n(Sidecar scope)"] S --> Q Q -->|yes| OUT Q -->|no| C C -->|yes| IN C -->|no| ROOT OUT --> ROOT IN --> ROOT ``` - **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이다. > [!warning] 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 네 요인을 전부 악화시키는 비용이다. ```text 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 — 적용 그대로의 완전한 파일이다. ```yaml 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 대상이 되지 않는다. 이것이 한 레버로 네 요인을 동시에 미는 메커니즘이다. ```text Sidecar로 scope를 좁히면: configuration size ↓ (proxy당 cluster/route/endpoint 수 감소) push 바이트·시간 ↓ (outgoing 부하 감소) Envoy 메모리 ↓ (받은 설정이 작으므로) 체감 rate of change ↓ (scope 밖 변경은 push 대상에서 제외) ``` **떴는지 한 번 확인** — scope 적용 전후로 한 proxy의 cluster 수가 줄어드는지 본다. ```bash istioctl proxy-config cluster -n | 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](gw__src-sidecar-scope.html) 및 [Sidecar scope note](gw__note-sidecar-scope.html)에 위임한다. > [!warning] 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](arch__src-phantom-workloads.html)(개념은 → [phantom workloads note](arch__note-phantom-workloads.html)) 참조. > [!tip] 순서: 먼저 줄이고, 그다음 키운다 > 자원 증설(scale up)·replica 증설(scale out)은 비용을 늘린다. 운영 순서는 항상 ① `Sidecar`/`exportTo`로 설정 크기 축소 → ② 변경률 안정화 → ③ 그래도 부족하면 scale up/out. 줄이기 전에 키우면 큰 설정을 더 빨리 밀 뿐, 곱셈 비용은 그대로다. --- ## 핵심 정리 ```text 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 전체에 곱해진다. `Sidecar` scope의 효과가 큰 진짜 이유. - **phantom/stale endpoint가 조용히 부하를 키운다.** 잘못 등록·미정리된 endpoint는 EDS를 부풀리고 불필요한 push와 503을 만든다. 성능 튜닝 전에 endpoint 정합성부터 확인할 것(→ [phantom workloads](arch__src-phantom-workloads.html)). - **메트릭 없이 튜닝하면 추측이다.** `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__note-data-plane-sync-state.html), xDS 계층은 → [xDS API 계층](xds__note-xds-api-layers.html).