Skip to content
Published on

virt-handler deep dive: kubelet 옆의 VM 노드 에이전트는 무엇을 하는가

Authors

들어가며

virt-controller가 cluster-wide orchestration이라면, virt-handler는 각 노드에서 실제 VM lifecycle을 이어받는 node-local agent다. Kubernetes 세계에 비유하면 kubelet과 성격이 조금 비슷하다. 물론 kubelet을 대체하는 것은 아니지만, VMI라는 새로운 워크로드 타입에 대해 kubelet 옆에서 동작하는 KubeVirt 전용 실행 조율자라고 보면 된다.

docs/components.mdvirt-handler를 host마다 하나씩 필요한 DaemonSet으로 설명한다. 실제 코드인 pkg/virt-handler/vm.go를 보면 이 설명이 꽤 정확하다는 것을 알 수 있다.

virt-handler가 꼭 노드마다 있어야 할까

클러스터 전체 상태만 보고는 VM을 제대로 맞출 수 없는 작업이 많기 때문이다.

  • 실제 libvirt domain 상태 읽기
  • launcher Pod 내부 QEMU와 통신
  • node 로컬 디바이스, cgroup, SELinux, mount 상태 확인
  • 네트워크와 디스크 attach의 host-side 보조 작업
  • migration source와 target 전환

이런 작업은 node-local context가 있어야 한다. 따라서 KubeVirt는 각 노드에 virt-handler를 배치한다.

vm.go가 보여주는 핵심 책임

pkg/virt-handler/vm.goVirtualMachineController 구조체를 보면 이 컴포넌트가 단순 상태 watcher가 아니라는 것이 드러난다. 내부에는 다음 책임이 함께 들어 있다.

  • launcher client 관리
  • container disk mounter
  • hotplug volume mounter
  • network setup 연동
  • cgroup manager 연동
  • device manager
  • heartbeat
  • migration proxy 연동
  • domain informer와 VMI informer 핸들링

virt-handler는 "VMI status를 조금 갱신하는 controller"가 아니라, 노드에서 VM 실행 환경을 맞추는 종합 에이전트다.

동작 흐름을 큰 그림으로 보면

virt-handler가 하는 일을 간단히 순서화하면 다음과 같다.

  1. 이 노드에 배정된 VMI를 watch한다.
  2. 해당 VMI에 대응하는 launcher Pod와 domain 상태를 본다.
  3. 필요한 스토리지와 디바이스, 네트워크 준비를 수행한다.
  4. launcher Pod 내부의 libvirt 제어 plane과 통신한다.
  5. domain 상태를 다시 VMI status로 반영한다.
  6. 종료, 재시작, migration 같은 상태 전환을 조율한다.

여기서 중요한 것은 virt-handler가 Kubernetes API와 libvirt world 사이의 번역 통로라는 점이다.

launcher Pod와는 어떻게 통신하는가

vm.golauncherClients, cmd-client, handler-launcher-com 계층을 사용한다. 즉 virt-handler는 launcher Pod 안에 들어 있는 command server와 통신해 VM 명령을 전달한다.

대표적으로 다음 종류의 요청이 이 경로를 탄다.

  • VM 실행 또는 동기화
  • shutdown
  • pause 또는 unpause
  • migration 시작
  • migration finalize
  • guest 관련 조회

중요한 것은 virt-handler가 QEMU를 직접 fork하는 것이 아니라, launcher 내부 command server를 통해 libvirt domain manager를 조작한다는 점이다. 이 계층 분리가 있어야 Pod sandbox 내부 실행과 node-side orchestration이 깔끔하게 나뉜다.

상태 반영은 왜 이렇게 복잡한가

virt-handler는 VMI informer와 domain informer 둘 다 본다. 이유는 간단하다.

  • VMI는 cluster가 원하는 상태를 담는다.
  • domain은 libvirt가 현재 알고 있는 실제 실행 상태를 담는다.

이 둘이 다르면 virt-handler가 중간에서 맞춰야 한다. 예를 들어:

  • VMI는 Running이어야 하는데 domain이 없음
  • domain은 떠 있는데 VMI status가 아직 반영 안 됨
  • migration 중 source와 target에서 domain 감지가 다름

이런 어긋남을 맞추는 것이 virt-handler의 핵심이다.

네트워크와 스토리지에서의 역할

많은 사람이 네트워크와 스토리지는 Pod가 이미 받았으니 virt-handler가 별로 할 게 없다고 생각한다. 실제로는 그렇지 않다.

네트워크

vm.gonetsetup, domainspec, network/vmispec과 연결된다. 이는 virt-handler가 guest NIC 구성, domain network spec, host-side wiring의 일부를 책임진다는 뜻이다.

특히 migration target 쪽에서는 target Pod가 실제로 guest를 받을 준비가 되었는지 판단해야 한다. 이때 네트워크 준비 상태는 매우 중요하다.

스토리지

containerDiskMounter, hotplugVolumeMounter, host-disk, container-disk 같은 의존성을 보면 virt-handler가 node-side mount 보조와 attach 경로를 꽤 많이 책임진다는 것을 알 수 있다.

즉 Pod에 볼륨이 attach되었다고 해서 곧바로 guest 디스크 준비가 끝나는 것은 아니다. guest가 이해할 디스크 경로와 hotplug 상태까지 이어줘야 한다.

device manager와 heartbeat가 왜 들어 있을까

이 부분은 virt-handler가 단순 watcher가 아니라는 강한 증거다.

device manager

하드웨어 가속과 특수 디바이스는 적절한 권한과 노출이 필요하다. deviceManagerController는 이런 자원 노출과 상태를 관리한다.

heartbeat

노드별 capability와 health 상태를 cluster에 반영해야 scheduling과 실행이 맞아떨어진다. heartbeat는 이 역할을 담당한다.

virt-handler는 노드가 "VM을 실행할 수 있는 상태인가"를 지속적으로 보고하는 역할도 가진다.

cgroup과 권한 조정까지 한다

vm.go에는 cgroup v2 unified mode에서 device permission 처리에 대한 주석이 있다. 여기서 알 수 있는 것은 KubeVirt가 단순히 Go 코드 몇 줄로 VM을 띄우는 것이 아니라, 리눅스 호스트의 자원 제약과 보안 모델을 세심하게 다룬다는 점이다.

예를 들어 device probing 과정에서 권한이 잘못되면 QEMU startup 자체가 깨질 수 있다. 그래서 virt-handler는 런타임과 커널 차이를 흡수하는 완충층이 된다.

migration target controller까지 별도로 있다

pkg/virt-handler/migration-target.go를 보면 migration target 전용 controller가 따로 존재한다. 이는 migration이 단순한 "상태 복사"가 아니라 target node에서 별도의 준비와 감지가 필요하다는 뜻이다.

target 측에서는:

  • target domain 감지
  • domain ready timestamp 기록
  • 자원 조정
  • finalize 호출

같은 작업이 필요하다. 즉 migration은 virt-controller만의 책임도 아니고 virt-launcher만의 책임도 아니다. virt-handler가 중간 orchestration을 강하게 맡는다.

자주 하는 오해

오해 1: virt-handler는 단순 status reporter다

아니다. launcher와 통신하고, 디바이스와 볼륨을 다루고, migration과 network 준비를 조율한다.

오해 2: node-local state는 거의 중요하지 않다

아니다. VM 실행은 결국 host 커널과 하이퍼바이저에 닿는다. node-local context 없이는 정확한 실행과 복구가 어렵다.

오해 3: kubelet만 있으면 충분하다

Pod lifecycle만 관리하는 kubelet과, VM domain lifecycle까지 이해하는 virt-handler는 역할이 다르다.

디버깅에서 먼저 볼 것

  • launcher Pod는 Running인데 guest가 안 보이면 virt-handler 로그를 본다.
  • migration target이 준비되지 않으면 migration-target 쪽 이벤트와 상태를 본다.
  • hotplug 또는 device 문제면 node-local mount, cgroup, device manager 쪽 흔적을 본다.
  • VMI status와 실제 domain 상태가 어긋나면 domain informer 반영 흐름을 본다.

마무리

virt-handler는 KubeVirt의 node execution plane 중심이다. 이 컴포넌트는 VMI와 domain을 함께 보고, launcher Pod와 통신하고, node-local 네트워크, 스토리지, 디바이스, migration 상태를 조율한다. Kubernetes가 Pod를 노드에 놓아 주면, 그 이후 VM다운 lifecycle을 만들어 내는 것은 상당 부분 virt-handler의 몫이다.

다음 글에서는 이 virt-handler가 명령을 전달하는 반대편, 즉 virt-launcher, libvirt, QEMU 스택이 Pod 내부에서 어떻게 조직되는지 살펴보겠다.