Skip to content

Latest commit

 

History

History
107 lines (81 loc) · 13.9 KB

File metadata and controls

107 lines (81 loc) · 13.9 KB

v1.14 — Karpenter CapacityBuffer: 가상 파드로 여유 용량을 미리 만들어두는 사전 프로비저닝

메타데이터

항목 내용
프로젝트 Karpenter (kubernetes-sigs/karpenter)
관련 릴리스 v1.14.0 (2026-07-11), 기능 자체는 alpha 피처 게이트(CapacityBuffer=false 기본값)
관련 PR #3028 — feat: Support CapacityBuffer API (merged 2026-06-19), #2898 — docs: Support Capacity Buffers in Karpenter (merged 2026-06-18, RFC), #3129 — feat: bump CapacityBuffer to v1beta1 and emit operator_* status metrics (merged 2026-07-09)
설계 문서 designs/capacity-buffers.md
발견 출처 v1.14.0 릴리스 노트 Features 항목("Support CapacityBuffer API (#3028)", "bump CapacityBuffer to v1beta1 ... (#3129)")
검증 근거 로컬 클론(sources/karpenter)의 실제 커밋 82671c0b(diff 28개 파일, +4487/-10), dfa29134(v1alpha1→v1beta1 리네임), 설계 문서 원문

개요

Karpenter는 지금까지 파드가 Pending 상태로 들어와야만("just-in-time") 노드를 만들었다. 이 방식은 항상 스케줄링 지연(노드 부팅 시간만큼)을 동반한다. v1.14에서 alpha 기능으로 추가된 CapacityBuffer API는 Kubernetes SIG Autoscaling의 표준 autoscaling.x-k8s.io/v1alpha1 CapacityBuffer 스펙을 그대로 지원해, "이 Deployment의 20% 만큼 여유 노드를 미리 켜둬라" 같은 선언을 받아 실제 파드 없이도 노드를 미리 프로비저닝한다. 핵심 구현 방식은 실제 파드를 만들지 않고 스케줄링 시뮬레이션에만 존재하는 "가상 파드(virtual pod)"를 매 프로비저닝 루프마다 새로 만들어 끼워넣는 것이다.

사전 지식 — Karpenter의 단일 루프 프로비저닝과 "Pending 파드가 있어야 노드가 생긴다"는 전제

Karpenter의 Provisioner.Reconcile은 매 루프마다 GetPendingPods로 스케줄 안 된 파드 목록을 모으고, Schedule이 그 파드들을 기존 노드/새 NodeClaim에 배치하는 시뮬레이션을 한 번 돌린 뒤, 시뮬레이션 결과대로 실제 NodeClaim을 만든다. 이 "단일 루프"(single provisioning loop) 구조는 실제 파드와 앞으로 만들 파드를 한 번에 같이 계산하기 때문에 배치 결정이 항상 일관된다는 장점이 있지만, 뒤집어 보면 Pending 파드가 실제로 존재해야만 이 루프가 노드를 만든다는 뜻이기도 하다.

노드를 미리 켜두고 싶은 사용자들은 이전까지 두 가지 우회로를 써왔다(설계 문서가 명시):

  • balloon pod: 실제 워크로드보다 우선순위가 낮은 더미 파드를 미리 배치해두고, 진짜 파드가 오면 그 balloon pod를 쫓아내며(preemption) 자리를 준다. 유지보수가 어렵고 스케줄러에 불필요한 preemption 부하를 준다.
  • 고정 크기 NodePool: 노드 레벨에서 최소 대수를 못박아둔다. 워크로드가 실제로 얼마나 필요한지와 무관하게 용량 계획을 수동으로 해야 한다.

두 방식 모두 "파드가 얼마나 필요한지"가 아니라 "노드가 몇 대 있어야 하는지"를 사용자가 직접 계산해야 하는 게 근본 한계다. Kubernetes SIG Autoscaling은 이미 Cluster Autoscaler용으로 autoscaling.x-k8s.io/v1alpha1 CapacityBuffer CRD를 표준화해뒀고, Karpenter는 이 표준 API를 그대로 채택해 파드 단위 추상화("이 워크로드만큼 여유를 둬라")를 제공하기로 했다. 다만 Cluster Autoscaler는 실행 중인 파드의 scale subresource에서 템플릿을 뽑아오는 반면, Karpenter는 Deployment/StatefulSet/ReplicaSet의 spec.template.spec을 직접 읽는다 — 실행 중인 파드가 하나도 없어도(워크로드를 막 만든 시점부터) 버퍼가 즉시 동작할 수 있다는 차이가 있다.

변경 분석 — Pending 파드가 있어야만 돌던 루프에 "가상 파드"를 주입

[변경 전] Pending 파드가 실제로 존재해야만 노드가 만들어짐
┌──────────────┐  GetPendingPods   ┌────────────────┐  Schedule   ┌───────────────┐
│ 실제 Pending  │ ────────────────▶│ 스케줄링 시뮬레이션 │ ──────────▶│ 필요한 만큼만   │
│ 파드 목록      │                  │ (기존 노드/신규     │             │ NodeClaim 생성 │
│ (etcd에 존재)  │                  │  NodeClaim 배치)    │             └───────────────┘
└──────────────┘                  └────────────────┘
   ※ 파드가 스케줄 대기 상태로 들어오기 전까지는 노드 생성 트리거가 없음
   ※ "미리 켜두고 싶다"는 balloon pod/고정 NodePool 같은 우회로만 존재

GetPendingPods는 원래 실제 Pending 파드만 반환했다(p.podUtils.Filter(...) → validate → 반환). v1.14는 이 뒤에 한 단계를 추가한다: CapacityBuffer 피처 게이트가 켜져 있으면, ReadyForProvisioning 조건이 True이고 목표 replica 수가 0보다 큰 모든 CapacityBuffer를 나열하고(listBuffersReadyForProvisioning), 각각의 podTemplateRef/scalableRef를 실제 워크로드 스펙으로 해석한 뒤(resolveVirtualPodSpec), 그 스펙으로 N개의 "가상 파드"를 in-memory로 만들어(buildVirtualPods) Pending 파드 목록 뒤에 덧붙인다(appendVirtualPods).

이 가상 파드들은 etcd에 한 번도 쓰이지 않는다 — 매 프로비저닝 루프마다 버퍼 상태(status)로부터 다시 만들어지고, 그 루프가 끝나면 사라진다. 설계 문서는 이를 캐싱하지 않는 이유를 명시한다: 클러스터 상태(파드 생성/삭제, 노드 스케일, 버퍼 상태 갱신)가 매 루프 바뀌기 때문에 항상 최신 버퍼 상태에서 다시 만들어야 정확성이 보장되고, 이 재생성 자체는 비용이 싼 연산이라고 판단했다.

가상 파드가 시뮬레이션에 섞여 들어가면 다음 단계인 Schedule은 실제 파드와 가상 파드를 구분하지 않고 함께 배치를 계산한다 — 기존 노드에 다 들어가면 새 NodeClaim을 안 만들고, 안 들어가면 새 NodeClaim을 만든다. 이 결과로 버퍼의 Provisioning 상태 조건이 갱신된다: 가상 파드가 새 NodeClaim 없이 기존 용량에 배치될 수 있을 때만 True가 된다 — "실제로 지금 쓸 수 있는 용량"과 "만들라고 요청은 했지만 아직 부팅 중인 용량"을 구분하기 위해서다.

다만 가상 파드를 실제 파드처럼 취급하면 안 되는 지점이 여러 군데 있어서, 구현은 그 경계를 세심하게 나눈다:

  • 노드 노미네이션(nomination) 제외: Results.Record에서 노드에 파드가 있으면 그 노드를 disruption(drift/expiry 포함 전체)으로부터 잠그는 노미네이션을 트리거하는데, 가상 파드는 매 루프 다시 주입되므로 노미네이션에 포함시키면 그 노드가 영원히 disruption 대상에서 빠지게 된다. 그래서 isVirtualBufferPod 체크로 걸러낸다.
  • 빈 노드 판정(emptiness) 별도 차단: 노미네이션에서 빠졌으니 노드에 남은 게 가상 파드뿐이면 "비어있다"고 오판해서 지워버릴 수 있다. 그래서 disruption/emptiness.goShouldDisruptcluster.HasBufferPods(providerID) 체크를 별도로 추가해, 가상 파드가 있는 노드는 emptiness 경로에서만 명시적으로 막는다. Cluster.bufferPodCounts(providerID → 개수 맵)는 매 프로비저닝 패스 후 통째로 갱신된다.
  • consolidation(다운사이징)은 별도 처리 불필요: single/multi-node consolidation은 대체 배치가 가능한지 SimulateScheduling으로 다시 확인하는데, 이 시뮬레이션이 GetPendingPods를 다시 호출하면서 가상 파드도 자연히 다시 섞여 들어간다 — 즉 별도 코드 없이도 "가상 파드가 못 들어가는 통합은 거절"이 성립한다.
  • 스케줄링 결정 기록(pod scheduling decisions) 제외: 가상 파드는 etcd에 없으므로 삭제 이벤트가 없다. MarkPodSchedulingDecisions에 그대로 넣으면 버퍼가 삭제/스케일다운돼도 그 기록이 영원히 정리되지 않고 메모리 누수로 남는다. filterVirtualPodErrors/filterVirtualPodMapping으로 걸러낸다.

설계 결정 및 트레이드오프

표준 API를 그대로 채택, 직접 의존은 안 함: CapacityBuffer 타입은 k8s.io/autoscaler의 upstream 정의를 Karpenter 자체 패키지(pkg/apis/autoscaling/v1alpha1, 이후 v1beta1)로 복사해왔다 — 모듈 전체를 의존성으로 끌어오지 않으면서 릴리스 주기를 Karpenter가 직접 통제하기 위해서다. 대신 향후 upstream과 동기화 부담이 생긴다.

alpha 피처 게이트로 시작: CapacityBuffer 게이트는 기본값 false다. 초기 구현은 podTemplateRef/scalableRef 모두 같은 네임스페이스 참조만 지원하고, expireAfter 필드(upstream sig-autoscaling 합의 필요)와 일회성(ephemeral) 버퍼 전략(Kueue 같은 배치 시스템용)은 명시적으로 다음 작업으로 미뤘다 — 최소 기능(active 전략, 같은 네임스페이스 참조)만으로 먼저 내보내고 피드백을 받는 전략이다.

설계 문서가 제시한 목표 중 아직 구현되지 않은 부분도 있다: 설계 문서는 "버퍼 파드에 더 낮은 disruption cost를 줘서 통합 시 실제 워크로드보다 먼저 쫓아내지도록 한다"는 목표를 적었지만, 실제 병합된 코드(pkg/utils/disruption/disruption.goEvictionCost)에는 v1.14.0 시점까지 가상 버퍼 파드를 구분하는 로직이 없다 — emptiness 차단과 노미네이션 제외만 구현됐고, disruption cost 차등화는 아직 후속 작업으로 남아있다.

merge 다음날 곧바로 v1beta1로 승격: #3028(alpha, 2026-06-19 merge) 이후 3주만인 #3129(2026-07-09 merge)에서 API 버전을 v1alpha1에서 v1beta1로 올렸다 — 릴리스 노트 기준 같은 v1.14.0 안에 두 PR이 모두 들어갔다. 사실상 alpha API로 공개된 기간이 짧았던 셈인데, 커밋 자체는 타입 이동(패키지 리네임)과 operator 상태 메트릭(operator_*) 추가가 대부분이라 API 스펙 자체가 바뀐 것은 아니다.

[변경 후] CapacityBuffer가 켜져 있으면 가상 파드가 매 루프 주입됨
┌───────────────┐            ┌──────────────────┐
│ 실제 Pending   │            │ CapacityBuffer CR  │
│ 파드 목록       │            │ (ReadyForProvisioning│
└──────┬────────┘            │  = True, replicas>0)│
       │                     └─────────┬──────────┘
       │           listBuffersReadyForProvisioning
       │           + resolveVirtualPodSpec + buildVirtualPods
       │                     ┌─────────▼──────────┐
       └────────────────────▶│  가상 파드(in-memory,│
                             │  etcd에 안 씀, 매 루프│
                             │  재생성)              │
                             └─────────┬──────────┘
                                       ▼
                        ┌───────────────────────────┐
                        │ Schedule (실제+가상 파드 함께)│
                        │  기존 노드에 다 들어가면      │
                        │   → Provisioning: True,     │
                        │     신규 NodeClaim 안 만듦    │
                        │  안 들어가면                 │
                        │   → NodeClaim 생성,          │
                        │     Provisioning: False      │
                        └───────────────────────────┘
   ※ 가상 파드는 노미네이션(disruption 전체 잠금) 대상에서 제외
   ※ emptiness만 별도로 HasBufferPods 체크, consolidation은 재시뮬레이션으로 자연히 처리

더 살펴볼 점

  • 설계 문서가 목표로 적은 "버퍼 파드에 낮은 disruption cost 부여"는 이후 릴리스에서 실제로 구현될까?
  • appendVirtualPods가 매 프로비저닝 루프마다 List+Get을 도는 구조(설계 문서/코드 TODO가 언급한 issue #3090)는 버퍼 개수가 늘어나는 대규모 클러스터에서 캐시 기반으로 바뀔까?
  • alpha에서 v1beta1로 3주 만에 넘어간 속도를 볼 때, GA 전환과 expireAfter(ephemeral 버퍼) 지원은 언제쯤 나올까?

참고 자료

참고 외 별도 확인 링크

본문 참고 자료로 충분했다.