ステータス:ドラフト(2026-10-17)、ソースコード+読み取り専用の実測値に基づく。 基準はCDI
v1.66.0(コミット938af7c5)とKubeVirtv1.9.0です。実測値はすでに インポートが完了しているDataVolume・PVC・CDIConfig・StorageProfileと、そのPVCを使用するVMのポッド内ディスクを読み取ったものです。
KubeVirtソースコードリーディング 6: CDI
目次
1. イントロダクション:PVCに「書き込む方法」を付加したもの
PVCは空の器です。VMにはOSがインストールされたディスクが必要なため、誰かがイメージを取得して器に書き込む必要があります。 DataVolume は
「このようなPVCを作成し、このソース(HTTP、レジストリ、別のPVC、アップロード)から書き込め」という内容を1つのオブジェクトに記述したCRDであり、 CDI はその処理を行う
コントローラーとワーカーポッドのセットです。KubeVirtとはストレージもリリースサイクルも別々であり、VMの dataVolumeTemplates につながります。VMコントローラーが
DataVolumeを作成し(handleDataVolumes, vm.go:512)、
VMIコントローラーはDataVolumeの準備が整うまでポッドを作成しません(areDataVolumesReady,
datavolumes.go:116)。第3部で確認した
「Delaying pod creation while DataVolume populates」がまさにその箇所です。
2. 図解:インポートのプロセス
3. コードパス
3.1 DataVolumeコントローラー:PVCの形状とサイズを決定する
インポート用のDataVolumeは pkg/controller/datavolume/import-controller.go の syncImport()
(:195)が担当します。
共通部分の syncDvPvcState()(controller-base.go:486)が
作成するPVCの仕様を描画しますが、ここで2つのフィールドが分岐します(renderPvcSpec,
util.go:108).
spec.pvcは記述された内容をそのままコピーします。spec.storageは、空のアクセスモード、ボリュームモード、ストレージクラスを StorageProfile で埋め(pvcFromStorage, :118)、 サイズを ファイルシステムの余分な容量分だけ膨らませます(renderPvcSpecVolumeSize, :308).
。次に、ボリュームポピュレータを使用するかどうかを決定します(shouldUseCDIPopulator,
controller-base.go:1316)。
ストレージクラスがCSIドライバーのものである場合は使用し、DataVolumeのアノテーション cdi.kubevirt.io/storage.usePopulator で無効にすることができます。使用する場合は、ソースを含む
VolumeImportSource CRを作成し(reconcileVolumeImportSourceCR, import-controller.go:348)、
ターゲットPVCの dataSourceRef がそれを指すように設定して作成します。
3.2 ポピュレータ:双子のPVC'に書き込み、PVを移動させる
Kubernetesのボリュームポピュレータの仕様では、 dataSourceRef が付いたPVCをポピュレータが書き込むまで誰もバインドしません。CDIのimportポピュレータは、
ターゲットPVCを直接操作せず、 同じ仕様を持つ双子のPVC'を作成します(createPVCPrime,
populator-base.go:194)。
名前は prime-<목표 PVC UID> であり、即座にバインドされるようにアノテーションを設定し、バックアップ対象から除外するラベルを付与します。
PVC'は従来から存在するimportコントローラー(pkg/controller/import-controller.go)によって監視されます。アノテーションを読み取ってimporterポッドを作成し
(createImporterPod, :531)、
ポッドの状態をPVC'のアノテーションに反映させます。ポピュレータ側( reconcileTargetPVC()
(import-populator.go:154)は
そのアノテーションをターゲットPVCにコピーしながら進捗を報告し、PVC'の処理が完了すると cc.Rebind(...)
(:206を通じて
PVのclaimRefをターゲットPVCに変更して アタッチします。イメージが含まれるPVが丸ごと所有者を変更するため、データのコピーが発生しません。この設計により、ターゲットPVCは最初から最後まで「未バインド → 書き込み完了状態でバインド」の2つの状態しか経由せず、途中で半分満たされたディスクを誰かがアタッチする隙がありません。
3.3 importer:段階的な処理とサイズ調整
importerポッドの本体は pkg/importer/data-processor.go のステートマシンです(ProcessDataWithPause,
:241). Info(ソース形式の確認) →
Transfer…(取得) → Convert(qemu-img convert (raw形式に変換) → Resize → Complete の順序であり、各ステップの関数が次のステップを返します。
サイズは resize()(:294)が決定します。
ファイルシステムボリュームの場合は ResizeImage()(:331が
「要求されたサイズ」と「利用可能なスペース」のうち小さい方に合わせてイメージを拡大しますが、利用可能なスペースが getUsableSpace()
(:396) →
util.GetUsableSpace() です。ブロックボリュームの場合はデバイスのサイズをそのまま使用します。
3.4 サイズを計算する2つの関数
pkg/util/util.go の2つの関数は互いに逆の処理を行います。
func GetRequiredSpace(filesystemOverhead float64, requestedSpace int64) int64 {
alignedSize := RoundUp(requestedSpace, DefaultAlignBlockSize)
spaceWithOverhead := int64(math.Ceil(float64(alignedSize) * (1 + filesystemOverhead)))
return spaceWithOverhead
}
ソース:containerized-data-importer v1.66.0 pkg/util/util.go の232〜241行目からコメントを削除(Apache-2.0)。
GetRequiredSpace、:232 はPVCの要求を作成する際に
(DataVolumeコントローラーが InflateSizeWithOverhead を経由して)、 GetUsableSpace、:223
はイメージサイズを決定する際(importer)に使用します。まとめると以下のようになります。
- PVC要求 = ⌈ 1MiB単位で切り上げ(要求) × (1 + 余分) ⌉
- イメージサイズ = 1MiB単位で切り捨て( ⌈ min(空き容量, PVC要求) ÷ (1 + 余分) ⌉ )
余分な容量はCDIConfigの status.filesystemOverhead において、ストレージクラス別の値があればそれを、なければグローバルな値を使用します
(GetFilesystemOverheadForStorageClass, common/util.go:629)。
グローバルデフォルト値は "0.06"(common.go:226)であり、
アライメント単位の DefaultAlignBlockSize は1MiB(ズレ防止等util.go:32)です。
ブロックモードではサイズを膨らませません(InflateSizeWithOverhead, :1572).
結果として、 spec.storage で10Giを要求すると、ゲストは10GiBのディスクを受け取り、PVCはその分大きくなります。 反対に、 spec.pvc で10Giを要求すると、PVCは正確に10Giとなり、ディスクは6%近く小さくなります。公式ドキュメントには「イメージとファイルシステムのメタデータの両方が収まるだけの大きなPVCを要求する」としか記載されていないため、この計算式をドキュメントに追加するPR(#4318、筆者が作成したドキュメントPR、レビュー中)がオープンされています。
3.5 StorageProfileと複製戦略
StorageProfileは、ストレージクラスごとにCDIが作成するオブジェクトです。主に2つの情報を保持します。
- claimPropertySets — このクラスに適したアクセスモードとボリュームモード。
spec.storageが空欄に残した部分を埋めるために使用されます(3.1)。ソースはプロバイダー (provisioner)名で検索するテーブル(CapabilitiesByProvisionerKey(storagecapabilities.go:39) であり、テーブルにないプロバイダーの場合は空になります。空のクラスにおいてspec.storageにアクセスモードが記述されていない場合、PVCを作成できません。 - cloneStrategy —
reconcileCloneStrategy()(storageprofile-controller.go:247)は、 ユーザーが指定した値 → ストレージクラスのアノテーションcdi.kubevirt.io/clone-strategy→ プロバイダーテーブルの推奨値 → 「VolumeSnapshotClassが存在する場合はsnapshot、存在しない場合はcopy(ホストコピー)」の順序で選択されます。
PVCを複製するDataVolumeが渡されると、 clone/planner.go の ChooseStrategy()
(:157) →
computeStrategyForSourcePVC()(:297)が
デフォルトの snapshot(:323)から開始し、
グローバルな上書きとStorageProfileを順次反映させ、snapshotであっても一致するVolumeSnapshotClassがない場合はホストコピーにフォールバックします(:354)。ドキュメントが言及している
スマートクローンがこのsnapshotのルート(planSnapshotFromPVC, :670:スナップショット → 一時PVCの復元 → Rebind)であり、 csi-clone はCSIドライバーのボリュームクローン(planCSIClone, :739),
copy )であり、ソースとターゲットにそれぞれポッドを起動してバイト単位でデータ転送を行うホストコピー(planHostAssistedFromPVC, :526)です。
4. 実測
v1.66.0がインストールされた小さなクラスターで読み取りを行いました(2026-10-17)。対象はレジストリイメージで満たされた1つのルートDataVolumeと、それを使用するVMです。
サイズ。 DataVolumeは spec.storage でアクセスモードRWO、要求 10Gi、共有ストレージのCSIクラスでした。
| 項目 | 値 | 計算 |
|---|---|---|
| DataVolume要求 | 10Gi = 10,737,418,240 B | — |
PVC spec.resources.requests.storage |
11,381,663,335 B | ⌈10,737,418,240 × 1.06⌉ = 11,381,663,335 ✓ |
PVC status.capacity.storage |
11,381,663,335 B | 等しい |
ゲストディスク(qemu-img info、raw) |
10,737,418,240 B = 10 GiB | ⌈11,381,663,335 ÷ 1.06⌉ → 1MiB切り捨て = 10GiB ✓ |
| ディスクファイルが実際に占有するサイズ | 867MiB | スパースファイル |
VMIの status.volumeStatus[].persistentVolumeClaimInfo.filesystemOverhead にも "0.06" が記載されていました。「学生VMが起動するまで」の第3部で「CDIのドキュメント通りなら10Gi、設計ドキュメントでは約9.4GiB — 未実測」として残されていた疑問の答えがここにあります。 spec.storage を使用すると 10GiB になります。
9.4GiBは spec.pvc で10Giを指定した場合のサイズ(10Gi ÷ 1.06 → 1MiB切り捨て ≈ 9.43GiB)に該当します。
ポピュレータの痕跡。 ターゲットPVCのアノテーションに cdi.kubevirt.io/storage.usePopulator: "true", cdi.kubevirt.io/storage.populator.pvcPrime: prime-<UID>,
cdi.kubevirt.io/storage.populator.progress: 100.0%, cdi.kubevirt.io/storage.pod.phase: Succeeded, cdi.kubevirt.io/storage.contentType: kubevirt
が残っていました。3.2節でポピュレータがPVC'のアノテーションをターゲットにコピーした結果です。PVC'とimporterポッドはすでに削除されていました。
設定。 CDIConfigの status.filesystemOverhead はグローバル 0.06、ストレージクラスの4つすべてが 0.06 であり、機能ゲートの
HonorWaitForFirstConsumer が有効になっていました。
StorageProfile。 共有ストレージのCSIクラス2つのclaimPropertySetsは ReadWriteMany·Filesystem で埋められていました(プロバイダーテーブルに存在するドライバー)。ノードローカルクラス2つはテーブルに存在しないプロバイダーのため、claimPropertySetsが空でした。4つのクラスすべてでcloneStrategyが copy であり、
クラスターにはVolumeSnapshotClassが1つも存在しませんでした。つまり、その小さなクラスターでPVCを複製する場合、スマートクローンではなく バイト単位で丸ごとコピーを行います。
「学生VMが起動するまで」の第4部で、学生ごとにPVCの複製ではなくオーバーレイ(書き込みレイヤー)を選択した理由と同じ事実です。
5. 制限事項
- importerポッドは完了後に削除されるため、ステップごとのログ(Info → … → Resize)を確認できませんでした。アノテーション
cdi.kubevirt.io/storage.pod.retainAfterCompletionを付与した新しいDataVersionで再度測定すれば確認可能ですが、新しく作成しないこととしたため 未実測となります。 - 「空きスペースが要求より小さい」場合(プロバイダが要求より小さいボリュームを割り当てた場合)は測定していません。共有ストレージはPVCサイズを強制しないことが多いため、測定したクラスターでは発生しませんでした。
- 複製(snapshot・csi-clone・copy)はコードのリーディングのみ行い、実行はしていません。ブロックモードも 未測定です。
TTの一言。
spec.storageとspec.pvcは1文字の違いのようですが、ディスクサイズが6%異なります。ゲストディスクをちょうど10GiBで受け取りたい場合はspec.storageを使い、PVCが少し大きくなることは覚えておいてください!
参考
すべて2026-10-17に閲覧しました。
- CDI v1.66.0 ソース: https://github.com/kubevirt/containerized-data-importer/tree/v1.66.0
- DataVolume ドキュメント: https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/datavolumes.md
- CDIConfig ドキュメント(filesystemOverhead): https://github.com/kubevirt/containerized-data-importer/blob/v1.66.0/doc/cdi-config.md
- サイズ計算をドキュメントに追加する PR #4318(レビュー中): https://github.com/kubevirt/containerized-data-importer/pull/4318
- Kubernetes ボリュームポピュレーター: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#volume-populators-and-data-sources
- 姉妹シリーズ「学生VMが立ち上がるまで」第3話: ディスクはPVCである, 第4話: PVC観点からのオーバーレイ