TT Lab
요금제

한국어

시작하기
블로그

블로그

KubeVirt 소스 코드 읽기 4: virt-handler — 노드에서 VMI 를 맞추기

상태: 초안 (2026-10-17), 소스 + 읽기 전용 실측. 기준은 KubeVirt v1.9.0(커밋 79d34c17)입니다. 실측은 작은 클러스터의 virt-handler·virt-launcher 로그와 VMI status 를 읽은 것입니다.

KubeVirt 소스 코드 읽기 4: virt-handler

목차

1. 직관: 원하는 것과 실제의 차이를 노드에서 메운다

3편의 VMI 컨트롤러는 파드가 준비되면 VMI 에 노드 이름을 적고 손을 뗍니다. 그 뒤로 VM 을 실제로 켜고 끄는 일은 그 노드의 virt-handler 몫입니다. virt-handler 도 컨트롤러이지만 비교 대상이 특이합니다.

둘을 견주어 「끈다 · 지운다 · 맞춘다」 중 하나를 고르고, 결과를 다시 VMI 의 status 에 적습니다. 그리고 파드 밖에서만 할 수 있는 일(다른 컨테이너의 디스크를 붙이기, 파드의 네트워크 네임스페이스에 장치 만들기)을 맡습니다. virt-launcher 의 특권을 작게 두려는 설계이고, 리뷰 안내서에도 「노드 수준의 특권 작업은 virt-launcher 가 아니라 virt-handler 에」 라는 원칙이 있습니다(8편).

2. 그림

virt-handler — 노드 한 대의 VMI 를 맞추는 데몬 두 가지를 본다: 클러스터의 VMI(바라는 것)와 파드 안의 도메인(실제) 노드 · virt-handler (DaemonSet) VMI 인포머 라벨 kubevirt.io/nodeName = 이 노드만 도메인 인포머 notify 서버로 받은 이벤트 + 300초마다 resync 작업 큐 → execute(key) → sync() shutdown 지울 때 delete 정리할 때 update 시작 · 변경 handleStartingVMI() — 파드 밖에서만 할 수 있는 일 컨테이너 디스크를 launcher 쪽에 붙이고 확인 파드 네트워크 네임스페이스에 브리지 · tap 만들기 /dev/kvm 같은 장치 파일 소유자 맞추기 virt-launcher 파드 cmd 서버 (gRPC) …/sockets/launcher-sock SyncVirtualMachine notify 클라이언트 도메인 이벤트를 handler 로 보낸다 syncVirtualMachine() 결과는 updateVMIStatus() 가 VMI status 에 적는다 (phase · interfaces · volumeStatus · conditions) 실측 타임라인(같은 VM, 로그의 시각): +0.000s 「Scheduled · Domain does not exist」 — handler 가 넘겨받음 +0.006s 파드 네트워크 설계(nmstate) — k6t-eth0 브리지 +0.863s launcher 의 notify 파이프 연결 +1.103s 「Scheduled · Domain status: Running」
그림: virt-handler 의 두 인포머, sync 의 세 갈래, virt-launcher 와의 통신

3. 코드 경로

3.1 두 인포머

cmd/virt-handler/virt-handler.go 의 Run()(:209)이 조립합니다.

두 인포머가 같은 키(네임스페이스/이름)를 큐에 넣으므로, VMI 가 바뀌어도 도메인이 바뀌어도 같은 execute() 가 돕니다.

3.2 이벤트는 어떻게 파드 밖으로 나오나

virt-launcher 안의 notify 클라이언트는 자기 파일시스템의 /var/run/kubevirt/domain-notify-pipe.sock 에 붙습니다. 이 소켓은 launcher 가 만든 것이 아니라 virt-handler 가 파드 안에 심어 둔 것입니다. pipe.InjectNotify() (notify-server/pipe/pipe.go:54)가 파드의 마운트 안에 듣는 소켓을 만들고, 들어온 연결을 handler 자신의 notify 서버로 잇습니다(Proxy(), :104). notify 서비스는 HandleDomainEvent 와 HandleK8sEvent 두 RPC 뿐입니다 (notify.proto:6). launcher 는 handler 의 소켓 경로를 알 필요도, 볼 권한도 없습니다.

반대 방향(handler → launcher)은 launcher 가 여는 cmd 서버입니다. handler 는 kubelet 의 파드 폴더를 /pods 로 마운트해 두고 /pods/<파드 UID>/volumes/kubernetes.io~empty-dir/sockets/launcher-sock (cmd-client/client.go:191)에 gRPC 로 붙습니다. Cmd 서비스(cmd.proto:6)에는 SyncVirtualMachine·ShutdownVirtualMachine·KillVirtualMachine·GetDomain·Ping 등 36개 RPC 가 있습니다. handler 와 launcher 의 판이 다를 수 있어(업그레이드 중) 이 proto 를 고치는 PR 은 옛 launcher 와도 말이 통하는지를 꼭 봅니다.

3.3 execute — 넘겨받을지 말지

execute()(vm.go:321)는 대부분 「처리하지 않을 이유」 를 고릅니다.

남은 경우만 sync() 로 갑니다.

3.4 sync — 세 갈래

sync()(vm.go:1433)는 먼저 VMI 와 도메인의 상태를 한 줄로 남깁니다(:1459).

c.logger.Object(vmi).Infof("VMI is in phase: %v | Domain status: %v, reason: %v", vmi.Status.Phase, domain.Status.Status, domain.Status.Reason)

출처: kubevirt/kubevirt v1.9.0 pkg/virt-handler/vm.go 1459행(Apache-2.0). 그다음 shouldShutdown·shouldDelete·shouldUpdate 를 계산하고, 주석이 밝힌 순서대로 하나를 고릅니다. 지우기 때문에 끄는 일 → 이미 꺼진 것 정리 → 스펙 변경과 첫 시작입니다 (:1571 부근의 switch). 마지막에 늘 updateVMIStatus() (:1097)로 도메인에서 읽은 것을 VMI status 에 적습니다. 상(phase), 인터페이스 목록, 게스트 OS 정보, 볼륨 상태, AgentConnected 같은 조건이 여기서 채워집니다.

3.5 맞추기 — 파드 밖에서만 할 수 있는 준비

shouldUpdate 갈래는 processVmUpdate()(:2256) → vmUpdateHelperDefault()(:1933)로 갑니다. 아직 Running 이 아닌 VMI 면 handleStartingVMI()(:2043)가 시작 전 준비를 합니다. 하나라도 준비가 안 되면 키를 1초 뒤로 미루고 돌아갑니다.

  1. 컨테이너 디스크. 디스크 이미지를 품은 컨테이너(volumeroot)가 준비됐는지 보고(ContainerDisksReady), 그 컨테이너의 파일을 launcher 의 container-disks 폴더에 붙입니다(MountAndVerify, container-disk/mount.go:246). 서로 다른 컨테이너의 파일시스템을 잇는 일이라 파드 안에서는 할 수 없습니다.
  2. 핫플러그 볼륨을 붙이고 준비를 기다립니다.
  3. 네트워크. setupNetwork()(controller.go:363)가 파드의 네트워크 네임스페이스에 들어가 브리지와 tap 장치를 만듭니다. 바라는 모양을 nmstate 형식으로 그린 뒤 적용합니다(5편 그림).
  4. 장치 소유자. setupDevicesOwnerships()(controller.go:320)가 /dev/kvm 등의 소유자를 비루트 QEMU(107)가 쓸 수 있게 맞춥니다.

준비가 끝나면 syncVirtualMachine()(:2123)이 SMBIOS·메모리 풍선 주기 같은 클러스터 설정을 옵션에 담아 client.SyncVirtualMachine(vmi, options) (cmd-client/client.go:332)를 부릅니다. VMI 는 JSON 으로 통째로 넘어가고, 그 뒤는 5편의 virt-launcher 몫입니다.

4. 실측: 넘겨받아 Running 이 되기까지

2026-10-13 에 다시 만들어진 VMI 하나(3편의 그 VM)의 시작을 virt-handler 와 virt-launcher 로그로 맞춰 보았습니다. 시각은 로그의 UTC 이고 「+」 는 virt-handler 가 처음 넘겨받은 줄에서부터 잰 시간입니다.

+초 누가 pos 내용
−0.337 launcher virt-launcher.go:85 Marked as ready(cmd 서버가 뜸)
0.000 handler vm.go:1461 VMI is in phase: Scheduled | Domain does not exist
+0.006 handler netpod.go:187·244 현재·바라는 파드 네트워크(브리지 k6t-eth0)
+0.863 handler pipe.go:116 Accepted new notify pipe connection
+0.864 launcher manager.go:1651 Domain defined.
+1.057 launcher manager.go:1612 Domain started.
+1.060 launcher server.go:269 Synced vmi
+1.103 handler vm.go:1459 VMI is in phase: Scheduled | Domain status: Running
+1.136 handler vm.go:1459 VMI is in phase: Running | Domain status: Running

handler 가 넘겨받은 뒤 1.1초 만에 VMI 가 Running 이 됐습니다. 네트워크 준비에서 notify 파이프 연결까지 0.86초가 가장 깁니다. launcher 쪽의 도메인 정의와 시작은 합쳐 0.2초입니다(5편).

5분 resync. 같은 VM 의 handler 로그에서 그 뒤 줄들은 …:03:25, …:08:25, …:13:25 처럼 정확히 300초마다 찍혀 있었습니다 (2026-10-16 하루치를 읽음). 3.1절의 defaultDomainResyncPeriodSeconds = 300 이 이벤트가 없어도 도메인을 다시 묻는 것입니다.

status 에 남은 것. 그 VMI 의 status 에는 interfaces[0].infoSource: domain, guest-agent, guestOSInfo(게스트가 보고한 OS 이름과 커널), 조건 AgentConnected(시작 8초 뒤)가 있었습니다. 앞의 둘은 handler 가 도메인과 게스트 에이전트에서 읽어 적은 것입니다.

5. 한계

티티 한마디. virt-handler 를 읽을 땐 「이 일은 왜 launcher 가 아니라 handler 가 하지?」 를 물어보세요. 거의 늘 「파드 밖을 만져야 해서」 예요. 특권은 노드 데몬 하나에 모으고, VM 마다 뜨는 파드는 가볍게 두는 거죠!

참고

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

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

댓글

아직 댓글이 없습니다.

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