본문 바로가기
1. K8s Core & Architecture/1.1. 컨트롤 플레인 (Control Plane) 심층 분석

Kube-apiserver의 캐싱 아키텍처와 성능 향상 원리

by K8s Architect 2026. 3. 18.

Kube-apiserver의 캐싱 아키텍처와 성능 향상 원리

1. 분산 시스템의 병목: 왜 Kube-apiserver에 캐시가 필요한가?

쿠버네티스 클러스터 내부에는 수많은 컴포넌트(Kubelet, Kube-scheduler, Kube-controller-manager 등)가 존재하며, 이들은 지속적으로 API 서버에 파드(Pod), 노드(Node), 서비스(Service) 등의 상태를 묻고(GET/LIST) 변화를 감시(WATCH)합니다. 만약 API 서버가 이러한 모든 읽기 요청을 백엔드 저장소인 etcd로 직접 포워딩한다면, etcd는 막대한 디스크 I/O와 네트워크 부하를 견디지 못하고 즉각적으로 마비될 것입니다.

etcd는 본질적으로 데이터를 안전하게 저장하고 합의(Raft)를 이루는 데 최적화된 시스템이지, 초당 수만 건의 대규모 읽기 요청을 처리하기 위한 인메모리 캐시 시스템이 아닙니다. 이러한 구조적 한계를 극복하고 API 서버의 응답 지연 시간(Latency)을 마이크로초 단위로 단축하기 위해 도입된 핵심 아키텍처가 바로 Kube-apiserver 내부의 인메모리 캐싱 계층인 'Watch Cache(내부 모듈명: Cacher)'입니다.

2. Watch Cache (Cacher)의 내부 구조와 동작 원리

Kube-apiserver 내부에 존재하는 Watch Cache는 etcd에 저장된 클러스터 상태 데이터의 완벽한 인메모리(In-memory) 복제본 역할을 수행합니다. API 서버가 구동될 때, 각각의 리소스 타입(Pods, ConfigMaps, Secrets 등)마다 별도의 Cacher 인스턴스가 생성됩니다.

  • 초기 동기화 (List & Watch): Cacher는 생성 직후 etcd를 향해 해당 리소스의 전체 목록을 가져오는 LIST 요청을 한 번 수행하여 메모리를 채웁니다. 그 이후에는 etcd와 지속적인 WATCH 스트림 연결을 맺고, etcd에서 발생하는 모든 상태 변화 이벤트(Add, Update, Delete)를 실시간으로 수신하여 자신의 로컬 메모리 캐시를 최신 상태로 동기화합니다.
  • 이벤트 분배 (Fan-out): 클러스터 내의 수백 개의 Kubelet이나 컨트롤러가 API 서버에 WATCH 요청을 맺을 때, API 서버는 이 요청을 etcd로 보내지 않습니다. 대신 Cacher가 자신이 etcd로부터 받은 단일 이벤트를 다수의 클라이언트 요청자들에게 복제하여 뿌려주는 Fan-out 패턴을 사용합니다. 이를 통해 etcd를 향한 커넥션 수는 극도로 최소화하면서도, 수천 개의 클라이언트에게 지연 없이 상태 변화를 스트리밍할 수 있습니다.

3. 읽기 요청(GET/LIST) 최적화와 ResourceVersion의 비밀

Watch Cache의 진가는 클라이언트가 데이터를 조회할 때 발휘됩니다. 쿠버네티스 API 클라이언트(client-go)는 데이터를 요청할 때 ResourceVersion이라는 메타데이터를 함께 전송하여 데이터의 신선도(Freshness)를 제어합니다.

  • 캐시 히트 (ResourceVersion="0" 또는 미지정): 클라이언트가 ResourceVersion="0"을 명시하거나 아예 지정하지 않고 데이터를 요청하면, API 서버는 이 요청을 etcd까지 내려보내지 않고 즉시 자신의 인메모리 Watch Cache에서 데이터를 읽어 반환합니다. 이는 디스크 I/O나 네트워크 페이로드 오버헤드를 완전히 제거하므로, API 서버의 성능을 극대화하는 가장 핵심적인 메커니즘입니다.
  • 정합성 보장 (ResourceVersion="특정 버전"): 클라이언트가 자신이 마지막으로 확인했던 특정 리비전 번호를 명시하여 요청할 경우, API 서버의 Watch Cache는 자신의 메모리에 해당 리비전 이상의 데이터가 확보되어 있는지 확인합니다. 만약 캐시가 아직 그 버전까지 동기화되지 않았다면, 캐시가 해당 버전으로 업데이트될 때까지 아주 짧은 시간 동안 요청을 블로킹(Blocking)하여 궁극적인 데이터 정합성(Eventual Consistency)을 보장합니다.
  • 캐시 우회 (Quorum Read): 관리자가 kubectl get pods 명령을 내릴 때 최신의 절대적인 상태가 필요하다면, 내부적으로 강한 일관성(Strong Consistency) 읽기 요청이 발생합니다. 이때는 Watch Cache를 완전히 우회(Bypass)하고 etcd 클러스터의 과반수 합의를 거친 최신 데이터를 직접 읽어옵니다. 성능 비용이 가장 비싼 작업입니다.

4. 대규모 데이터 조회를 위한 청킹(Chunking) 아키텍처

클러스터 규모가 커지면 단일 LIST 요청이 응답해야 할 데이터의 크기도 수백 메가바이트 단위로 팽창합니다. 만약 5,000개의 파드 목록을 요청받았을 때 캐시가 이를 하나의 거대한 JSON 응답으로 직렬화하여 반환하려 한다면, Kube-apiserver 프로세스는 즉각적인 메모리 스파이크를 겪고 OOM(Out of Memory)으로 강제 종료될 것입니다.

이를 방지하기 위해 캐시 아키텍처에는 청킹(Chunking) 메커니즘이 통합되어 있습니다. 클라이언트가 데이터를 요청할 때 한 번에 받을 개수 제한(limit=500)을 설정하면, API 서버는 캐시에서 첫 500개의 데이터를 잘라 반환하고 응답 헤더에 continue 토큰을 포함시킵니다. 클라이언트는 이 토큰을 사용해 다음 500개의 데이터를 순차적으로 요청합니다. 결과적으로 전체 메모리를 한 번에 소진하지 않고도 대규모 데이터를 안전하게 페이징(Paging)하여 전송할 수 있습니다.

5. 성능 튜닝 및 운영 시 주의사항

Watch Cache는 메모리를 담보로 CPU 연산과 응답 시간을 교환하는 구조이므로, 대규모 클러스터 운영 시에는 반드시 섬세한 메모리 튜닝이 동반되어야 합니다.

  • 메모리 한도 설정: 캐시는 API 서버 메모리 점유율의 절대적인 비중을 차지합니다. 노드가 1,000대 이상이거나 시크릿(Secret), 커스텀 리소스(CRD)의 객체 수가 비정상적으로 많은 클러스터에서는 Kube-apiserver 파드의 메모리 제한(Limits)을 8GB 이상으로 매우 넉넉하게 할당해야 메모리 압박을 견딜 수 있습니다.
  • 캐시 크기 수동 튜닝: 특정 리소스 객체의 변화량이 비정상적으로 많거나 크기가 클 경우, API 서버 구동 플래그인 --watch-cache-sizes를 통해 리소스별 캐시 버퍼의 크기를 강제로 조정할 수 있습니다. (예: --watch-cache-sizes=secrets#5000,pods#10000)
  • 클라이언트 설계 원칙: 클러스터 내부에서 동작하는 커스텀 오퍼레이터나 컨트롤러를 개발할 때는 반드시 Informer 패턴을 사용해야 합니다. Informer는 내부적으로 ResourceVersion="0" 기반의 LIST와 지속적인 WATCH를 병행하므로 Kube-apiserver의 캐시 메커니즘을 100% 활용합니다. 반대로 API 서버에 주기적으로 단순 폴링(Polling)을 수행하는 클라이언트를 작성하는 것은 캐시 메커니즘을 무력화시키고 API 서버를 죽이는 치명적인 안티 패턴입니다.