Kubernetes Device Plugin 메커니즘 원리와 구현

Kubernetes에서는 기본적으로 CPU와 메모리만을 리소스로 인식하지만, GPU와 같은 특수한 하드웨어 자원을 관리하기 위해 Device Plugin 프레임워크를 제공합니다. 이 문서는 Device Plugin의 작동 원리를 분석하고 간단한 예제를 통해 구현 방법을 설명합니다.

작동 원리

Device Plugin은 두 가지 주요 역할로 구성됩니다:

  • 플러그인 등록: Device Plugin이 시작될 때 Kubelet에 자신을 등록하여 리소스 존재를 알립니다
  • Kubelet 호출: 등록 후 Pod에서 해당 리소스를 요청하면 Kubelet이 플러그인 API를 호출하여 기능을 수행합니다

Kubelet 측 구현

Kubelet은 Registration이라는 gRPC 서비스를 제공합니다:

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

Device Plugin은 이 인터페이스를 통해 등록하며 다음 정보를 제공해야 합니다:

  • unix socket 이름: Kubelet이 플러그인과 통신하기 위한 경로
  • API 버전: 플러그인 버전 식별용
  • 리소스 이름: 도메인/리소스타입 형식 (예: nvidia.com/gpu)

Device Plugin 측 구현

Device Plugin은 다음 인터페이스들을 구현해야 합니다:

  • ListAndWatch: 사용 가능한 장치 목록을 반환하고 상태 변화를 감시합니다 (필수)
  • Allocate: 컨테이너에 장치를 할당하는 로직을 구현합니다 (필수)
  • GetDevicePluginOptions: 플러그인 옵션 정보를 반환합니다 (선택)
  • GetPreferredAllocation: 선호하는 할당 방식을 제공합니다 (선택)
  • PreStartContainer: 컨테이너 시작 전 실행되는 로직입니다 (선택)

구현 예제

간단한 Device Plugin을 구현해보겠습니다. 이 예제는 /etc/gophers 디렉토리의 파일을 장치로 취급합니다.

gRPC 서버 구현

ListAndWatch 메서드

// 장치 목록을 지속적으로 모니터링하고 변경사항을 전송
func (g *GopherPlugin) ListAndWatch(_ *pluginapi.Empty, stream pluginapi.DevicePlugin_ListAndWatchServer) error {
	devices := g.monitor.GetDevices()
	response := &pluginapi.ListAndWatchResponse{Devices: devices}
	
	if err := stream.Send(response); err != nil {
		return fmt.Errorf("장치 전송 실패: %v", err)
	}

	// 장치 변경 감시
	for {
		select {
		case <-g.monitor.UpdateChannel():
			devices = g.monitor.GetDevices()
			response = &pluginapi.ListAndWatchResponse{Devices: devices}
			if err := stream.Send(response); err != nil {
				return fmt.Errorf("업데이트 전송 실패: %v", err)
			}
		}
	}
}

Allocate 메서드

// 컨테이너에 장치 할당 정보 설정
func (g *GopherPlugin) Allocate(_ context.Context, req *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
	response := &pluginapi.AllocateResponse{}
	
	for _, containerReq := range req.ContainerRequests {
		containerResponse := &pluginapi.ContainerAllocateResponse{
			Envs: map[string]string{
				"GOPHER_DEVICES": strings.Join(containerReq.DevicesIDs, ","),
			},
		}
		response.ContainerResponses = append(response.ContainerResponses, containerResponse)
	}
	
	return response, nil
}

장치 모니터링

// 파일 시스템 이벤트를 통해 장치 추가/삭제 감시
type DeviceMonitor struct {
	path         string
	devices      map[string]*pluginapi.Device
	updateChan   chan struct{}
}

func (dm *DeviceMonitor) Watch() error {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return fmt.Errorf("watcher 생성 실패: %v", err)
	}
	defer watcher.Close()

	go func() {
		for {
			select {
			case event := <-watcher.Events:
				if event.Op&fsnotify.Create == fsnotify.Create {
					deviceID := filepath.Base(event.Name)
					dm.devices[deviceID] = &pluginapi.Device{
						ID:     deviceID,
						Health: pluginapi.Healthy,
					}
					dm.updateChan <- struct{}{}
				} else if event.Op&fsnotify.Remove == fsnotify.Remove {
					deviceID := filepath.Base(event.Name)
					delete(dm.devices, deviceID)
					dm.updateChan <- struct{}{}
				}
			}
		}
	}()

	return watcher.Add(dm.path)
}

Kubelet 등록

// Kubelet에 플러그인 등록
func (g *GopherPlugin) Register() error {
	conn, err := grpc.Dial(pluginapi.KubeletSocket, grpc.WithInsecure())
	if err != nil {
		return fmt.Errorf("kubelet 연결 실패: %v", err)
	}
	defer conn.Close()

	client := pluginapi.NewRegistrationClient(conn)
	request := &pluginapi.RegisterRequest{
		Version:      pluginapi.Version,
		Endpoint:     filepath.Base(g.socketPath),
		ResourceName: g.resourceName,
	}

	_, err = client.Register(context.Background(), request)
	return err
}

Kubelet 재시작 감지

// Kubelet 재시작 시 플러그인 재등록
func WatchKubeletRestart(stop chan<- struct{}) error {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return fmt.Errorf("watcher 생성 실패: %v", err)
	}
	defer watcher.Close()

	go func() {
		for event := range watcher.Events {
			if event.Name == pluginapi.KubeletSocket && event.Op == fsnotify.Create {
				stop <- struct{}{}
			}
		}
	}()

	return watcher.Add(pluginapi.KubeletSocket)
}

배포 및 테스트

DaemonSet으로 배포

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: gopher-device-plugin
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: gopher-device-plugin
  template:
    spec:
      containers:
      - name: gopher-device-plugin
        image: your-registry/gopher-device-plugin:latest
        volumeMounts:
        - name: device-plugin
          mountPath: /var/lib/kubelet/device-plugins
        - name: gophers
          mountPath: /etc/gophers
      volumes:
      - name: device-plugin
        hostPath:
          path: /var/lib/kubelet/device-plugins
      - name: gophers
        hostPath:
          path: /etc/gophers

테스트 Pod

apiVersion: v1
kind: Pod
metadata:
  name: test-gopher-pod
spec:
  containers:
  - name: test-container
    image: busybox
    command: ["sleep", "3600"]
    resources:
      requests:
        example.com/gopher: "1"
      limits:
        example.com/gopher: "1"

이렇게 구현된 Device Plugin은 Kubernetes 클러스터에서 특수 하드웨어 리소스를 효과적으로 관리할 수 있게 해줍니다. NVIDIA GPU, InfiniBand 어댑터 등 다양한 장치에 동일한 패턴을 적용할 수 있습니다.

태그: kubernetes device-plugin GPU NVIDIA CUDA

6월 17일 20:09에 게시됨