상태: 초안 (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 파드 하나에는 세 역할이 들어 있습니다.
- 번역기: VMI(쿠버네티스의 말)를 libvirt 도메인 XML(가상화의 말)로 바꿉니다. 이 일이 converter 입니다.
- 운전수: 파드 안에 libvirt 데몬을 띄우고, 그 데몬에게 도메인을 정의하고 시작하라고 시킵니다.
- 파수꾼: QEMU 가 끝나면 파드도 끝나게 하고, 쿠버네티스가 파드를 멈추려 하면 신호를 VM 쪽으로 넘깁니다.
4편의 virt-handler 가 「시작하라」 고 gRPC 로 부르면, 그 뒤의 일은 모두 이 파드 안에서 일어납니다.
2. 그림: 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)은
이 순서로 준비합니다.
- 폴더를 만들고(
initializeDirs), libvirt 데몬virtqemud를 띄웁니다 (:432). 로그 데몬virtlogd도 띄웁니다. v1.9.0 은 예전의 모놀리식libvirtd가 아니라 QEMU 드라이버만 맡는 모듈식 데몬을 씁니다.docs/components.md는 아직libvirtd라고 적혀 있으니 코드를 믿으세요. - libvirt 에 붙고, 도메인 관리자
NewLibvirtDomainManager(...)를 만듭니다. - cmd 서버를 임시 이름의 소켓으로 엽니다(
startCmdServer, :482). - 도메인 이벤트 감시를 시작하고, 신호 처리기를 겁니다.
markReady()(:80) — 임시 소켓의 이름을 진짜 이름(launcher-sock)으로 바꾸는 것이 곧 준비 신호입니다. virt-handler 는 그 이름의 소켓이 보일 때부터 말을 겁니다.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 의 중심입니다.
domainModifyLock을 잡습니다. handler 가 같은 VMI 를 여러 번 보내도(실측에서도 「Synced vmi」 가 1초 안에 네 번) 한 번에 하나씩 처리합니다.generateConverterContext()(:1150) — 번역에 필요한 바깥 사실을 모읍니다. 디스크 이미지의 형식, 블록 장치인지, GPU·SR-IOV 장치의 PCI 주소(7편), EFI 펌웨어 위치 등입니다. converter 가 파일시스템을 직접 뒤지지 않게 해서, converter 를 순수한 함수처럼 시험할 수 있게 한 구조입니다.Convert_v1_VirtualMachineInstance_To_api_Domain()(아래 3.3) — VMI →api.Domain구조체.lookupOrCreateVirDomain()(:1619) — 이름으로 찾아 없으면preStartHook()으로 디스크를 준비하고 XML 을 정의합니다. 성공하면 「Domain defined.」 (:1651).preStartHook()(:892) — cloud-init 데이터 준비, 컨테이너 디스크와 ephemeral 볼륨의 쓰기 층 만들기(3.4), 빈 디스크(emptyDisk) 만들기, ConfigMap·Secret 디스크 만들기.- 도메인이 꺼져 있고 VMI 가 아직 Running 이 아니면
startDomain()(:1596) — cloud-init ISO 를 만들고dom.CreateWithFlags()로 QEMU 를 시작합니다. 「Domain started.」 - 이미 도는 도메인이면
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
컨테이너 디스크는 이미지 안의 파일이라 읽기 전용입니다. 게스트가 쓰려면 그 위에 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 입니다. 일은 둘로 나뉩니다.
- virt-handler(파드 밖) 가 파드의 네트워크 네임스페이스에 브리지
k6t-eth0과 tap 을 만듭니다. 바라는 모양은masqueradeBindingSpec()(network/setup/netpod/netpod.go:415)이 그립니다. 브리지에는 고정 MAC02:00:00:00:00:00(link/mac.go:27)과 VM 전용 작은 사설 대역(api.DefaultVMCIDR)의 게이트웨이 주소를 주고, tap 의 소유자는 QEMU 의 UID 로 둡니다. 그리고 nftables 에 「게스트 주소에서 나가는 것은 파드 주소로 바꿔라」 는 규칙을 넣습니다(MasqPod.Setup, masquerade.go:99). - virt-launcher(파드 안) 는 게스트에게 그 대역의 주소를 주는 작은 DHCP 서버를 띄우고(
network/setup/launcher의ensureDHCP), 도메인 XML 에는<interface type='ethernet'>과<target dev='tap0' managed='no'/>를 적습니다 (domainspec/generators.go:106).managed='no'는 「tap 은 이미 누가 만들어 두었으니 libvirt 는 만들지 말고 열기만 하라」 는 뜻입니다. 비루트 libvirt 는 tap 을 만들 권한이 없으므로, 이 분업이 비루트 VM 을 가능하게 합니다.
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. 한계
- GPU·호스트 장치가 붙은 도메인은 이번에 도는 것이 없어 XML 을 읽지 못했습니다(7편, 미실측).
- DHCP 로 게스트가 받은 주소는 게스트 안을 봐야 해서 확인하지 않았습니다. 게스트 에이전트가 보고한 VMI status 의 주소는 파드 주소였습니다.
- 메모리 수치는 한 시점의 RSS 입니다. 시간에 따른 변화, 페이지 캐시와의 관계는 미실측입니다.
- CPU 모델은
host-model을 풀어 쓴 이름 있는 모델이었고(기능 목록 70여 줄), 노드의 CPU 를 드러내므로 글에서는 줄였습니다.
티티 한마디. converter 가 설정기 목록으로 바뀐 덕분에, 장치 하나를 고치고 싶으면
converter/compute/에서 그 장치 이름의 파일 하나만 열면 돼요. 첫 기여로 딱 좋은 크기예요!
참고
모두 2026-10-17 에 열람했습니다.
manager.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/manager.goconverter.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/converter/converter.goephemeral-disk.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/ephemeral-disk/ephemeral-disk.go- libvirt 도메인 XML 형식: https://libvirt.org/formatdomain.html
- libvirt 모듈식 데몬(virtqemud): https://libvirt.org/daemons.html
- 자매 시리즈 「학생 VM 이 뜨기까지」 3편: 디스크는 PVC 다, 4편: PVC 관점의 오버레이