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 어댑터 등 다양한 장치에 동일한 패턴을 적용할 수 있습니다.