DRA가 동작하는 방식
DRA가 동작하는 방식 (How DRA Works)
이 페이지는 Kubernetes가 동적 리소스 할당(DRA)으로 워크로드에 디바이스를 할당하는 방법과, 사전 스케줄링된 파드가 그 과정과 상호작용하는 방식을 설명해요.
출처: 문서
본문
DRA로 리소스 할당이 동작하는 방식 (How resource allocation with DRA works)
다음 섹션은 다양한 DRA 사용자 유형과 동적 리소스 할당 중에 Kubernetes 시스템을 위한 워크플로를 설명해요.
사용자 워크플로 (Workflow for users)
- 드라이버 생성 (Driver creation): 디바이스 소유자나 서드파티 엔티티가 클러스터에서 ResourceSlice를 만들고 관리할 수 있는 드라이버를 만들어요. 이 드라이버는 선택적으로 디바이스 범주와 그것을 요청하는 방법을 정의하는 DeviceClass도 만들어요.
- 클러스터 구성 (Cluster configuration): 클러스터 관리자가 클러스터를 만들고, 디바이스를 노드에 연결하고, DRA 디바이스 드라이버를 설치해요. 클러스터 관리자는 선택적으로 디바이스 범주와 그것을 요청하는 방법을 정의하는 DeviceClass를 만들어요.
- 리소스 청구 (Resource claims): 워크로드 운영자가 DeviceClass 안에서 특정 디바이스 구성을 요청하는 ResourceClaimTemplate 또는 ResourceClaim을 만들어요. 같은 단계에서 워크로드 운영자는 Kubernetes 매니페스트를 수정해 그 ResourceClaimTemplate 또는 ResourceClaim을 요청해요.
Kubernetes 워크플로 (Workflow for Kubernetes)
- ResourceSlice 생성 (ResourceSlice creation): 클러스터의 드라이버가 유사한 디바이스의 관리 풀에서 하나 이상의 디바이스를 나타내는 ResourceSlice를 만들어요.
- 워크로드 생성 (Workload creation): 클러스터 컨트롤 플레인이 새 워크로드에서 ResourceClaimTemplate 또는 특정 ResourceClaim에 대한 참조를 확인해요. 워크로드가 ResourceClaimTemplate을 사용하면
resourceclaim-controller라는 컨트롤러가 워크로드에 대한 ResourceClaim을 생성해요. 워크로드가 특정 ResourceClaim을 사용하면 Kubernetes는 그 ResourceClaim이 클러스터에 존재하는지 확인해요. ResourceClaim이 존재하지 않으면 파드는 배포되지 않아요. - ResourceSlice 필터링 (ResourceSlice filtering): 모든 파드에 대해 Kubernetes는 클러스터의 ResourceSlice를 확인해 다음 기준을 모두 충족하는 디바이스를 찾아요: 리소스에 접근할 수 있는 노드가 파드를 실행할 자격이 있어야 하고, ResourceSlice에 파드의 ResourceClaim 요구 사항과 일치하는 할당되지 않은 리소스가 있어야 해요.
- 리소스 할당 (Resource allocation): 파드의 ResourceClaim에 대한 적격 ResourceSlice를 찾은 후 Kubernetes 스케줄러가 할당 세부 정보로 ResourceClaim을 업데이트해요. 스케줄러는 first-fit 전략을 사용하고 이름의 사전식 순서로 풀과 ResourceSlice를 평가해요. 드라이버는 적절하게 이름을 지어 특정 슬라이스나 풀에 우선순위를 둘 수 있어요. 자세한 내용은 명명과 우선순위를 참고해요.
- 파드 스케줄링 (Pod scheduling): 리소스 할당이 완료되면 스케줄러는 할당된 리소스에 접근할 수 있는 노드에 파드를 배치해요. 드라이버가 노드 로컬 준비나 정리가 필요 없는 디바이스에 대해 선택적 노드 작업을 선언하지 않는 한, 해당 노드의 디바이스 드라이버와 kubelet이 gRPC로 조정해 디바이스와 파드의 디바이스 접근을 구성해요.
사전 스케줄링된 파드 (Pre-scheduled Pods)
spec.nodeName이 이미 설정된 파드를 만들면(여러분 또는 다른 API 클라이언트가) 스케줄러가 우회돼요. 그 파드가 필요로 하는 ResourceClaim이 아직 존재하지 않거나, 할당되지 않았거나, 파드를 위해 예약되지 않았다면, kubelet은 파드 실행에 실패하고 주기적으로 다시 확인할 거예요. 그 요구 사항은 나중에 여전히 충족될 수 있기 때문이에요.
이런 상황은 파드가 스케줄링될 때 스케줄러에서 동적 리소스 할당 지원이 활성화되지 않았을 때(버전 스큐, 구성, 기능 게이트 등)도 발생할 수 있어요. kube-controller-manager가 이것을 감지하고 필요한 ResourceClaim을 예약해 파드를 실행 가능하게 만들려고 시도해요. 하지만 이것은 그것들이 스케줄러에 의해 다른 어떤 파드를 위해 할당된 경우에만 동작해요.
노드에 할당된 파드는 막혀 있는 동안 정상 리소스(RAM, CPU)를 차단하고 그 리소스를 다른 파드에 사용할 수 없게 하므로 스케줄러를 우회하는 것을 피하는 것이 좋아요. 정상 스케줄링 흐름을 거치면서도 파드를 특정 노드에서 실행하려면 원하는 노드와 정확히 일치하는 노드 셀렉터로 파드를 만들어요:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-cats
spec:
nodeSelector:
kubernetes.io/hostname: name-of-the-intended-node
...
입력되는 파드를 admission 시간에 변형해 .spec.nodeName 필드를 해제하고 대신 노드 셀렉터를 사용할 수도 있어요.
디바이스 바인딩 조건 (Device binding conditions)
디바이스 바인딩 조건은 Kubernetes 스케줄러가 패브릭 연결 GPU나 재프로그래밍 가능한 FPGA 같은 외부 리소스가 준비되었음을 확인할 때까지 파드 바인딩을 지연시키게 해줘요.
이 대기 동작은 스케줄링 프레임워크의 PreBind 단계에서 구현돼요. 이 단계 동안 스케줄러는 바인딩을 진행하기 전에 필요한 모든 디바이스 조건이 충족되었는지 확인해요.
이것은 조기 바인딩을 피함으로써 스케줄링 신뢰성을 향상시키고 외부 디바이스 컨트롤러와의 조정을 가능하게 해요.
이 기능을 사용하려면 디바이스 드라이버(일반적으로 드라이버 소유자가 관리)가 ResourceSlice의 Device 섹션에 다음 필드를 게시해야 해요. 클러스터 관리자는 스케줄러가 이 필드를 존중하도록 DRADeviceBindingConditions와 DRAResourceClaimDeviceStatus 기능 게이트를 활성화해야 해요.
- bindingConditions: 파드가 바인딩되기 전에 (연관된 ResourceClaim의
.status.conditions필드에서) True로 설정되어야 하는 조건 유형의 목록. 이 조건들은 일반적으로 DeviceAttached나 DeviceInitialized 같은 준비 신호를 나타내요. - bindingFailureConditions: 연관된 ResourceClaim의 status.conditions 필드에서 True로 설정되면 실패 상태를 나타내는 조건 유형의 목록. 이 조건 중 하나라도 True이면 스케줄러는 바인딩을 중단하고 파드를 다시 스케줄링해요.
- bindsToNode:
true로 설정되면 스케줄러가 선택된 노드 이름을 ResourceClaim의status.allocation.nodeSelector필드에 기록해요. 이것은 파드의spec.nodeSelector에 영향을 주지 않아요. 대신 ResourceClaim 내부에 외부 컨트롤러가 디바이스 연결이나 준비 같은 노드별 작업을 수행하는 데 사용할 수 있는 노드 셀렉터를 설정해요.
bindingConditions와 bindingFailureConditions에 나열된 모든 조건 유형은 ResourceClaim의 status.conditions 필드에서 평가돼요. 외부 컨트롤러는 표준 Kubernetes 조건 의미론(type, status, reason, message, lastTransitionTime)을 사용해 이 조건들을 업데이트할 책임이 있어요.
스케줄러는 모든 bindingConditions가 True가 될 때까지 최대 600초(기본값)를 기다려요. 제한 시간에 도달하거나 bindingFailureConditions 중 하나가 True이면 스케줄러는 할당을 지우고 파드를 다시 스케줄링해요. 클러스터 관리자는 kube-scheduler 구성 파일을 편집해 이 제한 시간을 구성할 수 있어요.
KubeSchedulerConfiguration에서 이 제한 시간을 구성하는 예시는 아래와 같아요:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: DynamicResources
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: DynamicResourcesArgs
bindingTimeout: 60s
예시 (Example)
여기 DRA 드라이버가 사용 중이고 그 드라이버가 바인딩 조건을 지원하는 클러스터에서 볼 수 있는 ResourceSlice의 예시가 있어요:
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
name: gpu-slice-1
spec:
driver: dra.example.com
nodeSelector:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator-type
operator: In
values:
- "high-performance"
pool:
name: gpu-pool
generation: 1
resourceSliceCount: 1
devices:
- name: gpu-1
attributes:
vendor:
string: "example"
model:
string: "example-gpu"
bindsToNode: true
bindingConditions:
- dra.example.com/is-prepared
bindingFailureConditions:
- dra.example.com/preparing-failed
이 예시 ResourceSlice는 다음 속성을 가져요:
- 이 ResourceSlice는
accelerator-type=high-performance로 레이블된 노드를 대상으로 하므로 스케줄러는 특정 적격 노드 집합만 사용해요. - 스케줄러는 선택된 그룹에서 하나의 노드(예: node-3)를 선택하고
status.allocation.nodeSelector필드를 그 노드 이름으로 설정해요. dra.example.com/is-prepared바인딩 조건은 바인딩 전에 디바이스 gpu-1이 준비되어야 함(is-prepared 조건의 status가 True여야 함)을 나타내요.- gpu-1 디바이스 준비가 실패하면(preparing-failed 조건의 status가 True) 스케줄러는 바인딩을 중단해요.
- 스케줄러는 디바이스가 준비될 때까지 최대 600초(기본값)를 기다려요.
- 외부 컨트롤러는 ResourceClaim의 노드 셀렉터를 사용해 선택된 노드에서 노드별 설정을 수행할 수 있어요.
디바이스 바인딩 조건은 kube-apiserver와 kube-scheduler의 DRADeviceBindingConditions 기능 게이트로 제어돼요.
노드 할당 가능 리소스 (Node allocatable resources)
DRA로 관리되는 디바이스는 cpu, memory, hugepages 같은 노드 할당 가능 리소스로 구성된 기본 풋프린트를 가질 수 있어요. 이 기능은 이러한 DRA 기반 요청을 이러한 리소스에 대한 정규 파드 spec 요청과 함께 스케줄러의 표준 회계에 통합해요.
DRA 드라이버는 두 가지 별개의 모델을 사용해 디바이스가 노드 할당 가능 리소스를 소비하는 방식을 정의해요:
- 직접 리소스 매핑 (Direct Resource Mapping, mapping): DRA 디바이스가 표준 노드 리소스(커스텀 CPU 코어 풀이나 메모리 블록 같은 것)를 직접 제공해요. 클레임 할당이 노드의 표준 CPU나 메모리 용량에 직접 매핑돼요.
- 보조 디바이스 오버헤드 (Auxiliary Device Overhead, overhead): DRA 디바이스(GPU나 가속기 같은 것)가 파드나 컨테이너에 할당될 때 작동하려면 보조 오버헤드로 호스트 리소스(호스트 RAM 같은 것)를 요구해요.
파드 작성자를 위한 고려 사항 (Considerations for Pod Authors)
이 유형의 디바이스에 대한 클레임을 사용해 PodSpec을 작성할 때 알아야 할 몇 가지 사항이 있어요:
- 파드 수준 리소스를 사용할 때 스케줄러는 그것을 컨테이너 요청과 한도 모두에 대해 엄격히 검증해요: 모든 컨테이너 요청과 DRA 클레임 리소스의 합이 파드 수준 요청을 초과해서는 안 되며, 그렇지 않으면 파드 스케줄링이 실패해요. 각 개별 컨테이너의 한도에 그 DRA 할당을 더한 값이 파드 수준 한도를 초과해서는 안 되며, 그렇지 않으면 파드 스케줄링이 실패해요.
- 컨테이너의 총 리소스 요구 사항은 그 컨테이너 수준 리소스와 연관된 리소스 클레임의 노드 할당 가능 리소스의 합이에요.
- 클레임 공유 제한 (Claim Sharing Restriction): 직접 리소스 매핑(mapping)을 사용하는 클레임은 여러 파드에 걸쳐 공유될 수 없어요. 오버헤드가 있는 디바이스에 대한 클레임은 디바이스 공유를 지원할 수 있고 오버헤드는 파드별 또는 컨테이너별로 추적돼요.
- DRA 클레임이 있는 파드는 spec에서 표준 요청의 인플레이스 리사이징을 지원해요. 스케줄러는 리사이징된 표준 요청이 정적 DRA 할당과 결합되어도 여전히 노드에 맞는지 보장해요.
DRA 드라이버 작성자를 위한 세부 사항 (Details for DRA Driver Authors)
DRA 드라이버는 ResourceSlice 내 디바이스의 nodeAllocatableResources 필드를 사용해 이 노드 할당 가능 리소스 풋프린트를 선언해요. 이것은 요청된 DRA 디바이스 또는 용량을 노드의 status.allocatable에서 추적되는 표준 리소스로 변환하는 것을 정의해요(확장 리소스는 이 필드에 대해 지원되지 않음에 주의해요). 이것은 네이티브 리소스(CPU나 메모리 DRA 드라이버 같은 것)를 직접 노출하는 드라이버와 보조 노드 종속성(호스트 메모리가 필요한 가속기 같은 것)을 요구하는 디바이스 모두에 유용해요.
nodeAllocatableResources 필드는 두 가지 다른 사용 사례를 지원해요:
- 매핑 (Mapping): DRA 디바이스가 표준 리소스를 직접 나타낼 때 사용돼요(예: CPU나 메모리 DRA 드라이버). 스케줄러는
capacityMultiplier로 용량을 확장하거나deviceMultiplier로 디바이스 수를 확장하여 정확한 수량을 계산해요. - 오버헤드 (Overhead): 디바이스가 보조 노드 종속성(GPU가 소비하는 호스트 메모리 같은 것)을 요구할 때 사용돼요. 이것은 참조하는 컨테이너 수에 따라 선형적으로 확장되는 고정 perPod 비용 또는 가변 perContainer 비용으로 정의될 수 있어요.
예시: CPU DRA 드라이버 (매핑)
여기 CPU DRA 드라이버가 DRA 소비 가능 용량을 사용해 CPU 소켓을 128개 CPU의 풀로 노출하는 예시가 있어요. capacityKey는 소비된 cpu.example.com/cpu 용량을 노드의 표준 cpu 할당 가능 리소스에 직접 연결해요:
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
name: my-node-cpus
spec:
driver: cpu.example.com
nodeName: my-node
pool:
name: socket-cpus
generation: 1
resourceSliceCount: 1
devices:
- name: socket0cpus
allowMultipleAllocations: true
capacity:
"cpu.example.com/cpu": "128"
nodeAllocatableResources:
mapping:
cpu:
capacityKey: "cpu.example.com/cpu"
- name: socket1cpus
allowMultipleAllocations: true
capacity:
"cpu.example.com/cpu": "128"
nodeAllocatableResources:
mapping:
cpu:
capacityKey: "cpu.example.com/cpu"
capacityMultiplier: 1
예시: 보조 리소스가 있는 가속기 (오버헤드)
여기 가속기가 작동하기 위해 파드당 추가 8Gi 메모리를 요구하는 리소스 슬라이스의 예시가 있어요:
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
name: my-node-xpus
spec:
driver: xpu.example.com
nodeName: my-node
pool:
name: xpu-pool
generation: 1
resourceSliceCount: 1
devices:
- name: xpu-model-x-001
attributes:
example.com/model:
string: "model-x"
nodeAllocatableResources:
overhead:
memory:
perPod: "8Gi"
파드가 노드에 성공적으로 바인딩된 후 DRA를 통해 할당된 노드 할당 가능 리소스의 정확한 수량은 kube-scheduler에 의해 집계되어 파드의 status.nodeAllocatableResourceClaimStatuses 필드에 직접 포함돼요. 이것은 스케줄러에서 kubelet로의 명확하고 영속적인 핸드오프를 제공해요.
결정적으로, kubelet은 이 API를 네이티브로 소비해 시스템 수준 경계를 완벽하게 정렬해요:
- cgroups: 파드와 컨테이너 cgroup은 이제 DRA 기반 할당을 포함해 커널에 의해 워크로드가 인위적으로 제한되는 것을 방지해요.
- OOM 점수 (OOM Scores): kubelet은 컨테이너의 DRA 메모리 요청을 그 유효 메모리 요청에 반영해요.
노드 할당 가능 리소스는 알파 기능이며 kube-apiserver, kube-scheduler, kubelet에서 DRANodeAllocatableResources 기능 게이트가 활성화되면 켜져요.