Skip to content

Latest commit

 

History

History
150 lines (108 loc) · 15.3 KB

File metadata and controls

150 lines (108 loc) · 15.3 KB

v1.14 Karpenter 리사이즈 인지형 파드 리소스 계산

메타데이터

항목 내용
프로젝트 Karpenter (kubernetes-sigs/karpenter)
관련 릴리스 v1.14.0 (2026-07-11)
관련 PR #3065 (chore: use status resources in pod resource calculation) (생성 2026-06-02, merged 2026-07-01)
관련 설계 문서 designs/in-place-pod-vertical-scaling.md (#3037, 생성 2026-05-21, merged 2026-07-09)
발견 출처 v1.14.0 릴리스 노트 Chores 항목("use status resources in pod resource calculation (#3065)")
검증 근거 로컬 클론(sources/karpenter)의 실제 커밋 edccab9f(#3065, pkg/utils/resources/resources.go diff), designs/in-place-pod-vertical-scaling.md RFC 원문, kubernetes/kubernetes 저장소 staging/src/k8s.io/component-helpers/resource/helpers.go(v1.35.0)의 실제 구현, PR #3065 설명

개요

Karpenter는 컨솔리데이션, 드리프트, 만료 판단을 내릴 때 각 노드에 파드를 올려보는 시뮬레이션으로 필요 용량을 계산하는데, 지금까지 이 계산은 pod.spec.containers[].resources.requests만 읽었다. Kubernetes 1.33에서 베타로 기본 활성화된 InPlacePodVerticalScaling은 파드를 재시작하지 않고 리소스 요청량을 바꿀 수 있게 해주는데, 리사이즈가 진행되는 동안에는 spec.requests(원하는 값)와 kubelet이 실제로 점유 중인 값(status.allocatedResources)이 서로 어긋날 수 있다. v1.14.0은 파드 리소스 계산 함수 Ceiling()UseStatusResources: true를 켜서, kube-scheduler와 동일하게 spec과 실제 점유량 중 더 큰 값을 쓰도록 고쳤다. 이는 InPlacePodVerticalScaling 지원 RFC가 제안한 두 단계 중 1단계(Part 1)에 해당하며, 2단계(Part 2, VPA 웹훅에 의한 재생성 후 리소스 예측)는 v1.14.0 시점까지 아직 구현되지 않았다.

사전 지식

InPlacePodVerticalScaling: 재시작 없이 파드 리소스를 바꾸는 기능

KEP-1287이 도입한 이 기능은 Kubernetes 1.27에서 알파, 1.33에서 베타로 기본 활성화됐다. 기존에는 pod.spec.containers[].resources가 파드 생성 이후 불변이었지만, 이 기능이 켜지면 컨트롤러(대표적으로 VPA)가 resize 서브리소스를 통해 실행 중인 파드의 리소스 요청량을 patch할 수 있다.

kubectl patch pod my-pod --subresource=resize --patch '{
  "spec": {"containers": [{"name": "app", "resources": {"requests": {"cpu": "2"}}}]}
}'

kubelet은 이 patch를 받아 실제로 리소스를 조정하고, 진행 상황을 파드 상태에 두 가지 경로로 반영한다.

  • status.containerStatuses[].resourcesstatus.containerStatuses[].allocatedResources: kubelet이 실제로 점유(allocate)한 리소스 값.
  • PodResizePending / PodResizeInProgress 컨디션: 리사이즈가 즉시 불가능한지(Infeasible), 나중에 가능할 수 있는지(Deferred), 아니면 진행 중인지를 알려준다.

리사이즈 도중에는 spec.requests가 목표값을, status.allocatedResources가 kubelet이 지금 실제로 붙잡고 있는 값을 가리키므로 두 값이 다를 수 있다. 리사이즈 다운이 진행 중이면 spec이 더 작고, 리사이즈 업이 진행 중이면 spec이 더 크다.

kube-scheduler의 UseStatusResources

kube-scheduler는 파드가 실제로 얼마나 리소스를 쓰는지 계산할 때 k8s.io/component-helpers/resource 패키지의 PodRequests() 헬퍼를 쓴다. 이 헬퍼는 PodResourcesOptions.UseStatusResources 플래그를 받는데, 이 값이 켜지면 컨테이너별로 spec.requests, status.containerStatuses[].resources.requests(kubelet이 실제로 actuate한 값), status.containerStatuses[].allocatedResources(kubelet이 점유 중인 값) 세 값 중 최댓값을 쓴다(리사이즈가 Infeasible로 거부된 경우는 spec을 제외한 나머지 둘의 최댓값). kube-scheduler는 리사이즈 도중에도 노드의 실제 여유 용량을 안전하게 보수적으로 잡기 위해 이 옵션을 기본적으로 켜서 쓴다.

Karpenter의 클러스터 상태와 가용 용량 계산

Karpenter는 각 노드를 StateNode로 추적하며, 그 위에 스케줄된 파드마다 resources.RequestsForPods(pod)를 호출해 podRequests 맵에 저장한다(pkg/controllers/state/statenode.go). 노드의 가용 용량은 이 값들의 합으로 계산된다.

func (in *StateNode) Available() corev1.ResourceList {
    return resources.Subtract(in.Allocatable(), in.PodRequests())
}

RequestsForPods()는 내부적으로 Ceiling(pod).Requests를 파드마다 합산한 값이다(pkg/utils/resources/resources.go). 즉 Ceiling()이 각 파드의 리소스 요청량을 얼마로 보느냐가, 컨솔리데이션, 드리프트, 신규 스케줄링 시뮬레이션 전체에서 노드가 얼마나 비어 있는가를 결정한다.

변경 분석

변경 전: spec.requests만 읽어 리사이즈 도중 용량을 잘못 계산

[변경 전] Ceiling()은 spec.requests만 본다
┌───────────────────────────────────────────────┐
│ Node-B (target, Allocatable = 8 vCPU)          │
│  Pod-Z: 리사이즈 다운 진행 중                     │
│    spec.requests   = 2 vCPU  (목표값)           │
│    status.allocatedResources = 6 vCPU (실점유)  │
└───────────────────────────────────────────────┘
        │
        ▼
Karpenter Ceiling(Pod-Z) = spec.requests = 2
  → StateNode.PodRequests() 에 2로 반영
  → Available() = 8 - 2 = 6  (Karpenter가 보는 여유)

kube-scheduler(UseStatusResources=true) 관점
  → max(spec, allocated) = 6
  → 실제 여유 = 8 - 6 = 2

Karpenter 판단: 여유 6 vCPU → 컨솔리데이션으로 파드 이주 결정
      → kube-scheduler 판단: 실제 여유는 2 vCPU뿐 → 배치 거부
      → 파드가 일시적으로 Pending 상태에 빠짐

Ceiling()은 원래 resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{})를 그대로 호출해 UseStatusResources가 기본값인 false였다. 그 결과 파드 상태(status.allocatedResources)는 전혀 참조하지 않고 spec.requests만 읽었다. 평상시에는 spec.requests가 곧 실제 점유량과 같으므로 문제가 없지만, 리사이즈가 진행 중인 구간에서는 kube-scheduler가 보는 값과 Karpenter가 보는 값이 서로 달라졌다. RFC는 이 어긋남이 두 방향으로 위험하다고 짚는다. 리사이즈 다운 도중에는 Karpenter가 노드를 실제보다 더 비어있다고 착각해 컨솔리데이션으로 파드를 그 노드에 밀어넣으려다 스케줄러에게 거부당하고, 리사이즈 업 도중에는(스케줄러와 마찬가지로 spec을 그대로 읽으므로) 최댓값을 놓치지는 않지만 여전히 kubelet의 실점유 값과 무관하게 spec만 신뢰한다는 점에서 근본적으로 같은 계산 경로의 결함이었다.

변경 후: max(spec, actuated, allocated)로 kube-scheduler와 같은 시야를 확보

수정은 pkg/utils/resources/resources.goCeiling() 한 줄이다.

func Ceiling(pod *v1.Pod) v1.ResourceRequirements {
    return v1.ResourceRequirements{
        Requests: resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{UseStatusResources: true}),
        Limits:   resourcehelper.PodLimits(pod, resourcehelper.PodResourcesOptions{}),
    }
}

UseStatusResources: true가 켜지면 PodRequests()는 컨테이너마다 AggregateContainerRequests()를 거쳐 spec.requests, status.containerStatuses[].resources.requests, status.containerStatuses[].allocatedResources 세 값의 최댓값(determineEffectiveRequests, 리사이즈가 Infeasible이면 spec을 제외한 나머지 둘의 최댓값)을 반환한다. status.allocatedResources가 아예 없는 파드(리사이즈가 한 번도 일어나지 않은 일반 파드)는 spec.requests 하나만 남으므로 기존 동작과 동일하다. 이 값은 Ceiling().RequestsRequestsForPods()StateNode.podRequests 경로를 그대로 타고 Available(), PodRequests()에 반영되며, pkg/controllers/provisioning/scheduling/scheduler.go가 기존 노드의 여유 용량과 데몬셋 오버헤드를 계산할 때도 동일한 함수를 호출하므로 신규 스케줄링, 컨솔리데이션, 드리프트 재시뮬레이션 전체가 한 번에 kube-scheduler와 같은 시야를 갖게 된다.

설계 결정 및 트레이드오프

조건 없이 항상 켰다: k8s.io/component-helpersPodResourcesOptions.UseStatusResources 문서 주석은 InPlacePodVerticalScaling 기능이 꺼져 있다면 반드시 false여야 한다고 명시하지만, Karpenter는 클러스터에서 이 기능이 실제로 켜져 있는지 확인하는 별도 분기 없이 Ceiling()에서 무조건 true로 설정했다. 이것이 안전한 이유는 기능이 꺼진 클러스터에서는 애초에 kubelet이 status.allocatedResources를 채우지 않으므로 determineEffectiveRequestsspec.requests 하나만 보고 기존 동작으로 자연히 되돌아가기 때문이다(RFC의 폴백 표에 명시된 필드가 설정되지 않으면 spec으로 폴백해 하위 호환된다는 케이스). Karpenter 쪽에 별도 기능 플래그나 버전 검사를 두지 않고, API 서버/kubelet의 데이터 유무에 안전한 폴백을 맡긴 셈이다.

Requests만 고치고 Limits는 그대로 두었다: 같은 커밋에서 Limits: resourcehelper.PodLimits(pod, resourcehelper.PodResourcesOptions{})UseStatusResources를 켜지 않은 채 남았다. RFC 본문도 이 비대칭을 별도로 설명하지 않는다(확인 필요). 다만 RFC가 다루는 두 시나리오(리사이즈 도중의 용량 오판, 웹훅에 의한 재생성 후 요청량 변화) 모두 스케줄링 가능 여부를 좌우하는 것은 requests이지 limits가 아니므로, 이번 변경의 문제 범위 자체가 애초에 requests에 한정돼 있었다고 볼 수 있다.

RFC의 두 단계 중 Part 1만 먼저 릴리스했다: RFC는 문제를 두 부분으로 나눈다. Part 1은 이 리포트가 다루는 리사이즈 도중 용량을 정확히 계산하는 부분이고, Part 2는 컨솔리데이션으로 파드를 evict했을 때 VPA 같은 뮤테이팅 웹훅이 재생성된 파드에 다른 리소스 요청량을 설정할 수 있다는 것을 미리 예측하는 부분이다. Part 2는 VPA VerticalPodAutoscaler 오브젝트를 watch해서 status.recommendation을 읽는 방식을 1차안으로 제시하고, dry-run 파드 생성이나 어노테이션 기반 예측 등 세 가지 대안을 함께 검토했지만(각각 RBAC 범위, autoscaler 종속성, 스키마 합의 필요성이라는 트레이드오프가 있다), v1.14.0 코드베이스에는 VerticalPodAutoscaler 관련 구현이 아직 없다. Part 1은 업스트림 API 옵션 하나를 켜는 것만으로 끝나는 순수 정확성 개선이라 별도 설계 합의 없이 먼저 낼 수 있었던 반면, Part 2는 어떤 예측 메커니즘을 채택할지 자체가 트레이드오프 논의 대상이라 시간이 더 걸리는 구조다.

구현이 RFC 머지보다 먼저 나갔다: RFC(#3037)는 2026-05-21에 열려 2026-07-09에 머지됐고, 구현 PR(#3065)은 그 사이인 2026-06-02에 열려 2026-07-01에 먼저 머지됐다. RFC 리뷰가 진행되는 동안 이견이 적은 Part 1부터 코드로 확정한 것으로 보인다(확인 필요). RFC 자체가 최종 승인되기 전에 그 안의 일부가 이미 동작하는 코드로 존재했던 셈이다.

대안으로 검토됐다가 기각된 것: 리사이즈 중 컨솔리데이션 지연/차단: RFC의 대안 목록에는 리사이즈 이벤트 이후 consolidateAfter를 리셋하거나, 리사이즈가 진행 중인 노드를 컨솔리데이션 대상에서 아예 제외하는 안도 있었다. 더 단순하지만 핵심 문제(재생성 후 크기를 잘못 예측하는 것)를 해결하지 못하고, 목적지 노드의 용량 계산 자체도 고치지 못하며, 안전한 상황에서도 컨솔리데이션을 인위적으로 늦춘다는 이유로 기각됐다.

[변경 후] Ceiling()이 spec/actuated/allocated 중 최댓값을 본다
┌───────────────────────────────────────────────┐
│ Node-B (target, Allocatable = 8 vCPU)          │
│  Pod-Z: 리사이즈 다운 진행 중                     │
│    spec.requests             = 2 vCPU          │
│    status.resources.requests = (actuated)       │
│    status.allocatedResources = 6 vCPU           │
└───────────────────────────────────────────────┘
        │  UseStatusResources: true
        ▼
determineEffectiveRequests()
  = max(spec, actuated, allocated)   (Infeasible이면 spec 제외)
  = 6

Ceiling(Pod-Z).Requests = 6
  → RequestsForPods() → StateNode.podRequests
  → Available() = 8 - 6 = 2   (kube-scheduler와 동일)

Karpenter와 kube-scheduler가 같은 여유 용량을 보게 됨
  → 스케줄러가 거부할 이주는 애초에 컨솔리데이션 계획에서 배제

더 살펴볼 점

  • RFC Part 2(VPA 재생성 후 리소스 예측)는 CRD 직접 읽기 방식으로 먼저 들어갈 예정인데, 어느 v1.x 릴리스에서 실제로 구현될까?
  • Limits에는 UseStatusResources를 적용하지 않은 채로 남겨뒀는데, 리사이즈 도중 limit 기반 판단(예: 우선순위 기반 축출)이 필요한 경로가 실제로 존재할까?
  • CPU Startup Boost(리사이즈 업 이후 다시 축소되는 VPA 부가 기능)는 RFC에서 웹훅 로직을 그대로 복제해야 하는 부담으로 명시된 미해결 지점인데, Part 2 구현에서 이걸 어떻게 다룰까?

참고 자료

참고 외 별도 확인 링크

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