TT Lab
블로그

한국어

시작하기
블로그

블로그

KubeVirt 라이브 마이그레이션 실측 1: 게스트는 0.34초 멈췄는데 서비스는 2.4초 끊겼다

상태: 실측 (2026-10-31). TT Lab 실습 VM 세 대 안에 쿠버네티스와 KubeVirt v1.9.0 을 세운 중첩 클러스터에서 잰 값입니다. 기본 시험은 다섯 번, 막히는 조건과 CPU 모델 시험은 조건마다 한 번씩입니다. 숫자는 모두 마이그레이션 한 건마다 남긴 원자료(VMIM 단계 기록, libvirt 작업 정보, 탐침 기록)에서 뽑았습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 원인을 확인하지 못한 해석은 「추정」 으로 표시합니다.

목차

1. 결론부터

노드를 고치거나 커널을 올리려면 그 노드의 VM 을 어딘가로 옮겨야 합니다. KubeVirt 는 이것을 라이브 마이그레이션으로 합니다. 게스트를 끄지 않고 메모리를 다른 노드로 복사한 뒤 아주 짧게 멈췄다가 새 노드에서 이어서 돌립니다. 문서만 읽으면 「거의 끊기지 않는다」 로 들리는데, 직접 옮겨 보니 조건이 꽤 많았습니다.

메모리를 바쁘게 쓰는 VM, 노드 비우기(drain), 동시 마이그레이션 한도는 2편에서 봅니다.

2. 라이브 마이그레이션이 하는 일

KubeVirt 에서 VM 은 노드 위의 파드(virt-launcher) 안에서 도는 QEMU 프로세스입니다. 파드는 다른 노드로 「옮길」 수 없으므로, KubeVirt 는 대상 노드에 새 파드를 하나 더 띄우고, 게스트의 상태를 그쪽으로 복사한 뒤, 원래 파드를 끝내는 방식으로 옮깁니다. 이 과정을 시작하는 객체가 VirtualMachineInstanceMigration(줄여서 VMIM)입니다. virtctl migrate 도 이 객체를 하나 만들 뿐입니다.

라이브 마이그레이션: 게스트를 켠 채로 옮기기 부하 없는 1Gi VM, 5회 중앙값(실측) 원본 노드 대상 노드 ① 대상 파드 준비 · 2–4초 게스트 실행 중 virt-launcher 파드 A 새 파드 B 기동 빈 QEMU 가 기다림 ② 사전 복사 · 3–4회 반복, 약 88MiB/s 게스트 계속 실행 바뀐 페이지에 표시 받은 페이지 채움 아직 멈춰 있음 모두 합쳐 456MiB ③ 멈추고 마지막 복사 · 343ms 게스트 멈춤 남은 페이지 + CPU 상태 마지막 조각 받음 남은 것 ④ 대상에서 재개 파드 A 끝남 게스트 실행 파드 IP 는 새것 디스크는 옮기지 않는다 두 파드가 같은 RWX PVC(공유 스토리지)를 붙인다 ②는 1회차에 메모리 전체(0 으로 찬 페이지는 거의 안 보냄), 다음 회차부터는 그사이 바뀐 페이지만 보낸다.
그림: 라이브 마이그레이션의 네 단계: 대상 파드 준비, 사전 복사, 멈추고 마지막 복사, 대상에서 재개
  1. 대상 파드 준비. virt-controller 가 대상 노드에 virt-launcher 파드를 새로 만듭니다. 이 파드의 QEMU 는 게스트 없이 「들어올 마이그레이션」 을 기다립니다. 실측에서는 VMIM 을 만든 뒤 대상 파드가 준비되기까지 2–4초가 걸렸습니다. 대상 파드를 스케줄할 수 없으면 여기서 멈춥니다(5·6절).
  2. 사전 복사(pre-copy). 원본 쪽 QEMU 가 게스트 메모리를 보냅니다. 게스트는 그동안 계속 돌기 때문에 이미 보낸 페이지를 다시 고칩니다. 그래서 1회차에는 전체를, 다음 회차부터는 그사이 바뀐(더러워진) 페이지만 다시 보냅니다. 0 으로 찬 페이지는 거의 보내지 않아서 1.016GiB 게스트에서 실제로 보낸 양은 456MiB 였습니다. 반복은 3–4회, 속도는 약 88MiB/s 였습니다.
  3. 멈추고 마지막 복사(stop-and-copy). 남은 양을 목표 시간 안에 보낼 수 있게 되면 게스트를 멈추고 남은 페이지와 CPU 상태를 보냅니다. 이때가 게스트가 실제로 멈춘 시간이고 libvirt 가 Total downtime 으로 적습니다. KubeVirt v1.9 는 목표를 300ms 에서 시작해 진행이 막히면 조금씩 올립니다(상한 maxDowntimeMs 기본 900ms).
  4. 대상에서 재개. 대상 QEMU 가 게스트를 이어서 돌리고 원본 파드는 끝납니다. 디스크는 옮기지 않습니다. 두 파드가 같은 PVC 를 동시에 붙이기 때문인데, 그래서 PVC 가 여러 노드에서 함께 붙을 수 있는 RWX(ReadWriteMany)여야 합니다(5절).

메모리를 보내는 데이터는 두 노드의 virt-handler 파드 사이, 즉 파드 네트워크로 흐릅니다. 따로 마이그레이션 전용 망을 주지 않으면 서비스 트래픽과 같은 선을 나눠 씁니다.

3. 측정 환경과 방법

항목 값
바깥 TT Lab 실습 VM 세 대(각 4 vCPU·4GiB, 우분투 24.04, 중첩 가상화). 컨트롤 플레인 1, 워커 2. 워커 두 대는 CPU 세대가 다른 물리 서버 위에 있다
안쪽 쿠버네티스 kubeadm 1.36.4, containerd 2.3.5, Flannel v0.28.9
KubeVirt·CDI KubeVirt v1.9.0(virt-launcher 안 libvirt 11.10.0, QEMU 10.1.0, multifd 8채널), CDI v1.66.0
공유 스토리지 컨트롤 플레인의 커널 NFS + csi-driver-nfs 4.13.4, StorageClass 하나(RWX). RWO 비교용 local-path-provisioner v0.0.32
게스트 Fedora 44(1 vCPU, 메모리 1Gi, 루트 6GiB RWX, masquerade), 조건 시험은 cirros(128Mi)
워커 사이 회선 iperf3 10초 × 3: 790–799Mbps(세션망), 764–772Mbps(파드 네트워크)

재는 방법은 셋입니다.

한 번 세우는 데 걸린 시간도 적어 둡니다. VM 셋 준비 276초, csi-driver-nfs 52초, KubeVirt Available 122초, CDI Available 91초, Fedora 바탕 디스크 가져오기 193초였습니다. CDI 가져오기에서 한 번 걸렸는데, csi-driver-nfs 에 mountPermissions 를 주지 않으면 볼륨 폴더가 root 0755 라 가져오기 파드(uid 107)가 permission denied 로 끝없이 재시작합니다. NFS 는 fsGroup 으로 소유권을 바꿔 주지 않으므로 StorageClass 에 mountPermissions: "0777" 를 주어 풀었습니다.

4. 기본 실측: 부하 없는 1Gi VM 다섯 번

Fedora VM(1Gi, RWX, masquerade, evictionStrategy: LiveMigrate)을 두 워커 사이에서 다섯 번 왕복했습니다. 마이그레이션 정책은 두지 않았습니다(기본값). CPU 는 두 워커가 함께 낼 수 있는 이름 모델 Skylake-Client-v4 로 고정했습니다. 왜 그래야 했는지는 6절에서 봅니다.

회 VMIM 생성 → Succeeded libvirt 경과 게스트 멈춤 보낸 양 속도 반복 새 연결 끊김 유지 연결
1 11.4초 6680ms 413ms 459MiB 86.2MiB/s 4 2406ms 2205ms, 끊김
2 9.0초 6652ms 490ms 456MiB 87.1MiB/s 4 2205ms 2005ms, 끊김
3 10.3초 6262ms 343ms 455MiB 89.3MiB/s 3 2607ms 2605ms, 끊김
4 9.1초 6400ms 338ms 456MiB 88.3MiB/s 4 1204ms 1203ms, 끊김
5 10.1초 6208ms 342ms 456MiB 90.1MiB/s 4 2806ms 3007ms, 끊김
중앙값 10.1초 6400ms 343ms 456MiB 88.3MiB/s 4 2406ms 2205ms, 5/5 끊김

VMIM 을 만든 뒤 끝날 때까지 약 10초가 걸렸고, 그중 실제 복사(libvirt 경과)는 6.4초였습니다. 나머지는 대상 파드를 스케줄하고 띄우는 시간(1–2초), 대상 준비(0–2초), 끝난 뒤 상태를 정리하는 시간입니다.

4.1 왜 0.34초 멈춤이 2.4초 끊김이 되나

게스트는 0.34초 멈췄는데 서비스는 2.4초 끊겼다 부하 없는 1Gi VM 5회, 중앙값(범위), 실측 0초 1초 2초 3초 게스트 멈춤 libvirt 가 잰 값 343ms (338–490) 새 연결 무응답 Service 너머 탐침 2.4초 (1.2–2.8) 왜 다른가: masquerade 에서는 게스트가 파드 뒤에 숨는다 옮기기 전 클라이언트 Service 파드 A IP a → NAT → 게스트 TCP 연결을 맺어 둠 옮긴 뒤 클라이언트 Service 파드 B IP b → NAT → 게스트 엔드포인트가 B 로 바뀌어야 닿음 파드 A 가 사라지면 A 에 맺은 연결도 사라진다 유지하던 TCP 연결은 5회 모두 끊겼고, 다시 맺으면 됐다
그림: 게스트 멈춤 343ms 와 새 연결 무응답 2.4초의 비교, masquerade 에서 파드가 바뀌는 그림

KubeVirt 의 기본 네트워크 바인딩인 masquerade 에서 게스트는 파드 뒤에 숨어 있습니다. 바깥에서 보이는 주소는 virt-launcher 파드의 IP 이고, 파드 안에서 NAT 로 게스트에 넘깁니다. 그런데 2절에서 본 것처럼 마이그레이션은 새 파드를 만들어 옮기는 일이라, 끝나고 나면 게스트를 감싼 파드가 바뀌고 파드 IP 도 바뀝니다.

그러니 「라이브 마이그레이션이라 끊기지 않는다」 를 믿고 설계하면 안 됩니다. 클라이언트가 끊긴 연결을 다시 맺고, 몇 초짜리 실패를 재시도로 견디게 만들어 두어야 합니다. masquerade 가 아닌 바인딩(Multus 로 붙인 별도 망 등)에서 같은 값을 재지는 않았습니다(미실측).

5. 옮겨지지 않는 조건

VMI 의 상태에는 LiveMigratable 조건이 있습니다. False 면 그 이유가 함께 나오고, VMIM 을 만들려고 하면 승인 웹훅이 생성 자체를 거절합니다. cirros VM 으로 조건마다 실제로 만들어 보았습니다.

VM 디스크·네트워크 LiveMigratable 이유와 메시지(그대로) VMIM
RWO NFS StorageClass 의 RWO PVC False DisksNotLiveMigratable: PVC … is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode) 생성 거절
local-path local-path RWO PVC False 같은 메시지 생성 거절
hostDisk containerDisk + hostDisk False DisksNotLiveMigratable: cannot migrate VMI with non-shared HostDisk 생성 거절
bridge RWX 디스크, pod 네트워크에 bridge False InterfaceNotLiveMigratable: cannot migrate VMI which does not use masquerade or a migratable plugin to connect to the pod network 생성 거절
containerDisk(ctr 로 받음) 노드에 ctr images pull 로 미리 받아 둔 이미지 True Scheduling 에서 15분 4초 뒤 Failed
containerDisk(crictl 로 받음) 같은 이미지를 crictl pull 로 다시 받은 뒤 True Succeeded

5.1 RWO 는 저장소가 NFS 여도 막힌다

KubeVirt 는 저장소가 실제로 여러 노드에서 붙을 수 있는지를 보지 않고 PVC 의 접근 모드만 봅니다. 저장소가 NFS 라 원래 공유가 되더라도 PVC 를 RWO 로 만들었다면 옮길 수 없습니다. 실제 환경을 읽기만으로 점검해 보니 기존 VM 이 바로 이 경우였습니다. 저장소의 StorageProfile 은 RWX 를 낼 수 있는데 VM 을 만드는 코드가 PVC 를 RWO 로 적고 있어서 모두 LiveMigratable: False 였습니다. 옮길 수 있게 하려면 루트 디스크를 RWX PVC 로 다시 만들어야 합니다 (VM 을 끄고 RWX 로 복제해 바꾸거나, v1.9 의 볼륨 마이그레이션). 볼륨 마이그레이션은 이번에 재지 않았습니다(미실측). 접근 모드와 볼륨 모드 이야기는 학생 VM 이 뜨기까지 3편에 더 자세히 있습니다.

5.2 containerDisk 와 ctr 의 함정

containerDisk(디스크를 컨테이너 이미지로 받는 방식)는 옮겨집니다. 쓰기 층을 함께 보내는 블록 마이그레이션(BlockMigration)으로 처리됩니다. 그런데 폐쇄망처럼 이미지를 노드에 미리 넣어 두는 환경에서는 함정이 있었습니다.

이 동작은 업스트림에 이슈로 올렸습니다(kubevirt#19694). 그 전까지는 미리 넣는 이미지를 crictl pull 로 받거나, 반입 뒤 crictl inspecti 로 repoDigests 가 채워졌는지 확인하는 것이 안전합니다.

5.3 실패까지 걸리는 시간

막히는 곳에 따라 기다리는 시간이 크게 다릅니다. 웹훅이 거절하는 조건은 즉시 알 수 있지만, 대상 파드를 스케줄하지 못하면 5분, 대상 파드는 떴는데 이미지를 못 받으면 15분 뒤에야 Failed 가 됩니다. 기다리지 않으려면 VMIM 을 지우면 취소됩니다.

6. CPU 모델: 어느 노드끼리 옮길 수 있나

게스트는 처음 부팅할 때 본 CPU 기능을 계속 쓴다고 믿습니다. 옮겨 간 노드에 그 기능이 없으면 게스트가 죽을 수 있으니, KubeVirt 는 대상 파드에 노드 선택자를 붙여 기능이 맞는 노드에만 스케줄합니다. 노드마다 붙는 CPU 라벨(cpu-model.node.kubevirt.io/…, cpu-feature.node.kubevirt.io/…, cpu-model-migration.node.kubevirt.io/…)은 virt-handler 의 노드 라벨러가 답니다.

두 워커가 CPU 세대가 다른 인텔 호스트(family 6 의 model 151 과 170) 위에 있어서, 같은 조건을 그대로 시험할 수 있었습니다. 노드 라벨러는 두 워커에 같은 host-model 이름을 붙였지만 필수 기능은 달랐습니다. 한쪽에만 rfds-clear, 다른 쪽에만 ddpd-u·rfds-no 가 있었습니다.

CPU 설정 LiveMigratable 대상 파드에 붙은 선택자(실측) 결과
이름 모델 Skylake-Client-v4 True cpu-model.node.kubevirt.io/Skylake-Client-v4 Succeeded
host-model(기본값) True cpu-model-migration.node.kubevirt.io/<원본의 host-model 이름> + 원본 노드의 필수 기능 27개 + 공급사 대상 파드 Unschedulable, 정확히 5분 뒤 Failed
host-passthrough True cpu-vendor.node.kubevirt.io/Intel 하나뿐 Succeeded, 세대가 다른 노드로 옮겨짐
어느 노드도 못 내는 모델 cpu-model.node.kubevirt.io/<그 모델> VMI 자체가 Scheduling 에 머묾(VM 이 뜨지 않음)

여기서 읽을 것이 셋입니다.

  1. LiveMigratable 은 CPU 호환을 계산하지 않습니다. host-model VM 은 True 였지만 옮길 자리가 없었고, 대상 파드가 5분 동안 Pending 이다가 unschedulable pod … timeout period exceeded 로 끝났습니다.
  2. host-model 의 대상 조건은 「원본과 같은 host-model 이름 + 원본 노드의 필수 기능 전부」 입니다. 이름이 같아도 기능 하나가 모자라면 안 됩니다. 그리고 host-model 이름은 「가장 가까운 이름 모델」 일 뿐이라 세대를 알려 주지 않습니다. 세대가 다른 두 호스트가 같은 이름을 받았습니다.
  3. host-passthrough 는 공급사만 봅니다. 그래서 세대가 다른 노드로도 옮겨졌습니다. 이번 게스트는 대상에 없는 기능(ddpd-u·rfds-no 등)을 본 상태였으니, 그 기능을 실제로 쓰는 프로그램이었다면 위험했습니다. passthrough 는 CPU 가 완전히 같은 노드끼리만 써야 합니다.

이 관계는 사용자 가이드에 정리돼 있지 않아, 그 절을 더하는 문서 PR 을 올렸습니다(user-guide#1052).

6.1 섞인 하드웨어에서는 어떻게 되나

같은 규칙(위 host-model 선택자가 실측으로 확인해 준 규칙)을, 인텔 노드 넷과 AMD 노드 하나가 섞인 실제 노드들의 라벨에 적용해 계산해 보았습니다. 노드 이름을 빼고 쌍의 종류로 묶으면 이렇습니다.

섞인 노드: CPU 설정별로 옮길 수 있는 쌍 두꺼운 칸은 중첩 클러스터 실측, 나머지는 노드 라벨로 계산 host-model (기본값) 이름 모델 공통 모델 지정 host- passthrough 같은 CPU 기능 라벨이 같음 ✓ 옮겨짐 ✓ 옮겨짐 ✓ 안전 host-model 이름만 같음, 기능 1–3개 차이 ✗ 5분 뒤 실패 ✓ 옮겨짐 ⚠ 옮겨짐, 위험 다른 세대 인텔 기능 8–15개 차이 ✗ 대상 없음 ✓ 공통 모델로 ⚠ 스케줄됨, 위험 인텔 ↔ AMD 공급사가 다름 ✗ 대상 없음 △ 옛 모델뿐 ✗ 대상 없음 ✗ 대상 파드가 뜨지 못한다. LiveMigratable 은 그래도 True 로 보인다. ⚠ 공급사만 보고 옮긴다. 게스트가 대상에 없는 기능을 보고 있었다. △ 공통 이름 모델이 IvyBridge 이하뿐이라 AVX2 를 잃는다. 다른 세대 인텔끼리의 공통 모델 예: Skylake-Client-v4
그림: 섞인 노드에서 CPU 설정별로 옮길 수 있는 노드 쌍: 같은 CPU, host-model 이름만 같음, 다른 세대 인텔, 인텔과 AMD
노드 쌍 라벨로 본 차이 host-model 이름 모델 host-passthrough
CPU 가 같은 두 대 cpu-feature 라벨이 완전히 같다 된다 된다 된다(안전)
host-model 이름만 같은 두 대 원본 필수 기능 중 대상에 없는 것 1–3개(rfds-clear 하나, 또는 ddpd-u·rfds-no·vmx-any-errcode) 양쪽 다 막힘 공통 모델로 된다 스케줄은 되지만 위험
세대가 다른 인텔 한쪽엔 AVX-512 계열 12개, 다른 쪽엔 waitpkg·bhi-ctrl 등 8개가 없다 양쪽 다 막힘 공통 모델로 된다 스케줄은 되지만 위험
인텔과 AMD 서로 15–22개(가상화 확장부터 vmx·svm 으로 다르다) 막힘 공통 모델이 옛것뿐 막힘(공급사가 다름)

이 표는 라벨 계산입니다. 중첩 시험에서 확인한 것은 「host-model 이름만 같은 두 대」 줄 하나입니다.

6.2 내 노드에서 확인하는 법

두 노드의 CPU 기능 라벨을 비교하면 host-model VM 이 옮겨질지 미리 알 수 있습니다.

diff <(kubectl get node A -o json | jq -r '.metadata.labels|keys[]|select(startswith("cpu-feature."))') \
     <(kubectl get node B -o json | jq -r '.metadata.labels|keys[]|select(startswith("cpu-feature."))') \
  && echo "A·B CPU 기능 같음"

두 노드가 함께 낼 수 있는 이름 모델은 cpu-model.node.kubevirt.io/ 로 시작하는 라벨의 교집합입니다. 섞인 노드에서 옮길 VM 은 그중 하나를 spec.domain.cpu.model 에 적거나, KubeVirt CR 의 spec.configuration.cpuModel 로 클러스터 기본값을 바꿉니다.

7. 정리

확인할 것 이유(이 글의 실측)
PVC 접근 모드가 RWX 인가 RWO 면 저장소가 NFS 여도 웹훅이 거절한다
pod 네트워크 바인딩이 masquerade(또는 마이그레이션 가능한 플러그인)인가 bridge 면 거절한다
containerDisk 이미지를 ctr 로 넣지 않았나 대상이 없는 digest 를 받다가 15분 뒤 실패한다
CPU 모델을 정했나 host-model 은 True 로 보여도 5분 뒤 실패, passthrough 는 위험하게 성공
클라이언트가 재연결·재시도를 하나 멈춤 0.34초여도 새 연결은 2.4초 끊기고 유지 연결은 끊긴다

이 글의 실측은 TT Lab 의 실습 코스 「KubeVirt 라이브 마이그레이션」(사이트 경로 /course/kubevirt-live-migration)에서 그대로 따라 해 볼 수 있습니다. 같은 VM 셋과 같은 판으로 공유 스토리지, KubeVirt, 공통 CPU 모델 고르기, 옮기고 멈춤 시간 읽기, RWO 거절 읽기, drain 순서를 밟습니다. KubeVirt 의 구조가 처음이라면 학생 VM 이 뜨기까지와 KubeVirt 소스 코드 읽기 시리즈가 바탕이 됩니다.

티티 한마디. 이사하는 동안 손님이 문 앞에서 기다린 시간은 0.34초였는데, 손님은 2.4초 동안 초인종이 안 된다고 했어요. 집 주소(파드 IP)가 바뀌었거든요! 그리고 이삿짐 트럭은 「같은 동네(CPU 기능)」 로만 가요. 이름만 비슷한 동네는 도착해서야 길이 없다는 걸 알게 되니, 미리 지도를 맞춰 두세요.

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

댓글

아직 댓글이 없습니다.

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