TT Lab
요금제

한국어

시작하기
블로그

블로그

KubeVirt 소스 코드 읽기 5: virt-launcher — 도메인 XML 과 QEMU

상태: 초안 (2026-10-17), 소스 + 읽기 전용 실측. 기준은 KubeVirt v1.9.0(커밋 79d34c17)입니다. 실측은 도는 VM 두 대의 virt-launcher 파드 안에서 읽기 명령(virsh dumpxml, qemu-img info -U, /proc·/sys 읽기)만 쓴 것입니다.

KubeVirt 소스 코드 읽기 5: virt-launcher

목차

1. 직관: 번역기, 운전수, 파수꾼

virt-launcher 파드 하나에는 세 역할이 들어 있습니다.

4편의 virt-handler 가 「시작하라」 고 gRPC 로 부르면, 그 뒤의 일은 모두 이 파드 안에서 일어납니다.

2. 그림: SyncVMI 한 번

virt-launcher — VMI 를 도메인 XML 로 바꿔 띄우기 SyncVMI() 한 번이 하는 일, 위에서 아래로 파드 안의 프로세스 (실측 RSS) monitor 8.7Mi launcher 68.5Mi virtqemud 31.6Mi qemu-kvm 626Mi virtlogd 14.8Mi ← QEMU 의 부모는 PID 1 cmd 서버 SyncVirtualMachine cmd-server/server.go generateConverterContext() 디스크 정보 · GPU · SR-IOV · EFI 를 모은다 Convert_v1_VirtualMachineInstance_To_api_Domain 설정기 29개를 차례로, 이어서 디스크 루프 lookupOrCreateVirDomain() 없으면 preStartHook → DomainDefineXML preStartHook() cloud-init ISO · qemu-img 쓰기 층 · 빈 디스크 startDomain() → CreateWithFlags QEMU 프로세스 시작 syncDisks · network.Sync 핫플러그와 차이 맞추기 실측: Ready → Domain defined 1.20초 → Domain started 0.19초
그림: virt-launcher 안의 프로세스와 SyncVMI 일곱 걸음

3. 코드 경로

3.1 프로세스 트리를 세우는 쪽: cmd/

파드의 PID 1 은 virt-launcher-monitor 입니다. RunAndMonitor() (virt-launcher-monitor.go:98)가 /usr/bin/virt-launcher 를 자식으로 띄우고, SIGCHLD 를 받을 때마다 Wait4(-1, …) 로 좀비를 거둡니다. QEMU 는 libvirt 가 데몬처럼 띄우므로 부모가 끝나면 PID 1 의 자식이 됩니다. PID 1 이 좀비를 거두지 않으면 프로세스 표가 찹니다.

virt-launcher 의 main()(virt-launcher.go:347)은 이 순서로 준비합니다.

  1. 폴더를 만들고(initializeDirs), libvirt 데몬 virtqemud 를 띄웁니다 (:432). 로그 데몬 virtlogd 도 띄웁니다. v1.9.0 은 예전의 모놀리식 libvirtd 가 아니라 QEMU 드라이버만 맡는 모듈식 데몬을 씁니다. docs/components.md 는 아직 libvirtd 라고 적혀 있으니 코드를 믿으세요.
  2. libvirt 에 붙고, 도메인 관리자 NewLibvirtDomainManager(...) 를 만듭니다.
  3. cmd 서버를 임시 이름의 소켓으로 엽니다(startCmdServer, :482).
  4. 도메인 이벤트 감시를 시작하고, 신호 처리기를 겁니다.
  5. markReady()(:80) — 임시 소켓의 이름을 진짜 이름(launcher-sock)으로 바꾸는 것이 곧 준비 신호입니다. virt-handler 는 그 이름의 소켓이 보일 때부터 말을 겁니다.
  6. waitForDomainUUID()(:525)로 도메인이 정의되기를 기다립니다. 3편의 --qemu-timeout(240초 + 무작위)이 지나도 도메인이 없으면 스스로 끝납니다. 시작이 멈춘 파드가 영원히 남지 않게 하는 장치입니다.

3.2 SyncVMI: 요청 하나가 하는 일

cmd 서버의 SyncVirtualMachine()(cmd-server/server.go:250)은 JSON 으로 온 VMI 를 풀어 LibvirtDomainManager.SyncVMI() (manager.go:1380)에 넘깁니다. 이 함수가 virt-launcher 의 중심입니다.

  1. domainModifyLock 을 잡습니다. handler 가 같은 VMI 를 여러 번 보내도(실측에서도 「Synced vmi」 가 1초 안에 네 번) 한 번에 하나씩 처리합니다.
  2. generateConverterContext()(:1150) — 번역에 필요한 바깥 사실을 모읍니다. 디스크 이미지의 형식, 블록 장치인지, GPU·SR-IOV 장치의 PCI 주소(7편), EFI 펌웨어 위치 등입니다. converter 가 파일시스템을 직접 뒤지지 않게 해서, converter 를 순수한 함수처럼 시험할 수 있게 한 구조입니다.
  3. Convert_v1_VirtualMachineInstance_To_api_Domain()(아래 3.3) — VMI → api.Domain 구조체.
  4. lookupOrCreateVirDomain()(:1619) — 이름으로 찾아 없으면 preStartHook() 으로 디스크를 준비하고 XML 을 정의합니다. 성공하면 「Domain defined.」 (:1651).
  5. preStartHook()(:892) — cloud-init 데이터 준비, 컨테이너 디스크와 ephemeral 볼륨의 쓰기 층 만들기(3.4), 빈 디스크(emptyDisk) 만들기, ConfigMap·Secret 디스크 만들기.
  6. 도메인이 꺼져 있고 VMI 가 아직 Running 이 아니면 startDomain() (:1596) — cloud-init ISO 를 만들고 dom.CreateWithFlags() 로 QEMU 를 시작합니다. 「Domain started.」
  7. 이미 도는 도메인이면 syncDisks() 와 network.Sync() 로 핫플러그된 디스크·인터페이스의 차이만 맞춥니다.

3.3 converter: 설정기를 차례로 거는 빌더

converter/converter.go 의 Convert_v1_VirtualMachineInstance_To_api_Domain() (:967)은 장치마다 코드를 늘어놓지 않고 설정기(Configurator) 목록을 만들고(:993) 빌더가 차례로 겁니다(:1062). 목록에는 28개가 있고(메타데이터, 네트워크, TPM, vsock, 시계, 난수 장치, 입력 장치, 메모리 풍선, 그래픽, 소리, 호스트 장치, 워치독, 콘솔, SMBIOS, OS, virtiofs, 컨트롤러, CPU, IO 스레드, 메모리 …), 하이퍼바이저에 따라 KVM 또는 MSHV 설정기 하나가 더 붙어 29개입니다. 설정기 하나는 Configure(vmi, domain) 메서드 하나뿐이라, 장치 하나를 고치는 PR 은 설정기 파일 하나와 그 시험만 건드리게 됩니다. converter/compute/ 아래 파일 이름이 그대로 장치 이름입니다(watchdog.go, balloon.go, host_device.go …).

디스크는 설정기 밖에서, 빌더 뒤에 vmi.Spec.Domain.Devices.Disks 를 돌며 볼륨 종류별 변환 함수를 부릅니다. 세 가지만 보면 됩니다.

볼륨 함수 XML 의 모양
PVC·DataVolume(파일시스템) Convert_v1_FilesystemVolumeSource_To_api_Disk(:784) raw 파일 …/vmi-disks/<볼륨>/disk.img
컨테이너 디스크 Convert_v1_ContainerDiskSource_To_api_Disk(:898) qcow2 쓰기 층 + <backingStore> 에 이미지
ephemeral(PVC 위 임시 쓰기) Convert_v1_EphemeralVolumeSource_To_api_Disk(:922) qcow2 쓰기 층 + <backingStore> 에 PVC

도메인 이름은 메타데이터 설정기가 <네임스페이스>_<이름> 으로 정합니다 (metadata/configurator.go:34).

3.4 쓰기 층: pkg/ephemeral-disk/ephemeral-disk.go

도메인 XML 이 가리키는 파일과 장치 실측한 두 VM 의 <disk> · <interface> 를 그렸다 디스크 세 갈래 컨테이너 디스크 읽기 전용 바탕 disk_0.img 596Mi 쓰기 층 disk.qcow2 — 998Mi …/ephemeral-disks/disk-data/root/ qemu-img create -b 바탕 파드가 지워지면 emptyDir 와 함께 쓰기 층도 사라진다 PVC 디스크 — raw disk.img (10 GiB) /var/run/kubevirt-private/vmi-disks/root/disk.img 빈 디스크 emptyDisk scratch.qcow2 1.6G cloud-init noCloud.iso (raw) driver: cache=none · error_policy=stop · discard=unmap 네트워크 (masquerade 바인딩) eth0 파드 IP k6t-eth0 브리지 · NAT tap0 QEMU 가 연다 <interface type='ethernet'> target dev='tap0' managed='no' 브리지 MAC 은 고정값 02:00:00:00:00:00 (link/mac.go) 게스트 NIC 의 MAC 은 파드 eth0 의 MAC 을 그대로 쓴다(실측)
그림: 도메인 XML 이 가리키는 디스크 세 갈래와 masquerade 네트워크

컨테이너 디스크는 이미지 안의 파일이라 읽기 전용입니다. 게스트가 쓰려면 그 위에 qcow2 쓰기 층이 있어야 합니다. containerdisk.CreateEphemeralImages()(container-disk.go:328)가 컨테이너 디스크 볼륨마다 CreateBackedImageForVolume() (ephemeral-disk.go:96)을 부르고, 실제 명령은 이것입니다 (:143).

cmd := exec.Command("qemu-img",
	"create",
	"-f",
	"qcow2",
	"-b",
	backingFile,
	"-F",
	backingFormat,
	imagePath,
)

출처: kubevirt/kubevirt v1.9.0 pkg/ephemeral-disk/ephemeral-disk.go 145–154행(Apache-2.0). 쓰기 층의 자리는 GetFilePath() (:91)가 정하는 <ephemeral-disk-dir>/<볼륨>/disk.qcow2 이고, 그 폴더는 파드의 emptyDir 입니다. 파드가 지워지면 쓰기 층도 함께 사라집니다. 이미 파일이 있으면 다시 만들지 않으므로 같은 파드 안에서 launcher 가 SyncVMI 를 여러 번 받아도 안전합니다. 「학생 VM 이 뜨기까지」 4편이 말한 「골든 디스크 위의 얇은 쓰기 층」 이 바로 이 함수의 volume.Ephemeral 갈래(CreateEphemeralImages, :127)입니다.

3.5 네트워크: masquerade 바인딩

파드 네트워크를 쓰는 VM 의 기본 바인딩은 masquerade 입니다. 일은 둘로 나뉩니다.

4. 실측: 파드 안을 읽다

두 VM 의 virt-launcher 파드(compute 컨테이너)에서 읽었습니다(2026-10-17). VM A 는 2 vCPU·2Gi·PVC 루트, VM B 는 8 vCPU·8Gi·컨테이너 디스크 루트입니다.

프로세스 트리와 메모리(VM A). /proc/*/status 의 PPid 와 VmRSS 입니다.

PID 부모 프로세스 RSS 3편 추정식의 몫
1 — virt-launcher-monitor 8.7MiB 25Mi
13 1 virt-launcher 68.5MiB 100Mi
26 13 virtqemud 31.6MiB 40Mi
27 13 virtlogd 14.8MiB 25Mi
97 1 qemu-kvm 626MiB 게스트 2Gi + 30Mi 등

QEMU 의 부모가 PID 1 인 것이 3.1절의 「libvirt 가 데몬처럼 띄운 QEMU 를 monitor 가 거둔다」 입니다. QEMU 를 뺀 네 프로세스의 RSS 합은 123.6MiB 로, 추정식의 고정값(190Mi, QEMU 30Mi 제외) 안에 들었습니다. 공유 라이브러리를 프로세스마다 셈하는 RSS 의 성질상 실제 여유는 더 큽니다. 게스트는 2Gi 를 받았지만 QEMU 의 RSS 는 626MiB 였습니다. 게스트가 아직 만지지 않은 메모리는 호스트에서 잡히지 않습니다.

libvirt 연결. launcher 로그의 Connecting to libvirt daemon: qemu+unix:///session?socket=/var/run/libvirt/virtqemud-sock (libvirt.go:680)이 비루트 세션 모드로 virtqemud 에 붙은 것을 보여 줍니다.

도메인 XML(VM A, 줄임). virsh dumpxml default_fedora-vm 의 앞부분과 장치 일부입니다.

<domain type='kvm' id='1'>
  <name>default_fedora-vm</name>
  <maxMemory unit='KiB'>8388608</maxMemory>
  <memory unit='KiB'>2097152</memory>
  <vcpu placement='static'>2</vcpu>
  <os><type arch='x86_64' machine='pc-q35-rhel9.8.0'>hvm</type> …</os>
  <cpu mode='custom' match='exact' check='full'> … 기능 70여 줄 줄임 … </cpu>
  <devices>
    <emulator>/usr/libexec/qemu-kvm</emulator>
    <disk type='file' device='disk' model='virtio-non-transitional'>
      <driver name='qemu' type='raw' cache='none' error_policy='stop' discard='unmap'/>
      <source file='/var/run/kubevirt-private/vmi-disks/root/disk.img' index='2'/>
      <target dev='vda' bus='virtio'/>
      <alias name='ua-root'/>
    </disk>
    <interface type='ethernet'>
      <target dev='tap0' managed='no'/>
      <model type='virtio-non-transitional'/>
      <alias name='ua-default'/>
    </interface>
    <serial type='unix'><source mode='bind' path='/var/run/kubevirt-private/<VMI UID>/virt-serial0'/> …</serial>
  </devices>
</domain>

코드와 맞춰 보면 이렇습니다. 이름은 <네임스페이스>_<이름>, maxMemory 는 게스트의 4배(메모리 핫플러그 상한의 기본 비율 DefaultMaxHotplugRatio = 4), 디스크는 3.3절 표의 첫 줄, 사용자가 지은 이름 앞에는 ua- 가 붙고(user alias), 시리얼 콘솔과 VNC 는 TCP 가 아니라 kubevirt-private/<VMI UID>/ 아래 유닉스 소켓입니다. virt-handler 가 콘솔을 이 소켓으로 잇습니다. 그 밖에 pcie-root-port 컨트롤러가 10개 있었습니다. 일부는 디스크·NIC 가 쓰고, 남은 것은 allocateHotplugPorts() 가 핫플러그용으로 미리 깔아 둔 빈 자리입니다.

쓰기 층(VM B). 디스크 XML 과 qemu-img info -U 입니다.

파일 형식 가상 크기 실제 크기
/var/run/kubevirt/container-disks/disk_0.img(바탕, 읽기 전용) qcow2 3.5GiB 596MiB
/var/run/kubevirt-ephemeral-disks/disk-data/root/disk.qcow2(쓰기 층) qcow2, backing file = 위 파일 3.5GiB 998MiB(약 2.5일 사용)
/var/run/libvirt/empty-disks/scratch.qcow2(emptyDisk) qcow2 — 1.6G

쓰기 층의 경로가 GetFilePath() 의 <볼륨 이름>/disk.qcow2, backing 형식이 -F qcow2 와 같습니다. 바탕 596MiB 는 「학생 VM 이 뜨기까지」 3편의 실측과 같은 크기입니다. 같은 파드의 container-disks 폴더에는 disk_0.sock 도 있었습니다. 4편에서 virt-handler 가 이미지를 품은 컨테이너를 찾아 붙일 때 쓰는 표식입니다.

네트워크(VM A). 파드 안 /sys/class/net 에는 eth0, k6t-eth0(MAC 02:00:00:00:00:00), tap0(브리지 k6t-eth0 에 달림), lo 넷이 있었습니다. 도메인 XML 의 게스트 MAC 은 파드 eth0 의 MAC 과 같았습니다.

시간. launcher 로그에서 「Marked as ready」 → 「Domain defined.」 1.20초 → 「Domain started.」 0.19초였습니다(4편 표). 앞의 1.2초는 대부분 handler 쪽 준비(네트워크·장치) 시간이고, 정의와 시작 자체는 짧습니다.

5. 한계

티티 한마디. converter 가 설정기 목록으로 바뀐 덕분에, 장치 하나를 고치고 싶으면 converter/compute/ 에서 그 장치 이름의 파일 하나만 열면 돼요. 첫 기여로 딱 좋은 크기예요!

참고

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

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

댓글

아직 댓글이 없습니다.

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