장치 플러그인

장치 플러그인 (Device Plugins)

쿠버네티스는 시스템 하드웨어 리소스를 kubelet에 알리기(advertise) 위해 사용할 수 있는 장치 플러그인 프레임워크를 제공합니다.

셀러(vendors)는 쿠버네티스 자체의 코드를 사용자 정의하는 대신, 수동으로 또는 DaemonSet으로 배포하는 장치 플러그인을 구현할 수 있습니다. 대상 장치에는 GPU, 고성능 NIC, FPGA, InfiniBand 어댑터, 그리고 공급업체별 초기화와 설정이 필요한 유사한 컴퓨팅 리소스가 포함됩니다.

출처: 문서

본문

장치 플러그인 등록

kubelet은 Registration gRPC 서비스를 노출합니다:

service Registration {
	rpc Register(RegisterRequest) returns (Empty) {}
}

장치 플러그인은 이 gRPC 서비스를 통해 kubelet에 스스로를 등록할 수 있습니다. 등록 중에 장치 플러그인은 다음을 보내야 합니다:

  • Unix 소켓의 이름.
  • 빌드된 Device Plugin API 버전.
  • 알리고 싶은 ResourceName. 여기서 ResourceName확장 리소스 명명 규칙을 따라 vendor-domain/resourcetype이어야 합니다. (예: NVIDIA GPU는 nvidia.com/gpu로 알립니다.)

성공적인 등록 후 장치 플러그인은 관리하는 장치 목록을 kubelet에 보내고, kubelet은 kubelet 노드 상태 업데이트의 일부로 그 리소스를 API 서버에 알리는 역할을 맡습니다. 예를 들어 장치 플러그인이 hardware-vendor.example/foo를 kubelet에 등록하고 노드에 건강한 장치 두 개를 보고하면, 노드 상태는 노드에 "Foo" 장치가 2개 설치되어 사용 가능함을 알리도록 업데이트됩니다.

그런 다음 사용자는 파드 명세의 일부로 장치를 요청할 수 있습니다(container 참조). 확장 리소스 요청은 다른 리소스의 request와 limit을 관리하는 방식과 유사하지만 다음과 같은 차이가 있습니다:

  • 확장 리소스는 정수 리소스로만 지원되며 초과 할당(overcommit)될 수 없습니다.
  • 장치는 컨테이너 간에 공유될 수 없습니다.

예시 {#example-pod}

어떤 노드에서 hardware-vendor.example/foo 리소스를 알리는 장치 플러그인을 실행하는 쿠버네티스 클러스터가 있다고 가정해 보겠습니다. 다음은 이 리소스를 요청해 데모 워크로드를 실행하는 파드의 예시입니다:

---
apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
spec:
  containers:
    - name: demo-container-1
      image: registry.k8s.io/pause:3.8
      resources:
        limits:
          hardware-vendor.example/foo: 2
#
# This Pod needs 2 of the hardware-vendor.example/foo devices
# and can only schedule onto a Node that's able to satisfy
# that need.
#
# If the Node has more than 2 of those devices available, the
# remainder would be available for other Pods to use.

장치 플러그인 구현

장치 플러그인의 일반적인 워크플로에는 다음 단계가 포함됩니다:

  1. 초기화. 이 단계에서 장치 플러그인은 장치가 준비 상태임을 확인하기 위해 공급업체별 초기화와 설정을 수행합니다.

  2. 플러그인은 호스트 경로 /var/lib/kubelet/device-plugins/ 아래의 Unix 소켓으로 gRPC 서비스를 시작하며(이 경로는 하드코딩되어 있고 kubelet의 --root-dir나 다른 구성의 영향을 받지 않음), 다음 인터페이스를 구현합니다:

    service DevicePlugin {
          // GetDevicePluginOptions returns options to be communicated with Device Manager.
          rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}
    
          // ListAndWatch returns a stream of List of Devices
          // Whenever a Device state change or a Device disappears, ListAndWatch
          // returns the new list
          rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
    
          // Allocate is called during container creation so that the Device
          // Plugin can run device specific operations and instruct Kubelet
          // of the steps to make the Device available in the container
          rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
    
          // GetPreferredAllocation returns a preferred set of devices to allocate
          // from a list of available ones. The resulting preferred allocation is not
          // guaranteed to be the allocation ultimately performed by the
          // devicemanager. It is only designed to help the devicemanager make a more
          // informed allocation decision when possible.
          rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}
    
          // PreStartContainer is called, if indicated by Device Plugin during registration phase,
          // before each container start. Device plugin can run device specific operations
          // such as resetting the device before making devices available to the container.
          rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}
    }
    

    플러그인은 GetPreferredAllocation() 또는 PreStartContainer()에 유용한 구현을 제공할 필요는 없습니다. 이러한 호출의 가용성을 나타내는 플래그(있는 경우)는 GetDevicePluginOptions() 호출이 다시 보내는 DevicePluginOptions 메시지에 설정되어야 합니다. kubelet은 선택적 함수를 직접 호출하기 전에 항상 GetDevicePluginOptions()을 호출해 어떤 선택적 함수를 사용할 수 있는지 확인합니다.

  3. 플러그인은 호스트 경로 /var/lib/kubelet/device-plugins/kubelet.sock의 Unix 소켓을 통해 kubelet에 스스로를 등록합니다.

    워크플로의 순서가 중요합니다. 플러그인은 성공적인 등록을 위해 kubelet에 스스로를 등록하기 전에 gRPC 서비스를 제공하기 시작해야 합니다.

  4. 성공적으로 스스로를 등록한 후 장치 플러그인은 서빙 모드로 실행되며, 그 동안 장치 상태를 계속 모니터링하고 어떤 장치 상태 변화가 있으면 kubelet에 보고합니다. 또한 Allocate gRPC 요청을 제공할 책임이 있습니다. Allocate 동안 장치 플러그인은 장치 특정 준비를 수행할 수 있습니다. 예를 들어 GPU 정리 또는 QRNG 초기화입니다. 작업이 성공하면 장치 플러그인은 할당된 장치에 접근하기 위한 컨테이너 런타임 구성을 포함하는 AllocateResponse를 반환합니다. kubelet은 이 정보를 컨테이너 런타임에 전달합니다.

    AllocateResponse는 0개 이상의 ContainerAllocateResponse 객체를 포함합니다. 여기서 장치 플러그인은 장치에 대한 접근을 제공하기 위해 컨테이너 정의에 이루어져야 할 수정 사항을 정의합니다. 이러한 수정에는 다음이 포함됩니다:

    • 어노테이션
    • 장치 노드
    • 환경 변수
    • 마운트
    • 정규화된(fully-qualified) CDI 장치 이름

    Device Manager의 정규화된 CDI 장치 이름 처리는 kubelet과 kube-apiserver 모두에 대해 DevicePluginCDIDevices 기능 게이트가 활성화되어 있어야 합니다. 이는 쿠버네티스 v1.28에서 alpha 기능으로 추가되었고, v1.29에서 beta로, v1.31에서 GA로 승격되었습니다.

kubelet 재시작 처리

장치 플러그인은 kubelet 재시작을 감지하고 새 kubelet 인스턴스에 스스로를 다시 등록할 것으로 기대됩니다. 새 kubelet 인스턴스는 시작할 때 /var/lib/kubelet/device-plugins(장치 플러그인의 하드코딩된 경로) 아래의 모든 기존 Unix 소켓을 삭제합니다. 장치 플러그인은 Unix 소켓의 삭제를 모니터링하고 그런 이벤트가 발생하면 스스로를 다시 등록할 수 있습니다.

장치 플러그인과 건강하지 않은 장치

장치가 실패하거나 종료되는 경우가 있습니다. 이 경우 Device Plugin의 책임은 ListAndWatchResponse API를 사용해 kubelet에 상황을 알리는 것입니다.

장치가 건강하지 않은 것으로 표시되면 kubelet은 새 파드를 스케줄링하는 데 사용할 수 있는 장치 수를 반영하도록 Node의 이 리소스에 대한 allocatable 수를 줄입니다. 리소스의 capacity 수는 변하지 않습니다.

실패한 장치에 할당되었던 파드는 계속 그 장치에 할당됩니다. 장치에 의존하는 코드가 실패하기 시작하는 것이 일반적이며, 파드의 restartPolicyAlways가 아니면 파드가 Failed 상태에 들어가거나, 그렇지 않으면 크래시 루프에 들어갈 수 있습니다.

쿠버네티스 v1.31 이전에는 파드가 실패한 장치와 연관되어 있는지 여부를 알려면 PodResources API를 사용하는 것이 방법이었습니다.

ResourceHealthStatus 기능 게이트가 활성화되면(beta이며 v1.36부터 기본 활성화), allocatedResourcesStatus 필드가 각 파드의 .status 내 각 컨테이너 상태에 추가됩니다. allocatedResourcesStatus 필드는 컨테이너에 할당된 각 장치에 대한 건강 정보를 보고합니다. 각 리소스 건강 항목은 오류 세부 정보나 실패 이유 같은 건강 상태에 대한 추가 사람이 읽을 수 있는 컨텍스트를 포함할 수 있는 선택적 message 필드를 포함할 수 있습니다.

실패한 파드에 대해, 또는 결함이 의심되는 경우 이 상태를 사용해 파드 동작이 장치 실패와 연관될 수 있는지 이해할 수 있습니다. 예를 들어 가속기가 과열 이벤트를 보고하면 allocatedResourcesStatus 필드가 이를 보고할 수 있습니다.

장치 플러그인 배포

장치 플러그인을 DaemonSet, 노드 운영 체제용 패키지, 또는 수동으로 배포할 수 있습니다.

정규 디렉터리 /var/lib/kubelet/device-plugins(kubelet에 하드코딩됨)는 특권 접근이 필요하므로, 장치 플러그인은 특권 보안 컨텍스트에서 실행되어야 합니다. 장치 플러그인을 DaemonSet으로 배포한다면 /var/lib/kubelet/device-plugins이 플러그인의 PodSpec에서 볼륨으로 마운트되어야 합니다.

DaemonSet 방식을 선택하면 쿠버네티스에 다음을 의존할 수 있습니다: 장치 플러그인의 파드를 노드에 배치하기, 실패 후 데몬 파드를 재시작하기, 업그레이드 자동화를 돕기.

API 호환성

이전에는 버전 지정 체계가 Device Plugin의 API 버전이 Kubelet의 버전과 정확히 일치할 것을 요구했습니다. v1.12에서 이 기능이 Beta로 승격된 이후에는 더 이상 하드 요구 사항이 아닙니다. API는 버전이 지정되고 이 기능이 Beta로 승격된 이후로 안정적이었습니다. 이 때문에 kubelet 업그레이드는 매끄러워야 하지만, 안정화 전에 API에 변경이 있을 수 있어 업그레이드가 비파괴적임이 보장되지는 않습니다.

쿠버네티스의 Device Manager 구성 요소는 일반적으로 사용 가능한 기능이지만 장치 플러그인 API 는 안정적이지 않습니다. 장치 플러그인 API와 버전 호환성에 대한 정보는 Device Plugin API versions을 읽어보세요.

프로젝트로서 쿠버네티스는 장치 플러그인 개발자에게 다음을 권장합니다:

  • 향후 릴리스의 Device Plugin API 변경을 주시하라.
  • 역/전방 호환성을 위해 장치 플러그인 API의 여러 버전을 지원하라.

더 새로운 장치 플러그인 API 버전의 쿠버네티스 릴리스로 업그레이드해야 하는 노드에서 장치 플러그인을 실행하려면, 그 노드를 업그레이드하기 전에 두 버전 모두를 지원하도록 장치 플러그인을 업그레이드하세요. 이 접근 방식은 업그레이드 중 장치 할당이 계속 동작하도록 보장합니다.

장치 플러그인 리소스 모니터링

장치 플러그인이 제공하는 리소스를 모니터링하기 위해, 모니터링 에이전트는 노드에서 사용 중인 장치 집합을 발견하고, 메트릭이 어떤 컨테이너와 연관되어야 하는지 설명하는 메타데이터를 얻을 수 있어야 합니다. 장치 모니터링 에이전트가 노출하는 Prometheus 메트릭은 Kubernetes Instrumentation Guidelines을 따라야 하며, pod, namespace, container Prometheus 라벨을 사용해 컨테이너를 식별해야 합니다.

kubelet은 사용 중인 장치 발견을 가능하게 하고 이러한 장치에 대한 메타데이터를 제공하는 gRPC 서비스를 제공합니다:

// PodResourcesLister is a service provided by the kubelet that provides information about the
// node resources consumed by pods and containers on the node
service PodResourcesLister {
    rpc List(ListPodResourcesRequest) returns (ListPodResourcesResponse) {}
    rpc GetAllocatableResources(AllocatableResourcesRequest) returns (AllocatableResourcesResponse) {}
    rpc Get(GetPodResourcesRequest) returns (GetPodResourcesResponse) {}
}

List gRPC 엔드포인트 {#grpc-endpoint-list}

List 엔드포인트는 실행 중인 파드의 리소스에 대한 정보를 제공합니다. 여기에는 전용으로 할당된 CPU의 id, 장치 플러그인이 보고한 장치 id, 그 장치들이 할당된 NUMA 노드의 id 같은 세부 정보가 포함됩니다. 또한 NUMA 기반 머신의 경우 컨테이너에 예약된 메모리와 거대 페이지에 대한 정보를 포함합니다.

쿠버네티스 v1.27부터 List 엔드포인트는 DynamicResourceAllocation API가 ResourceClaim에 할당한 실행 중인 파드의 리소스에 대한 정보를 제공할 수 있습니다. 쿠버네티스 v1.34부터 이 기능은 기본 활성화됩니다.

// ListPodResourcesResponse is the response returned by List function
message ListPodResourcesResponse {
    repeated PodResources pod_resources = 1;
}

// PodResources contains information about the node resources assigned to a pod
message PodResources {
    string name = 1;
    string namespace = 2;
    repeated ContainerResources containers = 3;
}

// ContainerResources contains information about the resources assigned to a container
message ContainerResources {
    string name = 1;
    repeated ContainerDevices devices = 2;
    repeated int64 cpu_ids = 3;
    repeated ContainerMemory memory = 4;
    repeated DynamicResource dynamic_resources = 5;
}

// ContainerMemory contains information about memory and hugepages assigned to a container
message ContainerMemory {
    string memory_type = 1;
    uint64 size = 2;
    TopologyInfo topology = 3;
}

// Topology describes hardware topology of the resource
message TopologyInfo {
        repeated NUMANode nodes = 1;
}

// NUMA representation of NUMA node
message NUMANode {
        int64 ID = 1;
}

// ContainerDevices contains information about the devices assigned to a container
message ContainerDevices {
    string resource_name = 1;
    repeated string device_ids = 2;
    TopologyInfo topology = 3;
}

// DynamicResource contains information about the devices assigned to a container by Dynamic Resource Allocation
message DynamicResource {
    string class_name = 1;
    string claim_name = 2;
    string claim_namespace = 3;
    repeated ClaimResource claim_resources = 4;
}

// ClaimResource contains per-plugin resource information
message ClaimResource {
    repeated CDIDevice cdi_devices = 1 [(gogoproto.customname) = "CDIDevices"];
}

// CDIDevice specifies a CDI device information
message CDIDevice {
    // Fully qualified CDI device name
    // for example: vendor.com/gpu=gpudevice1
    // see more details in the CDI specification:
    // https://github.com/container-orchestrated-devices/container-device-interface/blob/main/SPEC.md
    string name = 1;
}

List 엔드포인트의 ContainerResourcescpu_ids는 특정 컨테이너에 할당된 전용 CPU에 해당합니다. 목표가 공유 풀에 속한 CPU를 평가하는 것이라면, List 엔드포인트를 아래 설명된 대로 GetAllocatableResources 엔드포인트와 함께 사용해야 합니다:

  1. GetAllocatableResources를 호출해 모든 allocatable CPU 목록을 얻는다
  2. 시스템의 모든 ContainerResources에서 GetCpuIds를 호출한다
  3. GetAllocatableResources 호출에서 GetCpuIds 호출의 모든 CPU를 빼낸다

GetAllocatableResources gRPC 엔드포인트 {#grpc-endpoint-getallocatableresources}

GetAllocatableResources는 워커 노드에서 처음에 사용 가능한 리소스에 대한 정보를 제공합니다. kubelet이 APIServer로 내보내는 것보다 더 많은 정보를 제공합니다.

GetAllocatableResources는 노드의 allocatable 리소스를 평가하는 데만 사용해야 합니다. 목표가 여유/미할당 리소스를 평가하는 것이라면 List() 엔드포인트와 함께 사용해야 합니다. GetAllocatableResources로 얻은 결과는 kubelet에 노출된 기반 리소스가 바뀌지 않는 한 동일하게 유지됩니다. 이런 일은 드물게 발생하지만 발생할 때(예: hotplug/hotunplug, 장치 건강 변화) 클라이언트는 GetAllocatableResources 엔드포인트를 호출할 것으로 기대됩니다.

그러나 CPU 및/또는 메모리 업데이트의 경우 GetAllocatableResources 엔드포인트를 호출하는 것만으로는 충분하지 않으며, 올바른 리소스 capacity와 allocatable을 반영하기 위해 Kubelet을 재시작해야 합니다.

// AllocatableResourcesResponses contains information about all the devices known by the kubelet
message AllocatableResourcesResponse {
    repeated ContainerDevices devices = 1;
    repeated int64 cpu_ids = 2;
    repeated ContainerMemory memory = 3;
}

ContainerDevices는 장치가 어떤 NUMA 셀에 친화적인지를 선언하는 토폴로지 정보를 노출합니다. NUMA 셀은 불투명한 정수 ID로 식별되며, 그 값은 장치 플러그인이 kubelet에 등록될 때 보고하는 것과 일치합니다.

gRPC 서비스는 kubelet의 루트 디렉터리(보통 /var/lib/kubelet/pod-resources/kubelet.sock) 안의 pod-resources/kubelet.sock Unix 소켓을 통해 제공됩니다. 장치 플러그인 리소스용 모니터링 에이전트는 데몬으로 또는 DaemonSet으로 배포할 수 있습니다. kubelet 루트 디렉터리(보통 /var/lib/kubelet/pod-resources) 안의 정규 디렉터리 pod-resources는 특권 접근이 필요하므로, 모니터링 에이전트는 특권 보안 컨텍스트에서 실행되어야 합니다. 장치 모니터링 에이전트가 DaemonSet으로 실행되면 pod-resources 디렉터리가 장치 모니터링 에이전트의 PodSpec에서 볼륨으로 마운트되어야 합니다.

DaemonSet 또는 호스트에서 컨테이너로 배포된 다른 앱에서 pod-resources/kubelet.sock에 접근할 때, 소켓을 볼륨으로 마운트하고 있다면 소켓 파일 자체 대신 pod-resources 디렉터리를 마운트하는 것이 좋은 관행입니다. 이는 kubelet 재시작 후 컨테이너가 이 소켓에 다시 연결할 수 있도록 보장합니다.

일반적인 Linux 노드에서 이는 /var/lib/kubelet/pod-resources/kubelet.sock 대신 /var/lib/kubelet/pod-resources/를 마운트하는 것을 의미합니다.

컨테이너 마운트는 마운트된 것에 따라 소켓 또는 디렉터리를 참조하는 inode로 관리됩니다. kubelet이 재시작되면 소켓이 삭제되고 새 소켓이 생성되는 반면, 디렉터리는 그대로 유지됩니다. 그래서 소켓의 원래 inode는 사용할 수 없게 됩니다. 디렉터리의 inode는 계속 작동합니다.

Get gRPC 엔드포인트 {#grpc-endpoint-get}

Get 엔드포인트는 실행 중인 파드의 리소스에 대한 정보를 제공합니다. List 엔드포인트에서 설명한 것과 유사한 정보를 노출합니다. Get 엔드포인트는 실행 중인 파드의 PodNamePodNamespace를 요구합니다.

// GetPodResourcesRequest contains information about the pod
message GetPodResourcesRequest {
    string pod_name = 1;
    string pod_namespace = 2;
}

Get 엔드포인트는 동적 리소스 할당 API가 할당한 동적 리소스와 관련된 파드 정보를 제공할 수 있습니다. 쿠버네티스 v1.34부터 이 기능은 기본 활성화됩니다.

Topology Manager와의 장치 플러그인 통합

Topology Manager는 리소스가 Topology 정렬 방식으로 조정되도록 하는 Kubelet 구성 요소입니다. 이를 위해 Device Plugin API가 TopologyInfo 구조체를 포함하도록 확장되었습니다.

message TopologyInfo {
    repeated NUMANode nodes = 1;
}

message NUMANode {
    int64 ID = 1;
}

Topology Manager를 활용하고자 하는 장치 플러그인은 장치 등록의 일부로 장치 ID와 장치 건강과 함께 값이 채워진 TopologyInfo 구조체를 다시 보낼 수 있습니다. 그러면 장치 관리자가 이 정보를 사용해 Topology Manager와 상의하고 리소스 할당 결정을 내립니다.

TopologyInfonodes 필드를 nil 또는 NUMA 노드 목록으로 설정하는 것을 지원합니다. 이를 통해 장치 플러그인은 여러 NUMA 노드에 걸친 장치를 알릴 수 있습니다.

주어진 장치에 대해 TopologyInfonil로 설정하거나 빈 NUMA 노드 목록을 제공하는 것은 장치 플러그인이 그 장치에 NUMA 친화성 선호가 없음을 나타냅니다.

장치 플러그인에 의해 장치에 대해 채워진 TopologyInfo 구조체의 예시:

pluginapi.Device{ID: "25102017", Health: pluginapi.Healthy, Topology:&pluginapi.TopologyInfo{Nodes: []*pluginapi.NUMANode{&pluginapi.NUMANode{ID: 0,},}}}

장치 플러그인 예시 {#examples}

다음은 장치 플러그인 구현의 몇 가지 예시입니다:

더 알아보기 (Learn more)