Skip to content

Split View: virt-launcher, libvirt, QEMU: 실제 VM 프로세스는 어디서 어떻게 뜨는가

|

virt-launcher, libvirt, QEMU: 실제 VM 프로세스는 어디서 어떻게 뜨는가

들어가며

이제 KubeVirt의 가장 중요한 실행 지점을 보자. virt-controller는 Pod를 만들고, virt-handler는 노드에서 조율한다. 그렇다면 실제 VM은 어디서 뜰까? 답은 virt-launcher Pod 안이다.

정확히 말하면, virt-launcher Pod는 VM 하나를 위한 실행 셸이고, 그 안에서 libvirt와 QEMU가 guest를 실제로 구동한다. cmd/virt-launcher/virt-launcher.gopkg/virt-launcher/virtwrap/manager.go는 이 실행 계층의 중심이다.

virt-launcher Pod를 어떻게 이해해야 하나

많은 초보 설명은 "virt-launcher가 VM을 띄운다"라고 끝난다. 하지만 더 정확한 설명은 이렇다.

  • virt-launcher Pod는 VM 실행에 필요한 namespace와 cgroup 경계를 제공한다.
  • 그 Pod 내부의 virt-launcher 프로세스는 command server와 domain manager를 준비한다.
  • libvirt 연결이 생성된다.
  • QEMU domain이 정의되고 실행된다.
  • 이후 이벤트와 guest agent, stats가 계속 감시된다.

virt-launcher는 단순 container entrypoint가 아니라 VM 실행 제어 plane의 local runtime이다.

cmd/virt-launcher/virt-launcher.go가 하는 일

이 엔트리 포인트 파일을 보면 초기화 순서가 꽤 명확하다.

1. libvirt event 구현 등록

프로세스 시작 시 libvirt.EventRegisterDefaultImpl()가 호출된다. 이는 이후 domain event monitoring을 위해 필요하다.

2. 각종 디렉터리 초기화

initializeDirs는 cloud-init, ignition, container disk, hotplug disk, secret, config map, downward metrics 등 guest 런타임에 필요한 여러 디렉터리를 준비한다.

여기서 이미 중요한 사실이 보인다. VM 부팅은 단순히 QEMU 바이너리 하나 띄우는 일이 아니라, 여러 디스크 소스와 메타데이터 채널을 정리하는 준비 작업과 함께 간다.

3. libvirt 연결 생성

createLibvirtConnection은 기본적으로 qemu:///system에 연결하고, 비 root 모드에서는 session URI를 사용한다. 즉 virt-launcher는 단순히 libvirt 라이브러리를 링크하는 것이 아니라, 실제 libvirt 제어 plane에 client로 붙어 domain을 정의한다.

4. command server 시작

startCmdServer는 unix socket 기반 command server를 띄운다. virt-handler는 이 소켓을 통해 launcher 측 domain manager를 호출한다.

5. domain event monitoring 시작

startDomainEventMonitoring은 libvirt 이벤트 루프를 돌리고 notifier를 시작한다. guest 상태 변화, 종료, agent 연동 상태가 여기서 반영된다.

왜 command server가 필요한가

이건 KubeVirt 구조를 이해하는 핵심 포인트다. virt-handler는 node agent이지만, QEMU는 launcher Pod 내부에 있다. 두 프로세스는 서로 다른 격리 경계에 있으므로 직접 함수 호출을 할 수 없다. 그래서 KubeVirt는 command server와 client 계층을 둔다.

흐름은 대략 이렇다.

  1. virt-handler가 VMI를 reconcile한다.
  2. launcher Pod 내부 socket에 요청을 보낸다.
  3. command server가 DomainManager 메서드를 호출한다.
  4. LibvirtDomainManager가 libvirt API를 통해 domain을 define, start, pause, migrate한다.

이 구조 덕분에 cluster node 쪽 orchestration과 Pod 내부 하이퍼바이저 제어가 분리된다.

DomainManager 인터페이스가 보여주는 책임 범위

pkg/virt-launcher/virtwrap/manager.goDomainManager 인터페이스를 보면, launcher가 단순 boot만 담당하는 것이 아니라는 점이 선명하다.

대표 메서드는 다음과 같다.

  • SyncVMI
  • PauseVMI
  • UnpauseVMI
  • KillVMI
  • DeleteVMI
  • MigrateVMI
  • PrepareMigrationTarget
  • FinalizeVirtualMachineMigration
  • HotplugHostDevices
  • GetDomainStats
  • GuestPing

즉 launcher는 guest lifecycle 전체를 담당하는 local hypervisor adapter다.

libvirt가 하는 일, QEMU가 하는 일

이 둘은 자주 한 덩어리로 말해지지만 역할이 다르다.

libvirt

  • domain 정의와 관리 API 제공
  • XML 기반 domain model 유지
  • migration API 제공
  • stats와 event 수집 인터페이스 제공

QEMU

  • 실제 guest CPU, 메모리, 디스크, NIC 에뮬레이션
  • KVM과 결합한 실행 엔진
  • virtio 장치 구현

KubeVirt는 대체로 libvirt를 주 제어 인터페이스로 사용하고, libvirt가 내부적으로 QEMU를 관리하도록 한다.

readiness와 socket 전환이 의미하는 것

markReady는 uninitialized socket 이름을 실제 socket 이름으로 바꾼다. 이 작은 동작이 중요한 이유는, virt-handler가 launcher가 아직 초기화 중인지 아니면 실제 명령을 받을 준비가 되었는지 구분할 수 있게 해주기 때문이다.

즉 launcher는 Pod가 Running이라고 곧바로 "VM 제어 준비 완료"라고 가정하지 않는다. command plane readiness가 별도로 있다.

guest agent와 이벤트 루프까지 launcher에 있는 이유

guest 내부 정보를 다루는 책임도 launcher에 많다. agent poller, guest info, filesystem, user, time sync 같은 기능이 manager.go 인터페이스와 virt-launcher.go 초기화 경로에 보인다.

이건 자연스럽다. guest agent는 결국 libvirt 또는 QEMU가 노출하는 guest 통신 채널과 가장 가깝기 때문이다. node agent인 virt-handler는 이를 직접 다루기보다 launcher에게 묻는 편이 맞다.

종료 시 왜 Pod가 바로 죽지 않을 수 있는가

docs/components.md도 언급하지만, Kubernetes가 virt-launcher Pod 종료를 시도할 때 guest가 아직 종료되지 않았다면 launcher는 signal을 guest 쪽으로 전달하고, 가능한 한 정상 종료를 기다린다.

이 동작은 매우 중요하다. 그렇지 않으면 Pod 종료가 guest 강제 종료와 거의 동일해져서 데이터 손상 위험이 커진다. KubeVirt는 Pod lifecycle과 VM lifecycle이 완전히 같은 것이 아니라는 점을 이 지점에서 드러낸다.

자주 하는 오해

오해 1: virt-launcher는 그냥 sidecar 없는 일반 컨테이너다

아니다. 이 Pod는 VM 실행 경계이며, 내부에 libvirt event loop, command server, guest agent 관련 로직이 있다.

오해 2: QEMU는 virt-handler가 직접 띄운다

아니다. virt-handler는 launcher의 command server를 호출하고, 실제 domain define과 실행은 launcher 내부 domain manager가 libvirt를 통해 수행한다.

오해 3: Pod Running이면 guest도 이미 정상이다

그렇지 않다. launcher 준비, libvirt 연결, domain define, agent 초기화는 별도 단계다.

디버깅 힌트

  • launcher Pod는 살아 있는데 guest가 안 뜨면 command server readiness와 libvirt 연결을 본다.
  • guest agent 관련 정보가 비면 launcher 쪽 poller와 event notifier를 본다.
  • 종료가 오래 걸리면 guest shutdown 전달과 grace period를 함께 본다.
  • migration 이상이면 PrepareMigrationTarget, MigrateVMI, FinalizeVirtualMachineMigration 경로를 분리해서 본다.

마무리

virt-launcher는 KubeVirt에서 가장 실행에 가까운 레이어다. 이 Pod는 VM을 위한 격리 경계를 제공하고, 그 내부의 launcher 프로세스는 libvirt 연결, command server, event monitor, guest agent poller를 세팅한다. 그 위에서 LibvirtDomainManager가 QEMU domain lifecycle을 제어한다. 결국 "VM이 Pod 위에서 돈다"는 말의 가장 구체적인 실체는 바로 이 launcher Pod 내부 실행 구조다.

다음 글에서는 launcher가 받은 VMI spec가 어떤 변환 과정을 거쳐 libvirt domain XML과 실제 guest 장치 구성으로 바뀌는지 보겠다.

virt-launcher, libvirt, QEMU: Where and How the Actual VM Process Runs

Introduction

Now let's look at KubeVirt's most critical execution point. virt-controller creates Pods, and virt-handler coordinates on the node. So where does the actual VM run? The answer is inside the virt-launcher Pod.

More precisely, the virt-launcher Pod is an execution shell for a single VM, and within it libvirt and QEMU actually run the guest. cmd/virt-launcher/virt-launcher.go and pkg/virt-launcher/virtwrap/manager.go are the center of this execution layer.

How to Understand the virt-launcher Pod

Many beginner explanations end with "virt-launcher launches the VM." But a more accurate explanation is:

  • The virt-launcher Pod provides namespace and cgroup boundaries needed for VM execution.
  • The virt-launcher process inside the Pod prepares a command server and domain manager.
  • A libvirt connection is created.
  • A QEMU domain is defined and executed.
  • Events, guest agent, and stats continue to be monitored afterward.

In other words, virt-launcher is not a simple container entrypoint but the local runtime of the VM execution control plane.

What cmd/virt-launcher/virt-launcher.go Does

Looking at this entry point file, the initialization sequence is quite clear.

1. Register libvirt Event Implementation

At process start, libvirt.EventRegisterDefaultImpl() is called. This is needed for subsequent domain event monitoring.

2. Initialize Various Directories

initializeDirs prepares various directories needed for the guest runtime, including cloud-init, ignition, container disk, hotplug disk, secret, config map, and downward metrics.

An important fact is already visible here: VM booting is not simply launching a single QEMU binary but goes together with preparation work organizing multiple disk sources and metadata channels.

3. Create libvirt Connection

createLibvirtConnection basically connects to qemu:///system, and in non-root mode uses the session URI. In other words, virt-launcher doesn't simply link the libvirt library -- it attaches as a client to the actual libvirt control plane to define domains.

4. Start Command Server

startCmdServer launches a unix socket-based command server. virt-handler calls the domain manager on the launcher side through this socket.

5. Start Domain Event Monitoring

startDomainEventMonitoring runs the libvirt event loop and starts the notifier. Guest state changes, termination, and agent connectivity are reflected here.

Why a Command Server Is Needed

This is a key point for understanding KubeVirt's architecture. virt-handler is a node agent, but QEMU is inside the launcher Pod. The two processes are in different isolation boundaries, so they cannot make direct function calls. Therefore KubeVirt places a command server and client layer.

The flow is roughly:

  1. virt-handler reconciles a VMI.
  2. Sends a request to the socket inside the launcher Pod.
  3. The command server calls a DomainManager method.
  4. LibvirtDomainManager defines, starts, pauses, or migrates the domain through the libvirt API.

Thanks to this architecture, cluster node-side orchestration and Pod-internal hypervisor control are separated.

The Responsibility Scope Shown by the DomainManager Interface

Looking at the DomainManager interface in pkg/virt-launcher/virtwrap/manager.go, it becomes clear that the launcher is not just responsible for boot:

Key methods include:

  • SyncVMI
  • PauseVMI
  • UnpauseVMI
  • KillVMI
  • DeleteVMI
  • MigrateVMI
  • PrepareMigrationTarget
  • FinalizeVirtualMachineMigration
  • HotplugHostDevices
  • GetDomainStats
  • GuestPing

In other words, the launcher is a local hypervisor adapter responsible for the entire guest lifecycle.

What libvirt Does vs What QEMU Does

These two are often spoken of as one lump, but their roles differ.

libvirt

  • Provides domain definition and management API
  • Maintains XML-based domain model
  • Provides migration API
  • Provides stats and event collection interfaces

QEMU

  • Actual guest CPU, memory, disk, NIC emulation
  • Execution engine combined with KVM
  • virtio device implementations

KubeVirt generally uses libvirt as the primary control interface, letting libvirt internally manage QEMU.

What Readiness and Socket Switching Mean

markReady renames the uninitialized socket name to the actual socket name. This small action is important because it allows virt-handler to distinguish whether the launcher is still initializing or is actually ready to receive commands.

In other words, the launcher does not assume "VM control ready" just because the Pod is Running. Command plane readiness exists separately.

Why Guest Agent and Event Loop Are Also in the Launcher

The responsibility of handling guest internal information also lies heavily with the launcher. Agent poller, guest info, filesystem, user, and time sync functions are visible in the manager.go interface and virt-launcher.go initialization path.

This is natural. The guest agent is ultimately closest to the guest communication channel exposed by libvirt or QEMU. It makes more sense for the launcher to handle this rather than the node agent virt-handler accessing it directly.

Why the Pod Might Not Die Immediately on Termination

As docs/components.md also mentions, when Kubernetes attempts to terminate the virt-launcher Pod, if the guest has not yet shut down, the launcher forwards the signal to the guest side and waits for graceful shutdown as long as possible.

This behavior is very important. Otherwise, Pod termination would be almost identical to guest forced termination, increasing the risk of data corruption. KubeVirt reveals at this point that Pod lifecycle and VM lifecycle are not exactly the same.

Common Misconceptions

Misconception 1: virt-launcher Is Just a Regular Container Without Sidecars

No. This Pod is the VM execution boundary, and internally contains libvirt event loop, command server, and guest agent related logic.

Misconception 2: QEMU Is Directly Launched by virt-handler

No. virt-handler calls the launcher's command server, and the actual domain definition and execution is performed by the domain manager inside the launcher through libvirt.

Misconception 3: If Pod Is Running, the Guest Is Already Normal

Not necessarily. Launcher preparation, libvirt connection, domain definition, and agent initialization are separate stages.

Debugging Hints

  • If the launcher Pod is alive but the guest isn't starting, check command server readiness and libvirt connection.
  • If guest agent related information is empty, check the launcher-side poller and event notifier.
  • If termination takes long, check guest shutdown delivery and grace period together.
  • For migration issues, check the PrepareMigrationTarget, MigrateVMI, and FinalizeVirtualMachineMigration paths separately.

Conclusion

virt-launcher is the layer closest to execution in KubeVirt. This Pod provides isolation boundaries for the VM, and the launcher process inside sets up libvirt connection, command server, event monitor, and guest agent poller. On top of that, LibvirtDomainManager controls the QEMU domain lifecycle. Ultimately, the most concrete substance of the phrase "VM runs on a Pod" is this launcher Pod's internal execution structure.

In the next article, we will look at what transformation process the VMI spec received by the launcher goes through to become libvirt domain XML and actual guest device configuration.