상태: 실측 (2026-10-31). 1편과 같은 중첩 클러스터(TT Lab 실습 VM 세 대, KubeVirt v1.9.0)에서 잰 값입니다. 정책 시험은 조건마다 1–3번, drain 은 장면마다 한 번, 동시 한도는 한도마다 한 번입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 원인을 확인하지 못한 해석은 「추정」 으로 표시합니다.
목차
1. 메모리를 계속 쓰는 VM
1편의 VM 은 아무 일도 하지 않았습니다. 실제 VM 은 메모리를 씁니다. 사전 복사는 「보내는 사이 바뀐 페이지를 다시 보낸다」 는 방식이라, 게스트가 고치는 속도가 보내는 속도와 비슷하면 끝나지 않습니다. 이것을 직접 만들어 보았습니다.
- 게스트(Fedora, 1Gi) 안에서
stress-ng --vm 1 --vm-bytes 512M --vm-method write64 --vm-keep -t 0를 돌렸습니다. 같은 512MB 를 계속 다시 씁니다. - 정책은
MigrationPolicy로 하나씩만 두고 옮겼습니다. 정책 없는 기본값은 대역폭 무제한,completionTimeoutPerGiB: 150,progressTimeout: 150,maxDowntimeMs: 900, autoConverge·postCopy 끔이었습니다(VMI 에 실제로 적용된 값).
| 시험 | 부하 | 정책 | 결과 | 걸린 시간 | 게스트 멈춤 | 서비스 끊김(새 연결 / 유지 연결) |
|---|---|---|---|---|---|---|
| light-1 | 128MB | 없음 | Failed Live migration has been aborted |
302초 | 없음(원본에서 계속 돎) | |
| heavy-1 | 512MB | 없음 | Failed | 301초 | 없음 | |
| heavy-2 | 512MB | bandwidthPerMigration: 32Mi |
Failed | 301초 | 없음 | |
| heavy-3 | 512MB | allowAutoConverge: true |
Succeeded | 33초 | 566ms | 2807ms / 2805ms, 끊김 |
| uffd-3 | 512MB | allowAutoConverge: true |
Succeeded | 36초 | 547ms | 1604ms / 1603ms, 끊김 |
| heavy-5 | 512MB | 32Mi + autoConverge | Succeeded | 43초 | 433ms | 4287ms / 4384ms, 끊김 |
| heavy-4 | 512MB | allowPostCopy: true |
Failed Postcopy is not supported: Userfaultfd not available: Operation not permitted |
2초 | 없음 | |
| heavy-6 | 512MB | 32Mi + postCopy | Failed(같은 이유) | 1초 | 없음 | |
| uffd-1 | 512MB | allowPostCopy: true, 노드 vm.unprivileged_userfaultfd=1 |
Succeeded(PostCopy) | 303초 | 미실측 | 2204ms / 3006ms, 끊김 |
| uffd-4 | 512MB | 같음 | Succeeded(PostCopy) | 304초 | 미실측 | 6214ms / 6670ms, 끊김 |
| uffd-2 | 512MB | 32Mi + postCopy, 노드 설정 켬 | Succeeded(PostCopy) | 306초 | 미실측 | 6211ms / 6213ms, 끊김 |
1.1 왜 기본값으로는 끝나지 않나
진행 중인 libvirt 작업 정보를 5초마다 읽어 보니, 수렴하지 못한 실행들은 300초 동안 사전 복사를 184–244번 되풀이했습니다. 남은 양은 15–90MiB 사이를 오갔고, 이것을 보내는 데 걸릴 예상 멈춤은 1,782–1,913ms 였습니다. 목표(300ms 에서 시작해 최대 900ms)에 끝내 닿지 못했습니다. 게스트가 더럽히는 속도는 초당 약 19,000–23,000쪽(74–92MiB/s)으로, 보내는 속도와 거의 같았습니다. 매 반복 같은 양이 다시 남는 것입니다.
128MB 만 계속 써도 실패했습니다. 그동안 게스트는 원본에서 멀쩡히 돌았기 때문에 서비스 끊김은 없었습니다. 대신 VM 이 옮겨지지 않으니 그 노드를 비울 수 없습니다(2절).
1.2 정책별로 정리하면
| 정책 | 512MB 부하에서 | 대가 |
|---|---|---|
| 없음(기본) | 수렴하지 못하고 약 300초 뒤 포기 | 노드를 비울 수 없다 |
| 대역폭 제한 32Mi | 마찬가지로 포기 | 제한을 걸었는데 실제 전송은 제한을 넘었다(아래) |
| autoConverge | 33–43초 만에 성공(3/3), 멈춤 433–566ms | 게스트 CPU 를 점점 늦춰 더럽히는 속도를 떨어뜨린다. 게스트 처리량이 얼마나 떨어지는지는 재지 않았다(미실측) |
| postCopy | 성공(3/3)이지만 사전 복사 제한 시간을 다 쓴 뒤에야 PostCopy 로 넘어감. 서비스 끊김 2.2–6.7초 | 노드에 vm.unprivileged_userfaultfd=1 필요. 사후 복사 중 원본이나 대상이 죽으면 게스트를 잃는다(문서 기준) |
autoConverge 는 이름 그대로 「수렴하게 만드는」 장치입니다. 진행이 막히면 QEMU 가 게스트 vCPU 를 조금씩 붙잡아 더럽히는 속도를 낮춥니다. 이번 부하에서는 이것 하나로 결과가 실패에서 30초대 성공으로 바뀌었습니다.
postCopy 는 반대 방향입니다. 일단 게스트를 대상에서 돌리고, 아직 오지 않은 페이지는 게스트가 건드리는 순간 원본에서 끌어옵니다. 리눅스의 userfaultfd 로 그 순간을 잡기 때문에 노드 커널이 허락해야 하고, 우분투의 기본값은 0 이었습니다(중첩 워커도 0). 켜면 성공했지만, KubeVirt 는 사전 복사 제한 시간을 다 쓴 뒤에야 PostCopy 로 넘겼습니다. 그리고 사후 복사 동안 게스트의 메모리가 두 노드에 나뉘어 있으므로 어느 쪽이든 죽으면 게스트를 잃습니다. 바쁜 VM 이 있는 환경이라면 autoConverge 부터 켜 보는 것이 이 실측에서 나온 순서입니다.
1.3 대역폭 제한이 지켜지지 않았다
bandwidthPerMigration: 32Mi 를 걸었을 때 설정은 모든 층에 들어가 있었습니다. VMI 의 적용 설정에 32Mi, QEMU 의 max-bandwidth: 33554432,
virsh migrate-getspeed 32 가 모두 찍혔습니다. 그런데 QEMU info migrate 의 처리량은 630.76Mbps(약 75MiB/s)였고, 10초 동안 처리한 양도
17.316GiB 에서 18.040GiB 로 약 73.5MiB/s 였습니다. 전송은 multifd 8채널로 나갔습니다. 제한이 없을 때(86–90MiB/s)와 거의 같습니다.
multifd 와 속도 제한의 관계 때문인지, KubeVirt 쪽 설정 때문인지는 multifd 를 끈 대조 시험을 해야 가를 수 있습니다(미실측). 그래서 지금은
「제한을 걸면 회선이 보호된다」 를 믿지 않고, 실제 전송량을 virsh domjobinfo 로 확인하는 것을 권합니다. 참고로 사용자 가이드에 적힌 기본 대역폭
64MiB/s 는 옛값이고 v1.9.0 의 실제 기본값은 무제한(0)이었습니다. 문서를 고치는 PR 은 이미 열려 있습니다.
1.4 정책을 거는 법
MigrationPolicy 는 네임스페이스나 VMI 라벨로 대상을 고릅니다. 바쁜 VM 이 모인 네임스페이스에 autoConverge 만 켜는 예입니다.
apiVersion: migrations.kubevirt.io/v1alpha1
kind: MigrationPolicy
metadata:
name: busy-vms-auto-converge
spec:
allowAutoConverge: true
selectors:
namespaceSelector:
kubernetes.io/metadata.name: busy-vms
적용됐는지는 마이그레이션 뒤 VMI 의 status.migrationState.migrationConfiguration 과 status.migrationState.migrationPolicyName 으로 확인합니다.
2. 노드 비우기(drain)
라이브 마이그레이션을 가장 많이 쓰는 때는 노드를 비울 때입니다. VM 에 evictionStrategy: LiveMigrate 를 두면 kubectl drain 이 VM 을 끄지 않고
옮깁니다. 이 경로를 네 장면으로 시험했습니다.
| 장면 | 무엇을 | 결과 |
|---|---|---|
| A | 512MB 부하가 걸린 VM(LiveMigrate)이 있는 노드를 drain(제한 600초) | drain 실패(600.9초). 웹훅이 Evacuation in progress: Eviction triggered evacuation of VMI … 로 거절하며 축출 마이그레이션을 만들지만, 수렴하지 못해 약 5분마다 포기하고 다시 만든다 |
| B | 디스크가 RWO 인데 evictionStrategy: LiveMigrate 인 VM 이 있는 노드를 drain(제한 120초) |
drain 실패(120.8초). VMI … is configured with an eviction strategy but is not live-migratable 로 계속 거절 |
| C | A·B 뒤 노드를 다시 스케줄 가능으로(uncordon) | 축출이 멈추지 않았다. 되돌린 뒤에도 축출 마이그레이션이 두 번 더 생겼고, 부하를 끄자 마지막 것이 성공해 VM 이 되돌린 노드를 떠났다 |
| D | 부하 끈 VM 셋(LiveMigrate)과 한 대(evictionStrategy None)가 있는 노드를 drain(제한 400초) | 두 대는 22초·13초 만에 옮겨졌다. None 인 VM 은 7.4초에 꺼지고 17.9초에 다른 노드에서 새 VMI 로 떴다(재부팅). 나머지 한 대는 받는 노드의 메모리 요청이 98% 라 Insufficient memory, 5분 뒤 실패하고 다시 시도하다 drain 400초 시간 초과 |
2.1 PDB 가 아니라 웹훅이다
drain 중에 kubectl get pdb -A 는 No resources found 였습니다. 사용자 가이드의 노드 유지보수 문서는 「VMI 마다 PodDisruptionBudget 을 만든다」 고
적고 있지만, v1.9.0 은 virt-launcher 파드의 축출 요청을 승인 웹훅(virt-launcher-eviction-interceptor.kubevirt.io)으로 받습니다. 웹훅은 축출을
거절하면서 VMI 에 축출 표시(status.evacuationNodeName)를 남기고, virt-controller 가 그것을 보고 축출 마이그레이션(kubevirt-evacuation-…)을 만듭니다.
drain 은 5초마다 다시 축출을 요청하고, 마이그레이션이 끝나 원본 파드가 사라지면 그때 통과합니다. 이 문서의 어긋남은 고치는 문서 PR 을 올렸습니다(user-guide#1051).
그래서 drain 이 끝나느냐는 그 노드의 모든 VM 이 옮겨지느냐에 달려 있습니다.
- 옮길 수 없는 VM(RWO 디스크 등)에
LiveMigrate를 두면 drain 이 끝나지 않습니다(장면 B). 옮길 수 없는 VM 은 None 이나LiveMigrateIfPossible로 둡니다(LiveMigrateIfPossible은 API 문서 기준이고 이번에 재지 않았습니다). - 수렴하지 못하는 VM 이 하나 있으면 drain 이 그 VM 을 기다리며 멈춥니다(장면 A). 1절의 autoConverge 가 여기서도 필요합니다.
- 받는 노드에 메모리 여유가 있어야 합니다(장면 D). 워커가 둘이면 한쪽을 비울 때 그 위의 VM 전부가 다른 쪽에 들어가야 합니다. 들어가지 못하면 대상 파드가 5분 동안 Pending 이다가 실패하고, 다시 만들고, 또 실패합니다.
- 축출 마이그레이션도 동시 한도를 따릅니다. 장면 D 에서 두 대가 한도(노드당 2)를 쓰는 동안 나머지 한 대는 12초 동안 Pending 이었습니다.
- drain 중 옮겨진 VM 의 서비스 끊김은 새 연결 3607ms, 유지 연결 3606ms·끊김이었습니다. 1편의 기본 시험보다 조금 길었습니다.
2.2 되돌려도 남는 축출 표시
장면 C 가 가장 뜻밖이었습니다. drain 이 실패해서 노드를 uncordon 했는데, VMI 의 status.evacuationNodeName 이 그대로 남아 있었습니다. virt-controller 는
이 표시를 보고 축출 마이그레이션을 계속 만들었고(두 번 uncordon 했는데 그 뒤로 두 번 더 생김), 부하를 끄자 그중 하나가 성공해 VM 이 방금 되돌린 노드를
떠났습니다. 「일단 drain 을 멈추고 되돌리자」 고 해도 VM 은 계속 이사 중인 셈입니다.
이 표시를 지우는 명령이 virtctl evacuate-cancel 입니다. 그런데 v1.9.0 virtctl 로 실행하면 이렇게 실패했습니다.
Internal error occurred: the server rejected our request due to an error in our request
서버(virt-api)는 「지금 축출 중인 노드 이름이 요청과 같을 때만 지운다」 는 조건을 거는데, v1.9.0 virtctl 이 요청에 노드 이름을 넣지 않아 생기는 버그입니다.
같은 요청에 본문 {"evacuationNodeName": "<노드>"} 를 넣어 보내면 200 으로 지워졌습니다. 이미 고쳐져서(kubevirt#17008, release-1.9 백포트 #18864)
v1.9.1 이상의 virtctl 을 쓰면 됩니다. 서버가 v1.9.0 이어도 virtctl 만 새것이면 됩니다.
3. 동시 마이그레이션 한도
cirros VM 여섯 대를 한 노드에 모은 뒤 VMIM 여섯 개를 한꺼번에 만들었습니다. KubeVirt CR 의 parallelOutboundMigrationsPerNode 만 바꿨고 클러스터 한도
(parallelMigrationsPerCluster: 5)는 그대로 두었습니다.
| 노드당 한도 | 여섯 대 모두 끝날 때까지 | 동시에 Running 최대 | 한 건의 Running → Succeeded |
|---|---|---|---|
| 1 | 33.9초 | 1 | 2–6초 |
| 2(기본) | 22.3초 | 2 | 3–4초 |
| 4 | 23.2초 | 4 | 8–9초 |
한도를 2에서 4로 올려도 빨라지지 않았습니다. 한 노드가 내보낼 수 있는 대역(약 90MiB/s)을 나눠 쓰니 한 건이 그만큼 느려집니다. 작은 VM 이 여럿이면 1 에서 2로 올릴 때의 이득(34초 → 22초)이 크고, 그 위는 회선이 상한입니다.
마이그레이션 전용 망(spec.configuration.migrations.network)은 재지 못했습니다(미실측). 중첩 노드에 NIC 가 둘뿐이고 Multus 가 없어 별도 망을 만들지
못했습니다. 이 구성에서는 마이그레이션, NFS 디스크, 서비스 트래픽이 모두 같은 약 800Mbps 선을 나눠 썼습니다. 운영에서 VM 이 크거나 많다면 전용 망이
먼저이고, 한도는 그다음입니다.
# KubeVirt CR 의 일부. 비워 두면 위 기본값(클러스터 5, 노드당 2, 대역폭 무제한)
spec:
configuration:
migrations:
parallelMigrationsPerCluster: 5
parallelOutboundMigrationsPerNode: 2
4. 이 측정의 한계
숫자를 가져다 쓰기 전에 조건을 확인해 주십시오.
- 중첩 클러스터입니다. KubeVirt 를 VM 안에서 다시 돌렸으므로 절대 시간은 물리 노드보다 느립니다. 특히 멈춤 시간과 속도는 물리 노드에서 더 좋을 가능성이 큽니다.
- 회선이 약 800Mbps 입니다. 10GbE 이상이라면 같은 부하라도 사전 복사가 수렴할 수 있습니다. 1절의 「기본값으로 실패」 는 「더럽히는 속도 ≈ 회선」 인 이 조건의 결과입니다.
- VM 이 작습니다. 기본 시험은 1Gi 게스트 하나, 조건 시험은 128Mi cirros 입니다. 수십 GiB 게스트의 시간은 재지 않았습니다(미실측).
- 반복 수가 적습니다. 기본 시험 5회, 정책 시험 1–3회, drain 장면과 동시 한도는 한 번씩입니다. 분포가 아니라 사례로 읽어 주십시오.
- 탐침은 시도마다 200ms 제한이라, 마이그레이션이 없을 때도 가끔 짧은 실패가 찍혔습니다. 가장 긴 연속 구간만 보았습니다.
- 재지 않은 것: autoConverge 가 게스트 처리량에 주는 영향, postCopy 의 게스트 멈춤 시간(대상의 작업 정보가 남지 않았다), 볼륨 마이그레이션, masquerade 가 아닌 네트워크 바인딩, 마이그레이션 전용 망, multifd 를 끈 대역폭 대조.
5. 정리: 라이브 마이그레이션을 믿기 전에
| 점검 | 이유(1·2편의 실측) |
|---|---|
| 디스크가 RWX, 네트워크가 masquerade | 아니면 웹훅이 VMIM 생성부터 거절 |
| CPU 모델을 노드들이 함께 내는 이름 모델로 | host-model 은 True 로 보여도 5분 뒤 실패 |
| 바쁜 VM 에 autoConverge | 기본값은 128MB 쓰기에도 300초 뒤 포기, autoConverge 는 33–43초 |
| postCopy 를 쓰려면 노드 sysctl 부터 | 없으면 1–2초 만에 실패 |
| 대역폭 제한은 실제 전송량으로 확인 | 32Mi 를 걸었는데 75MiB/s 가 나갔다 |
| 옮길 수 없는 VM 은 evictionStrategy None | LiveMigrate 면 drain 이 끝나지 않는다 |
| 받는 노드의 메모리 여유 | 98% 면 5분마다 실패·재시도 |
| v1.9.1 이상 virtctl | uncordon 뒤에도 남는 축출 표시를 지우려면 |
| 클라이언트 재연결 | 서비스 끊김은 게스트 멈춤보다 몇 배 길다 |
직접 해 보고 싶다면 TT Lab 의 실습 코스 「KubeVirt 라이브 마이그레이션」(사이트 경로 /course/kubevirt-live-migration)이 이 측정과 같은 VM 셋, 같은 판으로
준비돼 있습니다. 공통 CPU 모델 고르기부터 옮기고 멈춤 시간 읽기, RWO 거절 읽기, drain 까지 여덟 단계입니다.
티티 한마디. 짐을 싸는 동안 계속 어지르는 친구가 있으면 이사가 영영 안 끝나요. 그럴 땐 그 친구를 살살 느리게(autoConverge) 해 주면 30초 만에 끝나요. 그리고 이사를 취소했다고 끝난 게 아니에요. 문에 붙은 「이사 중」 쪽지(evacuationNodeName)를 떼지 않으면 짐이 혼자 나가 버린답니다!