作者:Mooloco
环境:PVE 8.x / Samsung 990 PRO 1TB / Windows 10 LTSC
目标:将 VM 从 QCOW2 虚拟磁盘迁移到 NVMe 物理分区直通,降低 CPU 开销
一、为什么要做这件事
QCOW2 格式虽然功能丰富(快照、精简配置),但代价是额外的 CPU 开销:
一次磁盘 I/O 的 CPU 周期消耗:
QCOW2 虚拟磁盘: ~11000 周期 ← 含格式转换 + COW 开销
RAW 分区直通: ~3300 周期 ← 仅块设备操作
CPU 开销降低约 70% ✅
二、准备工作
2.1 确认 VM 当前配置
qm config <VM_ID>
# 重点关注磁盘类型 (scsi0/virtio0) 和路径
2.2 确认目标硬盘
lsblk
# 找到你要迁移到的目标盘(如 /dev/nvme0n1)
2.3 关机
qm shutdown <VM_ID>
# 或
qm stop <VM_ID>
三、缩容数据盘(如有需要)
如果你的硬盘上没有空闲空间给新分区,需要从现有分区中割出空间。
3.1 严格的步骤顺序
卸载分区 ← 第一步 ↓ e2fsck -f ← 第二步:检查文件系统 ↓ resize2fs ← 第三步:先缩文件系统! ↓ parted resizepart ← 第四步:再缩分区 ↓ e2fsck ← 第五步:验证 ↓ 重新挂载 ← 第六步
⚠️ 绝不能先缩分区再缩文件系统! 否则文件系统超出分区边界,数据无法访问。
3.2 ⚠️ 最大的坑:单位不一致
这是最容易翻车的地方,请务必注意:
# resize2fs 的 G 是 GiB(2^30 = 1,073,741,824 字节) resize2fs /dev/nvme0n1p1 800G # 这里的 800G ≈ 800 GiB
# parted 的 G 是 GB(10^9 = 1,000,000,000 字节) parted /dev/nvme0n1 resizepart 1 800G # 这里的 800G = 800 GB ≈ 745 GiB
两者不一致的后果:
resize2fs 800G → 文件系统 800 GiB parted 800G → 分区大小 745 GiB → 文件系统比分区大 55 GiB! → e2fsck 报错: "filesystem size is larger than physical size"
✅ 正确做法
进入 parted 后切换单位:
parted /dev/nvme0n1 (parted) unit GiB ← 切换到 GiB!跟 resize2fs 保持一致 (parted) resizepart 1 800 ← 直接输入数字,不带单位 (parted) print
3.3 第二个坑:GPT 对齐导致差 1 MiB
即使单位和数值完全一致,仍可能遇到:
filesystem size: 209715200 blocks physical size : 209714944 blocks 差: 256 blocks × 4096 字节/块 = 1 MiB
原因:GPT 分区默认按 1 MiB 对齐,parted 自动调整了结束位置,导致分区比文件系统小了 1 MiB。
修复方法
直接按 block 数指定文件系统大小:
# 确认分区未挂载 findmnt /dev/nvme0n1p1 # 无输出后执行,指定精确的 block 数(物理分区可容纳的最大 block 数) resize2fs /dev/nvme0n1p1 209714944 # 验证 e2fsck -f /dev/nvme0n1p1
3.4 创建新分区
parted /dev/nvme0n1 (parted) unit GiB (parted) print free ← 查看空闲空间 (parted) mkpart primary <起始> <结束> (parted) quit
新分区不要格式化,保持裸分区状态。
四、数据迁移:QCOW2 → RAW 分区
# 推荐使用 qemu-img convert(只搬实际数据,比 dd 快)
qemu-img convert -p -O raw \
/var/lib/vz/images/<VM_ID>/vm-<VM_ID>-disk-0.qcow2 \
/dev/nvme0n1p<分区号>
参数说明:
-p:显示进度-O raw:输出为裸格式
qemu-img convert只搬运实际写入的数据块,不会像dd那样搬整个虚拟大小,速度快很多。
五、VM 配置切换
5.1 获取稳定的 by-id 路径
ls -l /dev/disk/by-id/ | grep nvme | grep part<分区号>
找到类似这样的路径:
/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_1TB_S6Z1NJ0W231490M-part2
为什么用 by-id 而不是 nvme0n1p2? 因为 Linux 内核的 NVMe 设备名 nvme0n1 / nvme1n1 可能因启动时序不同而变化。by-id 基于硬盘序列号,永远稳定。详见本文末的踩坑记录。
5.2 删除旧的磁盘引用
qm set <VM_ID> --delete scsi0
旧的 QCOW2 会变成 unused0 状态,仅移除引用,不删除文件。
5.3 添加 RAW 分区
qm set <VM_ID> --scsi0 /dev/disk/by-id/nvme-xxxx-part2,discard=on,ssd=1,iothread=1
推荐参数:
discard=on:启用 TRIM,让 SSD 及时回收空闲块ssd=1:让 VM 识别为 SSDiothread=1:独立 I/O 线程,提高高负载场景性能
5.4 设置 SCSI 控制器
qm set <VM_ID> --scsihw virtio-scsi-single
virtio-scsi-single 比默认的 virtio-scsi 有更好的多队列性能。
5.5 设置启动顺序
qm set <VM_ID> --boot order=scsi0;net0
六、验证与清理
6.1 启动 VM
qm start <VM_ID>
6.2 验证配置
qm config <VM_ID>
# 确认 scsi0 指向 by-id 路径
6.3 保留旧 QCOW2 作为回退方案
# 确认运行正常后,重命名而非删除
mv /var/lib/vz/images/<VM_ID>/vm-<VM_ID>-disk-0.qcow2 \
/var/lib/vz/images/<VM_ID>/vm-<VM_ID>-disk-0.qcow2.bak
6.4 确认无误后删除旧镜像
运行一周无问题后,在 PVE Web 界面或通过命令行清理:
qm disk unlink <VM_ID> --idlist unused0
rm /var/lib/vz/images/<VM_ID>/vm-<VM_ID>-disk-0.qcow2.bak
七、性能收益
| 对比项 | QCOW2 虚拟磁盘 | RAW 分区直通 |
|---|---|---|
| CPU 开销 | 20-25% | 5-8% |
| 4K 随机 IOPS | ~15K | ~35K+ |
| 连续读写 | 受 QCOW2 格式转换限制 | 接近设备上限 |
| TRIM 支持 | 需 QEMU 配合 | 直通到设备 |
放弃的能力
| 功能 | 说明 |
|---|---|
| QCOW2 快照 | 不再支持虚拟机级快照 |
| 精简置备 (Thin Provision) | 分区空间一次性分配 |
| 在线迁移(需共享存储) | 本地 RAW 分区不能热迁移 |
八、踩坑记录
坑 1:设备名变化导致 VM 找不到盘
现象: 重启 PVE 节点后,VM 启动停在 BIOS 界面,提示没有可引导的磁盘。
原因: 配置写的是 /dev/nvme0n1p2,但重启后内核枚举顺序变了,盘变成了 nvme1n1p2。
根因: Linux 内核按驱动加载时序给 NVMe 命名,不是按物理插槽。GB-PVE 上有两个 NVMe 盘(990 Pro + SN730 系统盘),两个控制器谁先完成初始化谁就是 nvme0,时序受 ACPI 状态影响,每次启动不完全一致。
解决方法: 使用 /dev/disk/by-id/ 路径替代裸设备名。
/dev/disk/by-id/nvme-Samsung_SSD_990_PRO_1TB_xxxxx-part2
基于硬盘序列号,永远指向同一块盘,不受枚举顺序影响。
坑 2:resize2fs 和 parted 的单位不一致
详见本文第三节。
坑 3:GPT 对齐导致的 1 MiB 差异
详见本文第三节。
坑 4:Windows VirtIO 驱动导致 PE 看不到盘
现象: VM 无法启动后,想进 WinPE 修复引导,发现 diskpart 里看不到盘。
原因: Windows 修复环境默认不带 VirtIO 驱动,无法识别 VirtIO 磁盘。
解决方法: 挂载 virtio-win.iso 作为 VM 的第二个光驱,在 PE 启动时手动加载驱动。
附录:推荐 VM 配置示例
cpu: host scsihw: virtio-scsi-single scsi0: /dev/disk/by-id/nvme-Samsung_SSD_990_PRO_1TB_S6Z1NJ0W231490M-part2,discard=on,iothread=1,ssd=1 balloon: 0 agent: 1 boot: order=scsi0;net0
本文档由 Hermes Agent 整理,基于 Mooloco 的实际操作经验。