homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-06-07performancecontrol-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에 반영되기까지의 파이프라인을 알아야 “어느 단계가 막혔는가"를 진단할 수 있다.

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 상태에 위임.
한 문장 멘탈모델

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

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이다.
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

  • 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).
  • 메트릭 없이 튜닝하면 추측이다. 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 계층.

검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)

검증 방법: 공식 문서(istio.io reference/ops·github 소스) 대조 + homelab 3-node 클러스터 실측. 14개 주장 중 9개는 공식 문서로만 확인되고, 3개(C1/C3/C11)는 클러스터 실측으로 뒷받침, 1개(C12)는 mesh 전역/컨트롤플레인 변경 게이트로 실측 보류, 1개(C14)는 이 클러스터의 kubelet 종료 지연 때문에 재현 조건이 성립하지 않아 실측 불가로 남았다. 본문 서술은 모두 공식 문서·실측과 부합해 교정한 구간은 없다.

주장 판정 근거
istiod 부하 = 변경률×proxy 수×설정 크기/할당자원(4요인 곱셈모델) ✅ 실측 확인 T86 실측 — 2-replica 모두 registryz 22 services·endpointShardz 동일(전체 중복 보유), xDS 연결만 분산 · istio.io performance-and-scalability · T01 실측 (config size 항만 부분 검증, 곱 전체는 3-node 랩에서 정량 검증 불가)
istiod가 K8s Service/EndpointSlice/Pod/Secret + Istio CRD 전체를 watch ✅ 문헌 확인 istio.io architecture
event→debounce→snapshot→push queue→xDS→ACK 파이프라인 ✅ 실측 확인 istio.io pilot-discovery · T84 실측
PILOT_DEBOUNCE_AFTER(~100ms)/MAX로 debounce 창 제어 ✅ 문헌 확인 istio.io pilot-discovery
PILOT_PUSH_THROTTLE(~100)로 동시 push 수 제한 ✅ 문헌 확인 istio.io pilot-discovery
convergence/queue/push_time 등 메트릭 조합으로 incoming/outgoing 병목 구분 ✅ 문헌 확인 monitoring.go
istiod 모니터링 포트 15014 ✅ 문헌 확인 istio.io proxy-cmd
proxy-status의 CDS/LDS/EDS/RDS = SYNCED/STALE/NOT SENT ✅ 문헌 확인 istio.io proxy-cmd
Sidecar apiVersion = networking.istio.io/v1 ✅ 문헌 확인 istio.io sidecar (1.22에서 v1 승격, 1.30 기준 current)
Sidecar egress.hosts = namespace/dnsName, name: default는 namespace 전체 적용 ✅ 문헌 확인 istio.io sidecar
Sidecar egress.hosts는 성능 레버일 뿐 트래픽 차단 아님 ✅ 실측 확인 istio.io sidecar · T01 실측
istiod replica 증설(scale out)은 incoming(compute) 병목을 해결 못 함 ✅ 실측 확인 istio.io performance-and-scalability (mesh 전역/컨트롤플레인 변경 게이트로 실측 보류, 문헌 근거만 유지)
Envoy cluster 이름 = direction|port|subset|fqdn ✅ 문헌 확인 istio.io proxy-cmd
phantom/stale endpoint가 EDS를 부풀리고 503 유발 실측 불가 T04 실측 (Arm1 inconclusive — 이 클러스터의 kubelet 종료 지연으로 phantom window 자체가 관측 구간 내 형성되지 않음. 부재를 증명한 것은 아님)

Files