TT Lab
요금제

한국어

시작하기
블로그

블로그

KubeVirt 소스 코드 읽기 7: 장치 패스스루 — GPU 가 도메인 XML 이 되기까지

상태: 초안 (2026-10-17), 소스 + 읽기 전용 실측. 기준은 KubeVirt v1.9.0(커밋 79d34c17)입니다. GPU 를 붙인 VM 은 꺼져 있어 그 도메인 XML 은 코드로 그린 예상입니다(미실측). 실측은 KubeVirt CR, 노드의 allocatable, virt-handler 로그를 읽은 것입니다.

KubeVirt 소스 코드 읽기 7: 장치 패스스루

목차

1. 직관: 세 군데가 같은 장치를 알아야 한다

PCI 장치를 VM 에 넘기려면 세 군데가 같은 사실을 알아야 합니다.

  1. 호스트 커널: 그 장치를 원래 드라이버에서 떼어 vfio-pci 에 묶어야 합니다. 그래야 사용자 공간(QEMU)이 IOMMU 의 보호 아래 장치를 직접 다룰 수 있습니다.
  2. 쿠버네티스: 장치가 「노드의 자원」 으로 보여야 스케줄러가 그 노드에 파드를 앉히고, kubelet 이 파드에 장치 파일을 넣어 줍니다. 이 다리가 장치 플러그인입니다.
  3. libvirt: 도메인 XML 에 <hostdev> 가 있어야 QEMU 가 그 PCI 주소를 게스트에 붙입니다.

KubeVirt 는 1을 하지 않습니다(관리자 몫). 2와 3을 잇는 일을 구성요소 넷이 나눠 합니다.

2. 그림: 일곱 걸음

GPU 한 장이 VMI 스펙에서 도메인 XML 까지 번호 순서. 위 셋은 미리 깔리는 길, 아래 넷은 VM 을 켤 때의 길 ① KubeVirt CR: permittedHostDevices pciVendorSelector 10DE:xxxx → resourceName nvidia.com/<GPU> ② virt-handler 장치 관리자 — sysfs 를 훑는다 vendor:device 가 맞고 드라이버가 vfio-pci 인 장치만 센다 (vfio-pci 가 아니면 vGPU 가능한 VF 인지만 한 번 더 본다) ③ 장치 플러그인 → kubelet 등록 → 노드 allocatable 장치 ID 는 IOMMU 그룹 번호 ④ VMI spec.domain.devices.gpus[].deviceName virt-controller 가 파드 limits 에 그 자원을 1개 넣는다 ⑤ 스케줄러 · kubelet Allocate() /dev/vfio/vfio · /dev/vfio/<그룹> 을 파드에 넣고 환경 변수 PCI_RESOURCE_NVIDIA_COM_<GPU>=0000:01:00.0 ⑥ virt-launcher: 주소 풀 → createPCIHostDevice() 환경 변수에서 PCI 주소를 꺼내 <hostdev> 를 만든다 메모리 오버헤드 +1Gi (VFIO 는 게스트 RAM 전체를 잠근다) ⑦ <hostdev mode='subsystem' type='pci' managed='no'> <source><address domain='0x0000' bus='0x01' …/></source> 실측: ①은 켜져 있는데 그 GPU 노드의 allocatable 은 0 virt-handler 로그(nvidia.go:33): 「Skipping non-vfio-pci device without vGPU profile … driver: nvidia」 — 호스트 드라이버가 쥠 → ② 에서 걸러져 ③ 이 0. vfio-pci 로 바꿔 묶어야 VM 이 받는다
그림: GPU 한 장이 VMI 스펙에서 도메인 XML 까지 가는 길과 실측한 막힘

3. 코드 경로

① 허용 목록 — KubeVirt CR

KubeVirt CR 의 spec.configuration.permittedHostDevices.pciHostDevices[] 에 pciVendorSelector(벤더:장치 16진수)와 resourceName 을 적습니다. 기능 게이트 HostDevices 도 켜야 합니다. 여기 없는 장치는 VMI 가 이름을 적어도 쓸 수 없습니다. virt-controller 가 파드를 그리기 전에 validatePermittedHostDevices() (renderresources.go:493)로 확인합니다.

② 장치 관리자가 sysfs 를 훑는다 — virt-handler

virt-handler 안의 DeviceController.Run()(device_controller.go:481)은 KubeVirt 설정이 바뀔 때마다 refreshPermittedDevices() 가 불리도록 콜백을 겁니다 (:497). 이 함수가 updatePermittedHostDevicePlugins()(:208)를 거쳐 discoverPermittedHostPCIDevices()(pci_device.go:258)를 부릅니다. 이 함수가 /sys/bus/pci/devices 를 돌며 하는 일은 셋입니다.

  1. 장치의 벤더:장치 가 허용 목록에 있는지.
  2. 드라이버가 vfio-pci 인지(:278). 아니면 shouldPermitNvidia()(nvidia.go:23)가 한 번 더 봅니다. NVIDIA 드라이버에 묶인 장치라도 vGPU 프로필을 가진 가상 기능(VF)이면 받아 주고, 그 밖에는 로그를 남기고 버립니다.
  3. 장치의 IOMMU 그룹 번호와 NUMA 노드를 읽습니다.

③ 장치 플러그인 — kubelet 에 자원 등록

자원 이름마다 PCIDevicePlugin 이 하나씩 떠서(Start, pci_device.go:62) kubelet 의 장치 플러그인 소켓에 등록합니다. kubelet 에게 내미는 장치 ID 는 PCI 주소가 아니라 IOMMU 그룹 번호입니다 (constructDPIdevices, :132). VFIO 는 그룹 단위로 장치를 넘기므로, 같은 그룹의 장치를 서로 다른 VM 에 줄 수 없기 때문입니다. 등록이 되면 노드의 status.allocatable 에 nvidia.com/<GPU 이름>: 1 처럼 나타납니다. 같은 장치 관리자가 devices.kubevirt.io/kvm·tun·vhost-net 같은 일반 장치도 같은 방식으로 내놓습니다.

④ 파드가 자원을 요청 — virt-controller

VMI 에 spec.domain.devices.gpus[].deviceName: nvidia.com/<GPU 이름> 이 있으면, 3편의 템플릿이 자원 렌더러 옵션 WithGPUsDevicePlugins()(renderresources.go:149)로 compute 컨테이너의 limits 에 그 자원을 1개씩 넣습니다. 메모리 오버헤드에는 1Gi 가 더해집니다 (hypervisorbackend.go:130). VFIO 는 DMA 를 위해 게스트 RAM 전체를 잠그기 때문이라고 주석에 적혀 있습니다. 판단 기준은 vmitrait.HasVFIO() (vmitrait.go:33) — 호스트 장치·GPU·SR-IOV 중 하나라도 있으면입니다.

⑤ Allocate — 장치 파일과 주소를 넘긴다

스케줄러가 그 자원이 남은 노드에 파드를 앉히면, kubelet 이 장치 플러그인의 Allocate() (pci_device.go:152)를 부릅니다. 답은 두 가지입니다.

이 환경 변수가 노드(handler)와 파드(launcher)를 잇는 유일한 끈입니다. launcher 는 sysfs 를 보지 않고 이 값만 믿습니다.

⑥ launcher 가 <hostdev> 를 만든다

5편의 generateConverterContext() 가 getGPUDevices()(manager.go:1138)를 부르고, gpu.CreateHostDevices()(gpu/hostdev.go:39)가 주소 풀을 만듭니다. 풀은 ⑤의 환경 변수를 읽어 채웁니다(AddressPool.load, addresspool.go:46). GPU 마다 주소를 하나씩 꺼내 createPCIHostDevice()(hostdev.go:141)가 api.HostDevice 를 만들고, converter 의 HostDeviceDomainConfigurator (compute/host_device.go:42)가 도메인에 붙입니다. 같은 GPU 목록이 vGPU(mediated device)면 MDEV_PCI_RESOURCE_… 변수와 CreateMDEVHostDevices 쪽으로 갑니다. v1.9.0 에는 장치 플러그인 대신 DRA(Dynamic Resource Allocation)로 받는 길(hostdevice/dra/)도 기능 게이트 뒤에 들어와 있습니다.

⑦ 예상 XML (미실측)

코드대로라면 GPU 하나(이름 gpu1)의 도메인 XML 은 이런 모양입니다.

<hostdev mode='subsystem' type='pci' managed='no'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
  <alias name='ua-gpu-gpu1'/>
</hostdev>

managed='no' 가 중요합니다. managed='yes' 면 libvirt 가 VM 을 켤 때 장치를 호스트 드라이버에서 떼어 vfio-pci 에 묶어 주지만, 파드 안의 비루트 libvirt 에는 그럴 권한이 없습니다. 그래서 묶는 일은 호스트에서 미리 해 두어야 하고(1절의 1), KubeVirt 는 이미 묶인 장치만 셉니다(②). 별칭은 사용자 별칭 접두사 ua- 에 GPU 접두사 gpu- 와 이름이 붙습니다. 또 이런 장치가 있는 VMI 는 virt-handler 가 라이브 마이그레이션 불가로 표시합니다(vmiContainsNonMigratablePCIHostDevices, vm.go:1304).

4. 실측: 허용 목록은 켜졌는데 자원이 0

작은 클러스터의 GPU 노드 하나를 VM 전용으로 쓰려고 설정해 둔 상태를 읽었습니다(2026-10-17).

5. 한계

티티 한마디. 「허용 목록에 넣었는데 왜 안 보이지?」 싶으면 virt-handler 로그에서 Skipping 을 찾아보세요. KubeVirt 는 이미 vfio-pci 에 묶인 장치만 세요. 묶는 건 호스트 몫이에요!

참고

모두 2026-10-17 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다