Skip to content

Latest commit

 

History

History
120 lines (88 loc) · 15.2 KB

File metadata and controls

120 lines (88 loc) · 15.2 KB

v1.14 — Karpenter Dynamic Resource Allocation: "노드가 아직 없는" 스케줄링 루프에서 GPU/디바이스를 배분하는 법

메타데이터

항목 내용
프로젝트 Karpenter (kubernetes-sigs/karpenter)
관련 릴리스 v1.14.0 (2026-07-11)
관련 PR #3082 — feat: dra allocator implementation (merged 2026-06-11), #3110 — feat: dra consumable capacity + partitionable devices support (merged 2026-06-24), #3113 — feat: dra allocator integration (merged 2026-06-27)
이전 상태를 만든 PR #2384 — fix: Don't schedule a pod with DRA requirements (merged 2025-09-02, v1.10.0)
설계 문서 designs/dra-scheduling.md (#3082에서 추가, v1.14.0 시점까지 내용 변경 없음)
발견 출처 v1.14.0 릴리스 노트 Features 항목("dra allocator implementation (#3082)", "dra allocator integration (#3113)", "dra consumable capacity + partitionable devices support (#3110)")
검증 근거 로컬 클론(sources/karpenter)의 실제 커밋 961af74b(#3082, 21개 파일 +9162), 11d5e0d4(#3113, scheduler.go/nodeclaim.go/initialization.go 등 34개 파일), 6a953204(#2384, IgnoreDRARequests 옵션 도입), 설계 문서 원문

개요

Kubernetes의 Dynamic Resource Allocation(DRA)은 파드가 GPU 같은 특수 디바이스를 ResourceClaim으로 요청하고, 클러스터에 실제로 존재하는 디바이스(ResourceSlice)와 매칭시켜 배분하는 표준 메커니즘이다. 문제는 Karpenter처럼 "아직 존재하지 않는 노드"를 미리 시뮬레이션해서 만들어야 하는 오토스케일러는, 업스트림 kube-scheduler의 DRA 할당 로직을 그대로 가져다 쓸 수 없다는 데 있다 — 업스트림은 항상 구체적인 노드 하나를 기준으로 디바이스를 배분하지만, Karpenter는 아직 어떤 인스턴스 타입이 뜰지 확정되지 않은 "여러 인스턴스 타입의 중첩 상태"를 놓고 배분 가능성을 판단해야 한다. v1.14.0은 이 문제를 위해 Karpenter 자체 DRA 할당기(pkg/scheduling/dynamicresources)를 새로 구현하고, 스케줄러/디스럽션/노드 초기화 컨트롤러 전체에 통합했다.

사전 지식

Kubernetes DRA: ResourceClaim / ResourceSlice / DeviceClass

Kubernetes DRA(Dynamic Resource Allocation)는 GPU 같은 디바이스를 resource.k8s.io API 그룹의 세 리소스로 모델링한다. DeviceClass는 "이런 종류의 디바이스"를 정의하는 클래스이고, 파드는 ResourceClaim(또는 템플릿)으로 "이 클래스의 디바이스를 N개 달라"고 요청한다. 실제로 클러스터에 존재하는 디바이스는 각 노드의 DRA 드라이버(DaemonSet 형태로 배포)가 ResourceSlice 오브젝트로 API 서버에 publish한다. 업스트림 kube-scheduler는 파드를 스케줄링할 때 그 파드의 ResourceClaim들을 이미 배치가 확정된 노드의 ResourceSlice 목록과 매칭시켜, 어떤 구체적인 디바이스를 예약할지 status.allocation에 기록한다.

Karpenter의 NodeClaim: "아직 없는 노드"의 중첩 상태

Karpenter의 프로비저닝 시뮬레이션은 업스트림 스케줄러와 근본적으로 다른 모델을 쓴다. 파드가 기존 노드 어디에도 못 들어가면, Karpenter는 그 파드를 놓을 새 NodeClaim을 만드는데, 이 시점에는 어떤 구체적인 인스턴스 타입이 실제로 뜰지 아직 정해지지 않았다 — NodeClaim은 "이 NodePool이 허용하는 후보 인스턴스 타입들 중 하나"라는 중첩(superposition) 상태로 존재한다. 이후 같은 스케줄링 루프에서 다른 파드들이 이 NodeClaim에 계속 추가되면서 요구사항(CPU/메모리/토폴로지 등)이 점점 좁혀지고, 후보 인스턴스 타입 집합도 그만큼 줄어든다. 마지막에 남은 후보 중 실제 인스턴스 타입 하나가 선택되면서 노드가 실제로 만들어진다. DRA 디바이스 배분도 이 모델 위에서 똑같이 동작해야 한다 — "이 GPU를 요청하는 파드가, 아직 어떤 인스턴스 타입이 뜰지 모르는 NodeClaim에 들어갈 수 있는가"를 판단해야 하는 것이다.

변경 분석

변경 전: DRA 파드는 스케줄링 대상에서 사실상 제외돼 있었다

[변경 전] Karpenter는 ResourceClaim을 이해하지 못했다
┌──────────────┐                                    ┌───────────────────┐
│ DRA 리소스를   │   IgnoreDRARequests=true(기본값)     │ 리소스 요구량 계산에  │
│ 요청하는 파드   │ ─────────────────────────────────▶│ ResourceClaim이     │
│ (GPU 등)      │   → 클레임 자체를 계산에서 빠뜨림        │ 아예 반영 안 됨       │
└──────────────┘                                    └───────────────────┘
       │
       │  IgnoreDRARequests=false
       ▼
┌──────────────────────┐
│ 영구적으로 스케줄 불가   │   ※ 디바이스 가용성/토폴로지 판단 로직 자체가 없음
│ (DRAError로 즉시 거부)  │   ※ "이 인스턴스 타입에 실제로 그 GPU가 있는가"를
└──────────────────────┘      전혀 검증하지 않은 채 노드를 만들거나, 아예 거부만 함

2025-09-02 merge된 #2384(v1.10.0)가 IgnoreDRARequests 옵션을 도입하기 전까지, Karpenter는 파드의 ResourceClaim을 스케줄링 계산에 전혀 반영하지 않았다. #2384 이후에도 이 옵션의 기본값은 true로, "DRA 리소스 요청은 무시하고 나머지 리소스만으로 배치를 계산한다"는 뜻이었다 — 실제로 그 인스턴스 타입에 필요한 디바이스가 있는지, 이미 다른 파드가 그 디바이스를 차지하고 있는지는 전혀 검증하지 않았다. 옵션을 꺼서 false로 두면 반대로 DRA 요청이 있는 파드는 DRAError로 즉시 영구 거부됐다 — 어느 쪽이든 "실제 디바이스 가용성을 보고 판단한다"는 오토스케일러 본연의 기능은 없었다.

변경 후: 업스트림 DRA 스케줄러를 시뮬레이션하는 전용 할당기

v1.14.0은 pkg/scheduling/dynamicresources 패키지에 Karpenter 전용 DRA 할당기를 새로 구현했다(#3082, +9162줄). 핵심 동작은 다음과 같다.

디바이스 풀 구성: 클러스터에 이미 publish된 ResourceSlice(in-cluster 디바이스)와, 클라우드 프로바이더가 인스턴스 타입 메타데이터로 제공하는 ResourceSliceTemplate(아직 뜨지 않은 인스턴스 타입이 launch되면 갖게 될 template 디바이스)을 함께 다룬다. 아직 Initialized 상태에 도달하지 않은 노드는 이미 publish한 slice가 있어도 무시하고 template 디바이스로만 취급한다 — 초기화 중인 노드의 실제 디바이스와 template 디바이스가 이중으로 잡히는 것을 막기 위해서다.

인스턴스 타입별 독립 DFS: NodeClaim의 각 후보 인스턴스 타입을 독립적으로 깊이 우선 탐색(DFS)한다. 탐색 순서는 "클레임 → 리퀘스트 → 디바이스 슬롯"이며, 각 슬롯에서 in-cluster 디바이스를 먼저 시도하고 실패하면 template 디바이스로 넘어간다. 한 인스턴스 타입에서 DFS가 실패하면 그 인스턴스 타입은 후보에서 제외된다. 여러 인스턴스 타입에 걸쳐 누적된 토폴로지 요구사항(zone 등)은 리셋되지 않고 계속 좁혀진다 — "인스턴스 타입 A는 zone-A에서만, B는 zone-B에서만"처럼 서로 양립 불가능한 시나리오를 자연스럽게 걸러내기 위해서다.

전역 디바이스 경합 추적: 여러 NodeClaim이 같은 in-cluster 디바이스를 동시에 요청할 수 있으므로, AllocationTracker가 "이 디바이스가 어느 NodeClaim/인스턴스 타입에 예약됐는지"를 추적한다. 같은 NodeClaim의 서로 다른 인스턴스 타입 후보끼리는 같은 디바이스를 각자 예약해도 되지만(어차피 하나만 실제로 뜨므로), 다른 NodeClaim이 예약한 디바이스는 즉시 배제된다.

노드 초기화 게이팅: DRA 드라이버가 실제로 ResourceSlice를 publish하기 전에는 그 디바이스를 실제로 쓸 수 없다. 그래서 노드 초기화 컨트롤러(pkg/controllers/nodeclaim/lifecycle/initialization.go)는 NodeClaim에 기록된 karpenter.sh/requested-dra-drivers 어노테이션의 모든 드라이버가 완전한 pool을 publish할 때까지 Initialized 조건을 Unknown(사유 DRADriverPoolsNotPublished)으로 묶어둔다.

이 전체 로직은 #3113(스케줄러 통합, 34개 파일)에서 scheduler.go/nodeclaim.go/existingnode.go/disruption 패키지 전반에 실제로 연결됐다 — 신규 NodeClaim 배치, 기존 노드 배치, consolidation/drift 재시뮬레이션까지 동일한 할당기를 공유한다.

설계 결정 및 트레이드오프

왜 업스트림 스케줄러 코드를 재사용하지 않고 새로 구현했는가: 설계 문서가 명시하는 문제는 두 가지다 — (1) NodeClaim은 여러 인스턴스 타입의 중첩 상태이므로 각 인스턴스 타입을 독립적으로 평가하고 불가능한 것을 쳐내야 하고, (2) 여러 NodeClaim이 같은 in-cluster 디바이스를 두고 경합할 수 있으므로 전역적으로는 NodeClaim 단위로, 그러나 한 NodeClaim 안에서는 인스턴스 타입 후보별로 조건부로 추적해야 한다. 업스트림 스케줄러는 이미 구체적인 노드 하나를 대상으로 동작하므로 이런 "아직 안 정해진 것들 중 하나"라는 개념 자체가 없다 — 그대로 재사용할 수 있는 코드가 아니었다.

초기 구현 범위를 의도적으로 좁혔다: 설계 문서의 "Scope Exclusions"는 admin access, 파티셔닝 가능 디바이스(공유 카운터), consumable capacity(allowMultipleAllocations), 디바이스 taint, NodeClaim 간 in-flight 디바이스 공유, "가장 덜 제약적인 해"를 찾는 multi-solution 최적화를 초기 구현(#3082) 범위에서 명시적으로 제외했다. DFS는 조건을 만족하는 첫 번째 해를 그대로 채택하며, 여러 인스턴스 타입 중 어느 것이 디바이스를 더 적게 낭비하는지 비교하지 않는다 — 최적해보다 "일단 동작하는 배치"를 먼저 내보내는 쪽을 택한 것이다. 다만 이 중 파티셔닝 가능 디바이스와 consumable capacity는 불과 2주 뒤 #3110에서 실제로 구현돼 같은 v1.14.0 릴리스에 함께 들어갔다 — 설계 문서 본문은 v1.14.0 시점까지 이 exclusion 목록을 갱신하지 않은 채로 남아 있어(확인 필요), "초기 최소 구현 → 빠르게 반복 확장"이라는 실제 개발 속도가 문서 갱신보다 앞서 있다.

5초 타임아웃으로 병리적 케이스를 강제 차단: DFS는 컨텍스트 5초 타임아웃으로 제한된다. 백트래킹을 동반한 전수 탐색이므로 클레임/디바이스 조합이 나쁘면 탐색 공간이 커질 수 있는데, 이 경우 스케줄링 루프 전체가 멈추는 대신 해당 탐색만 포기하고 넘어가는 안전장치다.

IgnoreDRARequests를 GA 전 임시 플래그로 명시: 코드 주석은 이 플래그를 "DRA 지원이 formal하게 GA되면 제거될 것"이라고 못박는다 — 즉 v1.14.0의 DRA 지원은 아직 정식 기능이 아니라 단계적으로 검증 중인 상태이고, 디폴트(true, DRA 요청 무시)를 유지한 채 명시적으로 옵트인해야 새 할당기가 동작한다.

[변경 후] 인스턴스 타입 중첩 상태에서 DRA 디바이스를 배분
┌────────────────┐
│ 후보 인스턴스 타입 │  각 인스턴스 타입마다 독립적으로
│  A, B, C, ...   │  Claim → Request → Device 슬롯 DFS
└───────┬────────┘
        ▼
┌────────────────────────────┐   in-cluster 디바이스 우선,
│ Pool(ResourceSlice, 실물)     │   부족하면 template(미실행) 디바이스로
│ + Template(ResourceSliceTpl) │──▶ 폴백
└────────────────────────────┘
        │  DFS 실패 시 그 인스턴스 타입 후보에서 제외
        │  성공한 인스턴스 타입들의 토폴로지 요구는 계속 누적
        ▼
┌───────────────────┐   Commit() 시 AllocationTracker에 전역 예약
│ AllocationResult   │──▶ (다른 NodeClaim은 즉시 배제,
│ (InstanceTypes,    │    같은 NodeClaim의 다른 IT 후보는 허용)
│  Requirements)     │
└─────────┬─────────┘
          ▼
┌───────────────────────────┐
│ 노드 초기화: requested-dra-  │  DRA 드라이버가 ResourceSlice
│ drivers 어노테이션의 모든 드   │  전체 pool을 publish할 때까지
│ 라이버가 pool 완성할 때까지    │  Initialized=Unknown으로 대기
│ Initialized 보류            │
└───────────────────────────┘

더 살펴볼 점

  • 설계 문서의 "Scope Exclusions"가 실제로는 v1.14.0 안에서 이미 일부(파티셔닝 가능 디바이스, consumable capacity) 뒤집혔는데, 문서 자체는 언제 갱신될까 — 아니면 "초기 구현 범위" 기록으로 의도적으로 남겨둔 것일까?
  • DFS가 "가장 덜 제약적인 해"가 아니라 첫 발견한 해를 그대로 쓰는 지금 방식은, 인스턴스 타입 선택 순서에 따라 디바이스를 더 낭비하는 노드가 뜨는 경우를 실제로 만들어낼까?
  • IgnoreDRARequests 플래그가 제거되는(=GA로 선언되는) 기준은 무엇이고, 언제쯤으로 예상할 수 있을까?

참고 자료

참고 외 별도 확인 링크

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