ステータス:実測(2026-10-31)。 TT Lab の実習用 VM 3 台の中に Kubernetes と KubeVirt v1.9.0 を構築したネスト環境のクラスターで測定した数値です。 基本テストは 5 回、ブロックされる条件と CPU モデルのテストは条件ごとに 1 回ずつです。数値はすべて、マイグレーション 1 回ごとに残したローデータ(VMIM のフェーズ記録、libvirt のタスク情報、プローブの記録)から抽出了しました。作成ルール:実測データを基本とし、測定できなかったものは「未計測」、原因を確認できなかった解釈は「推定」と表示します。
目次
- 1. 結論から
- 2. ライブマイグレーションが行うこと
- 3. 測定環境と方法
- 4. 基本の実測:負荷のない 1Gi VM での 5 回のテスト
- 5. 移行できない条件
- 6. CPU モデル:どのノード間で移行できるのか
- 7. まとめ
1. 結論から
ノードの修正やカーネルのアップデートを行うには、そのノード上の VM をどこかへ移行させる必要があります。KubeVirt ではこれを ライブマイグレーションで行います。ゲストを停止させずに メモリを別のノードへコピーしたあと、ごく短時間停止させてから新しいノードで処理を継続します。ドキュメントだけを読むと「ほとんど途切れない」ように聞こえますが、実際に移行させてみると 条件がかなり多くありました。
- ゲストの停止とサービスの途切れは異なる数値です。 負荷のない 1Gi VM での 5 回のテストにおいて、libvirt が測定した停止時間は中央値で 343ms でしたが、Service を経由した新しい接続は 中央値で 2.4 秒間応答がなく、維持していた TCP 接続は 5 回すべて切断されました。
- 移行できない VM が思った以上にたくさんあります。 ディスクが RWO だとストレージが NFS であってもブロックされ、local-path・hostDisk・Pod ネットワークの bridge もブロックされます。
containerDisk は移行できますが、ノードにイメージを
ctrのように受け取らせていると、15 分待った末に失敗します。 LiveMigratable: Trueは CPU を考慮しません。 デフォルト値の host-model は、移行元のノードの CPU 機能を移行先にすべて要求するため、同じ名前と思われる ノード間であっても機能の 1 つや 2 つの違いによって 5 分後に失敗しました。混在するハードウェア環境では、 名前付きの CPU モデルを指定しておくことが正解でした。
メモリを酷使する VM、ノードのドレイン(drain)、同時マイグレーションの制限については パート 2で取り上げます。
2. ライブマイグレーションが行うこと
KubeVirt において VM とは、ノード上の Pod(virt-launcher)内で稼働する QEMU プロセスです。Pod は別のノードへ「移行」させることができないため、KubeVirt は
移行先のノードに新しい Pod をもう 1 つ立ち上げ、ゲストの状態をそちらにコピーしたあと、元の Pod を終了させる という方式で移行します。このプロセスを開始するオブジェクトが
VirtualMachineInstanceMigration(略称:VMIM)です。 virtctl migrate もこのオブジェクトを 1 つ作成するだけです。
- 移行先 Pod の準備。 virt-controller が移行先のノードに virt-launcher Pod を新規作成します。この Pod 内の QEMU はゲストなしで「流入するマイグレーション」を 待ち受けます。実測では、VMIM を作成してから移行先 Pod の準備が整うまでに 2~4 秒かかりました。 移行先 Pod をスケジュールできない場合、ここで停止します(5・6節)。
- 事前コピー(pre-copy)。 移行元の QEMU がゲストのメモリを送信します。ゲストはその間も稼働し続けるため、すでに送信したページが再度書き換えられます。そのため、 1回目のラウンドでは全体を、次のラウンドからはその間に変更された(ダーティになった)ページのみを再送信します。0 で埋められたページはほとんど送信されないため、1.016GiB のゲストにおいて実際に 送信されたデータ量は 456MiB でした。反復回数は 3~4 回、速度は約 88MiB/s でした。
- 停止と最終コピー(stop-and-copy)。 残りのデータ量を目標時間内に送信できるようになると、ゲストを停止し、残りのページと CPU 状態を送信します。このタイミングが
ゲストが実際に停止した時間であり、libvirt が
Total downtimeとして記録する時間です。KubeVirt v1.9 は目標値を 300ms から開始し、処理が追いつかない場合は少しずつ 引き上げます(上限はmaxDowntimeMsデフォルトで 900ms)。 - 移行先での再開。 移行先の QEMU がゲストの処理を継続し、元の Pod は終了します。 ディスクは移行しません。 2 つの Pod が同じ PVC を同時にアタッチするためですが、そのため PVC が複数のノードから同時にアタッチ可能な RWX(ReadWriteMany)である必要があります(5節)。
メモリを送信するデータは、2 つのノードの virt-handler Pod 間、すなわち Pod ネットワーク経由で流れます。別途マイグレーション専用のネットワークを割り当てない限り、サービス用のトラフィックと 同じ回線を共有します。
3. 測定環境と方法
| 項目 | 値 |
|---|---|
| 外部 | TT Lab の実習用 VM 3 台(各 4 vCPU・4GiB、Ubuntu 24.04、ネスト仮想化)。コントロールプレーン 1 台、ワーカー 2 台。2 台のワーカーは CPU 世代が異なる物理サーバー上に存在する |
| 内部の Kubernetes | 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 1 つ(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(Pod ネットワーク) |
測定方法は 3 つあります。
- VMIM のフェーズ:0.5 秒ごとに VMIM のフェーズ(Pending → Scheduling → Scheduled → PreparingTarget → TargetReady → Running → Succeeded)を記録しました。
- libvirt による測定値:完了後、移行先の Pod から
virsh domjobinfo --completedを用いて経過時間、停止時間(Total downtime)、送信量、反復回数を読み取りました。 実際に適用された設定は、VMI のstatus.migrationState.migrationConfigurationから読み取りました。 - 外部から見た途切れ:ゲスト内にエコーサーバー(
ncat -lk 7777 --exec /bin/cat)を立ち上げ、コントロールプレーンから VMI を選択する ClusterIP Service に対して 20ms ごとにリクエストを送りました。毎回新しく確立する接続(新規接続)と、1 つの接続を継続して使用する接続(維持接続)の 2 種類があり、試行ごとに 200ms 以内に応答がない場合は失敗とみなします。「途切れ」とは、最も長い連続失敗区間を指します。
構築にかかった時間も記録しておきます。VM 3 台の準備に 276 秒、csi-driver-nfs に 52 秒、KubeVirt Available に 122 秒、CDI Available に 91 秒、Fedora のベースディスクの取得に 193 秒かかりました。CDI の取得時に一度引っかかりましたが、csi-driver-nfs に mountPermissions を指定しないとボリュームフォルダーが root 0755 になり、インポート Pod(uid 107)が permission denied で無限に再起動してしまいます。NFS は fsGroup による所有権の変更を行わないため、StorageClass に mountPermissions: "0777" を指定して解決しました。
4. 基本の実測:負荷のない 1Gi VM での 5 回のテスト
Fedora VM(1Gi、RWX、masquerade、 evictionStrategy: LiveMigrate)を 2 つのワーカー間で 5 回往復させました。マイグレーションポリシーは指定していません(デフォルト値)。CPU は 2 つのワーカーが両方とも提供できる名前付きモデル 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 | 2406ms、5/5 切断 |
VMIM を作成してから完了するまでに約 10 秒かかり、そのうち実際のコピー(libvirt 経過時間)は 6.4 秒でした。残りは移行先 Pod のスケジュールと起動にかかる時間(1~2秒)、移行先の準備(0~2秒)、完了後の状態クリーンアップの時間です。
4.1 なぜ 0.34 秒の停止が 2.4 秒の切断になるのか
KubeVirt のデフォルトネットワークバインディングである masquerade 環境では、ゲストは Pod の背後に隠れています。外部から見えるアドレスは virt-launcher Pod の IP であり、Pod 内で NAT を介してゲストに転送されます。しかし、2 節で見たようにマイグレーションは 新しい Pod を作成して移行する 作業であるため、完了するとゲストを包む Pod が切り替わり、Pod の IP も変更されます。
- 維持していた接続が切断される理由。 もともとポッドAを経由して結ばれていたTCP接続のNAT状態は、ポッドAのネットワークの中にあります。ポッドAが終了すると、その状態も消滅します。5回すべて維持接続がタイムアウトで切断され、再構築された接続は成功しました。
- 新しい接続もしばらく行き場がない理由(推測)。 ServiceのエンドポイントがポッドAからポッドBに移行し、各ノードのルールに反映されるまでは、停止が終了した後も新しい接続は届きません。プローブの記録で失敗区間は、VMIMがSucceededに見える約1秒前に始まり、そのあと1〜3秒続きました。停止時間の0.34秒を除いた残りがエンドポイントの切り替えに使われた時間と見ていますが、エンドポイントが切り替わった時刻は個別に記録していません(未計測)。
そのため、「ライブマイグレーションだから途切れない」と信じて設計してはいけません。 クライアントが切断された接続を再確立し、数秒単位の失敗をリトライで 耐えられるように作っておく必要があります。マスカレードではないバインディング(Multusでアタッチした別途のネットワークなど)では同じ値の測定はしていません(未計測)。
5. 移行できない条件
VMIの状態には LiveMigratable 条件があります。Falseの場合はその理由が併記され、VMIMを作ろうとすると承認Webhookが作成自体を拒否します。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ディスク、ポッドネットワークに 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)として処理されます。しかし、
閉域網のようにイメージをあらかじめノードに配置しておく環境には落とし穴がありました。
- ノードで
ctrによりプルするか、ctr images importによりインポートしたイメージは、CRIにおいてストレージダイジェスト(repoDigests)が空になり、元のポッドのimageIDがsha256:<설정 digest>のみ残ります。 - KubeVirtはターゲットポッドのイメージを「元と同じダイジェスト」に固定しようと、
imageIDのsha256値をそのまま付加して<저장소>@sha256:<값>を作成します。この値はストレージに存在しない参照であるため、ターゲットポッドはNotFound·ImagePullBackOffに陥り、VMIMは 15分4秒 の間Schedulingにとどまった末に失敗しました。 - 同じイメージを
crictl pull(CRIパス)で再度プルしてrepoDigestsを埋めた後、新しく起動したVMは移行に成功しました。
この挙動はアップストリームにIssuesとして報告しました(kubevirt#19694)。それまでの間は、事前に配置するイメージを crictl pull でプルするか、インポート後に crictl inspecti でrepoDigestsが満たされていることを確認するのが安全です。
5.3 失敗までに要する時間
ブロックされる箇所によって待機時間が大きく異なります。Webhookが拒否する条件は即座にわかりますが、 ターゲットポッドをスケジューリングできない場合は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のノードラベラーが付与します。
2台のワーカーがCPU世代の異なるインテルホスト(family 6のmodel 151と170)上にあったため、同一条件のままテストすることができました。ノードラベラーは2台のワーカーに
同一の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 1つのみ |
Succeeded、世代の異なるノードへ移行 |
| どのノードも提供できないモデル | cpu-model.node.kubevirt.io/<그 모델> |
VMI自体がSchedulingにとどまる(VMが起動しない) |
ここから読み取れるポイントは3つあります。
LiveMigratableはCPUの互換性を計算しません。 host-modelのVMはTrueでしたが移行先がなく、ターゲットポッドが5分間Pending状態になったのち、unschedulable pod … timeout period exceededで終了しました。- host-modelのターゲット条件は「元と同じhost-model名 + 元ノードの必須機能すべて」です。 名前が同じであっても、機能が1つでも不足していると移行できません。そしてhost-model名は「最も近い名前モデル」であるだけで、世代を教えてくれません。世代の異なる2台のホストが同じ名前を受け取りました。
- host-passthroughはベンダーのみを確認します。 そのため、世代の異なるノードにも移行できました。今回のゲストは移行先に存在しない機能(
ddpd-u·rfds-noなど)を認識した状態だったため、その機能を実際に使用するプログラムであれば危険でした。passthroughはCPUが完全に一致するノード同士でのみ使用すべきです。
この関係はユーザーガイドに整理されていなかったため、該当セクションを追加するドキュメントPRを提出しました(user-guide#1052).
6.1 混在したハードウェアではどうなるか
同じルール(上記のhost-modelセレクターが実測で検証したルール)を、インテル製ノード4台とAMD製ノード1台が混在する実際のノードのラベルに適用して計算してみました。ノード名を除外してペアの種類ごとにまとめると以下のようになります。
| ノードペア | ラベルから見た差異 | host-model | 名前モデル | host-passthrough |
|---|---|---|---|---|
| CPUが同一の2台 | cpu-feature ラベルが完全に一致 |
可能 | 可能 | 可能(安全) |
| host-model名のみ同一の2台 | 元の必須機能のうち移行先にないものが1〜3個(rfds-clear が1つ、または ddpd-u·rfds-no·vmx-any-errcode) |
両方でブロック | 共通モデルとして可能 | スケジュールは可能だが危険 |
| 世代の異なるインテル | 一方にはAVX-512系が12個、もう一方には waitpkg·bhi-ctrl など8個がない |
両方でブロック | 共通モデルとして可能 | スケジュールは可能だが危険 |
| インテルとAMD | 相互に15〜22個(仮想化拡張から vmx·svm まで異なる) |
ブロック | 共通モデルが古いもののみ | ブロック(ベンダーが異なる) |
- host-modelではCPUが完全に一致するペアのみ可能です。 5台のうちそのようなペアは1つだけでした。「同じ製品群だから大丈夫だろう」という予測は通用せず、差異はセキュリティ緩和策やプリフェチ制御といった目立ちにくい機能ビットが1、2個あるかどうかでした。
- 名前モデルを指定すると範囲が広がります。 インテル製4台がともにサポートできる名前モデルは31個あり、その中には
Skylake-Client-v4·Skylake-Client-noTSX-IBRS·Broadwell-noTSX-IBRS·Haswell-noTSX-IBRSのようにAVX2を維持するモデルがあります。AMDを含めた5台がともにサポートできるモデルはIvyBridge·SandyBridge·Westmere·Nehalem·Penryn(各-v1を含む)たった10個しかないため、AVX2が失われます。ベンダーが混在している場合、「移行可能性」と「ゲストパフォーマンス」をトレードオフすることになります。 - 実際にはハードウェア以外にも考慮すべき点があります。メモリの少ないノードや、デバイスパススルーで紐付けられたノードは受け入れ側から除外すべきであり、そうすると使用可能なペアはさらに減少します。
この表はラベルの計算に基づいています。ネストされたテストで検証されたのは「host-model名のみ同一の2台」の1行のみです。
6.2 自環境で確認する方法
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 기능 같음"
2つのノードがともにサポートできる名前モデルは、 cpu-model.node.kubevirt.io/ で始まるラベルの積集合です。混在環境で移行するVMには、そのうちの1つを
spec.domain.cpu.model に記述するか、KubeVirt CRの spec.configuration.cpuModel でクラスターのデフォルト値を変更します。
7. まとめ
| 確認すべき項目 | 理由(本記事での実測) |
|---|---|
| PVCのアクセスモードがRWXであるか | RWOの場合、ストレージがNFSであってもWebhookが拒否する |
| ポッドネットワークのバインディングがマスカレード(またはマイグレーション可能なプラグイン)であるか | bridgeの場合は拒否される |
containerDiskイメージを ctr で配置していないか |
存在しないダイジェストをターゲットがプルしようとして15分後に失敗する |
| CPUモデルを指定しているか | host-modelはTrueに見えても5分後に失敗し、passthroughは危険な状態で成功する |
| クライアントが再接続・リトライを実装しているか | 停止時間が0.34秒であっても新規接続は2.4秒間途切れ、既存接続は切断される |
本記事の実測は、TT Labの実習コース「KubeVirt ライブマイグレーション」(サイトパス /course/kubevirt-live-migration)でそのまま追体験できます。同じVMセットとバージョンを用い、共有ストレージ、KubeVirt、共通CPUモデルの選択、移行および停止時間の測定、RWO拒否の確認、ドレインの手順を進めます。KubeVirtのアーキテクチャが初めての場合は、 学生VMが起動するまでと KubeVirtのソースコードリーディング
シリーズが基礎となります。
ティティの一言。 引っ越し中にお客さんがドアの前で待っていた時間は0.34秒でしたが、お客さんは2.4秒間インターホンが鳴らなかったと言っていました。住所(ポッドIP)が変わったからです!そして引っ越しトラックは「同じ地区(CPU機能)」にしか行きません。名前だけ似ている地区は、到着して初めて道がないことに気づくので、あらかじめ地図を合わせておいてください。