创建虚拟机

以安装 Kali 为例:

将ISO镜像上传至pve,在选项ISO 镜像中选择要安装的系统

机型: 有“i440fx”和“q35”两种。这俩都是英特尔的芯片组型号,440fx是非常古老的型号了,支持PCI总线,q35是相对的型号,支持PCIE总线。一般选择q35

BIOS: 有“SeaBIOS”和“OVMF(UEFI)”两种,SeaBIOS对应传统型BIOS。如果选择OVMF(UEFI),还需要指定一个EFI磁盘

SCSI控制器: 选择默认的VirtIO SCSI就行,这是PVE官方推荐的。
Qemu代理: 开启此项后,PVE就可以跟虚拟机进行通信,比如发出关机指令之类的。如果没有开启Qemu代理,当你在PVE界面里让某个虚拟机关机,PVE会通过ACPI层面发出关机指令,但是如果虚拟机没有加载正确的ACPI驱动的话,就无法正确响应关机指令。至于QGA,Linux系统使用APT或YUM命令安装,Windows系统的下载链接,下载下来是ISO格式,里面不光有QGA,还有VirtIO驱动。


NUMA:简单来讲,就是为每一个物理CPU分配一个内存池,而不是所有CPU共用一个内存池。这个对家用电脑单CPU来讲也是没啥意义的,不用选。

Ballooning设备:Ballooning的意思是膨胀,这个选项的作用是让PVE动态地调整虚拟机运行时使用的内存大小。虚拟机需要安装Ballooning驱动,不然也不起作用。Linux很早就默认自带Ballooning驱动了。Windows需要额外安装驱动,并且可能会降低性能,PVE官方建议Win虚拟机关闭这个选项

模型:通常不建议选择最下面两个。E1000是英特尔的千兆网卡型号,有很好的兼容性,大多数系统都自带驱动,适合大多数情况。VirtIO是半虚拟化,虽然也是虚拟出来的硬件,但可以获得接近实际硬件的性能,但是需要虚拟机加载相应驱动。如果网卡大于千兆,那么选择VirtIO网卡。

虚拟机创建完成后,在硬件里可以修改虚拟机的硬件配置,添加PCI、USB等设备

开机自启动启动/关机顺序:可以设置虚拟机跟随PVE自动启动和关机,适合配置自动任务。其中,顺序的数值表示开机顺序,数值1表示PVE启动后第一个被启动,关机则相反,最后一个关机。启动延时指的是当前虚拟机开机后到下一个虚拟机开机的时间间隔,单位是秒。关机超时是超过这个时间未完成关机,PVE会强制关机。
引导顺序:记得设置
QEMU Guest Agent:与创建虚拟机时系统界面中选项是一致的

虚拟磁盘

对比

以下是 Proxmox VE (PVE) 中常见虚拟磁盘格式的对比表格,涵盖性能、特性、适用场景等方面:

特性 / 格式 Raw (raw) QCOW2 (qcow2) VMDK (vmdk) Ceph RBD (rbd) LVM (lvm) / LVM-thin (lvmthin)
格式类型 无格式原始磁盘 QEMU 写时复制格式 VMware 虚拟磁盘格式 Ceph 块设备 逻辑卷管理 / 精简配置逻辑卷
性能 ⭐⭐⭐⭐⭐ 最高 ⭐⭐⭐ 中等(有元数据开销) ⭐⭐⭐⭐ 高 ⭐⭐⭐⭐ 高(网络延迟影响) ⭐⭐⭐⭐ 高
快照支持 ❌ 不支持 ✅ 支持(内置) ✅ 支持 ✅ 支持(Ceph层) ✅ 支持(LVM-thin)
克隆/链接克隆 ❌ 不支持 ✅ 支持(qcow2快照链) ✅ 支持 ✅ 支持(Ceph克隆) ✅ 支持(LVM-thin快照克隆)
压缩 ❌ 不支持 ✅ 支持(可选) ✅ 支持(部分版本) ❌ 不支持 ❌ 不支持
加密 ❌ 不支持 ✅ 支持(LUKS或qcow2加密) ✅ 支持(VMware加密) ✅ 支持(Ceph LUKS) ✅ 支持(LUKS on LVM)
精简配置 ❌ 不支持(预分配) ✅ 支持 ✅ 支持 ✅ 支持 ✅ 仅 LVM-thin 支持
迁移/导出兼容性 ✅ 通用(需转换) ✅ QEMU/KVM通用 ✅ VMware/PVE通用 ❌ 仅Ceph集群内 ❌ 仅本机LVM
存储效率 ❌ 占用全部预分配空间 ✅ 按需增长,节省空间 ✅ 按需增长 ✅ 按需分配 ✅ LVM-thin 按需分配
适用场景 高性能需求、数据库 通用场景、开发测试 VMware迁移、混合环境 分布式存储、高可用集群 本地高性能、企业级部署
备份支持 ✅(需停机或fsfreeze) ✅(在线快照备份) ✅(Ceph快照) ✅(LVM快照,需停机或冻结)
最大容量限制 无(受文件系统限制) ~2EB(理论) ~62TB(vmdk v6) 无(Ceph集群决定) 受VG/LV限制(TB级)

补充说明

  • Raw:性能最佳,但不支持高级功能(快照、克隆等),适合对IO性能要求极高的场景。
  • QCOW2:PVE默认推荐格式,功能丰富,适合大多数通用场景。
  • VMDK:主要用于从 VMware 迁移虚拟机到 PVE,兼容性好。
  • Ceph RBD:适用于分布式存储环境,支持高可用和横向扩展,但依赖网络性能。
  • LVM / LVM-thin:本地存储高性能方案,LVM-thin 支持精简配置和快照,适合企业生产环境。

推荐选择

  • 新手 / 通用用途qcow2
  • 追求极致性能rawlvm
  • 从 VMware 迁移vmdk
  • 分布式/高可用架构rbd
  • 企业本地存储管理lvmthin

💡 提示:PVE 7.x+ 已默认使用 qcow2,因其功能与性能平衡良好。

压缩

在 Proxmox VE(PVE)虚拟化环境中,虚拟磁盘文件(如 qcow2、vmdk、raw 等)在客户机内删除文件后,磁盘镜像体积不会自动缩小 —— 这是因为虚拟磁盘默认“只增不减”,已分配的空间即使被 Guest OS 标记为“空闲”,宿主机仍视其为“已使用”。

当然有,如果您的存储空间非常有限,之前提到的“移动存储”方法(需要至少一个收缩后磁盘大小的额外空间)可能不适用。

这里为您提供几种占用额外空间更少的替代方案,从最推荐到最复杂的顺序排列。

方案一:使用 qemu-img convert

这个方法本质上和 Proxmox 的“移动存储”功能一样,都是创建一个新的、压缩过的磁盘文件。但的优势在于手动操作提供了更大的灵活性,例如,您可以将临时文件输出到另一个有足够空间的存储上,操作完成后再移回来。

前提:

  1. 虚拟机内部已执行 fstrim -av
  2. 强烈建议关闭虚拟机以保证数据一致性。

操作步骤:

假设您的磁盘文件位于 /var/lib/vz/images/100/vm-100-disk-0.qcow2

  1. 进入磁盘文件所在目录

    1
    cd /var/lib/vz/images/
  2. 执行转换
    这个命令会读取旧文件,并创建一个新的、已收缩的文件。

    1
    2
    # qemu-img convert -O qcow2 <原文件名> <新文件名>
    qemu-img convert -O qcow2 vm-100-disk-0.qcow2 vm-100-disk-0-shrinked.qcow2
    • -O qcow2: 指定输出格式为 qcow2。
    • 这个过程不是原地进行的,需要额外的空间来存放 vm-100-disk-0-shrinked.qcow2 文件。但您可以在命令中指定一个位于其他存储(如挂载的USB硬盘)的路径来存放新文件。
  3. 验证并替换文件
    检查新文件的大小,确认已经收缩。

    1
    ls -lh

    确认无误后,备份旧文件并用新文件替换。

    1
    2
    mv vm-100-disk-0.qcow2 vm-100-disk-0-original.qcow2
    mv vm-100-disk-0-shrinked.qcow2 vm-100-disk-0.qcow2
  4. 启动虚拟机测试
    启动虚拟机,检查一切是否正常。正常运行后,可以删除备份的旧文件 vm-100-disk-0-original.qcow2

优点

  • 操作灵活,可以利用其他存储空间。
  • qemu-img 是 Proxmox 自带的核心工具,无需安装。

缺点

  • 仍然需要临时空间(等于收缩后的磁盘大小)。
  • 需要手动重命名文件,有一定操作风险。

方案二:先“清零”再转换(追求极致压缩)

这个方案稍微复杂一些,但能确保所有未使用的空间都被物理性地清零,从而让 qemu-img 的压缩效果达到最好。分为两步:

第一步:在虚拟机内部将可用空间填零

fstrim 只是告诉系统哪些块是“未使用”的,但这些块里可能还存留着旧数据的痕迹。通过填充零,我们可以确保这些空间在物理上是空的。

Linux 虚拟机 内,执行以下命令:

1
2
3
4
5
6
7
# 创建一个填满零的大文件,直到占满所有剩余空间
dd if=/dev/zero of=zero.file bs=4M status=progress

# 当你看到 "No space left on device" 的错误时,说明已经写满,按 Ctrl+C 停止即可
# 然后立即删除这个文件,释放空间
rm zero.file
sync # 确保所有缓存写入磁盘

Windows 虚拟机 内,可以使用微软官方的 SDelete 工具:

1
2
3
4
# 下载 SDelete: https://docs.microsoft.com/en-us/sysinternals/downloads/sdelete
# 在 PowerShell (管理员) 中执行
.\sdelete.exe -z C:
# -z 参数就是 zero free space

第二步:在宿主机上执行 qemu-img convert

在虚拟机关机后,执行和方案一中完全相同的 qemu-img convert 操作。由于虚拟机内的未使用空间现在都变成了零,qemu-img 在转换时能更高效地识别和抛弃这些零块,压缩效果可能会更好。

优点

  • 压缩率可能最高。

缺点

  • 在虚拟机内写零文件会产生大量I/O,比较耗时。
  • 同样需要额外的临时空间进行转换。

方案三:使用 virt-sparsify(最接近“原地”操作的工具)

virt-sparsifylibguestfs-tools 工具包中的一个命令,专门用于“稀疏化”磁盘镜像,即回收空闲空间。的一个巨大优势是可以选择 in-place(原地) 操作,几乎不需要任何额外的存储空间

前提:

  • 需要在 PVE 宿主机上安装工具包。
  • 虚拟机必须关机

操作步骤:

  1. 在虚拟机内部执行 TRIM

    1
    2
    # PowerShell
    Optimize-Volume -DriveLetter C -ReTrim -Verbose # TRIM C盘
    1
    2
    # Linux
    sudo fstrim -av
  2. 安装工具包

    1
    2
    apt update
    apt install libguestfs-tools
  3. 执行原地收缩
    进入磁盘文件所在目录,然后执行命令。

    1
    2
    # cd /var/lib/vz/images/100/
    virt-sparsify --in-place vm-100-disk-0.qcow2

    --in-place 参数是关键,会直接在原文件上进行修改。工具会先在磁盘镜像内部找到并清空未使用空间,然后尝试在文件系统层面收缩文件。

优点

  • --in-place 模式几乎不需要额外的临时空间,完美符合您的要求。
  • 自动化程度高,一条命令解决问题。

缺点

  • 需要额外安装软件包。
  • 原地操作有一定风险,虽然工具很成熟,但强烈建议您在操作前对 qcow2 文件做一个备份,以防万一。

验证压缩结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@pve:/var/lib/vz/images/101# qemu-img info vm-101-disk-1.qcow2 

image: vm-101-disk-1.qcow2 # qcow2 磁盘文件名(VM 101 的第二块盘)
file format: qcow2 # 磁盘格式:qcow2,支持快照/稀疏/压缩
virtual size: 100 GiB (107374182400 bytes) # 虚拟容量,VM 内看到的磁盘大小
disk size: 36.9 GiB # 实际占用的物理空间(已成功瘦身后的结果)
cluster_size: 65536 # qcow2 cluster 大小:64KiB(默认且合理)
Format specific information: # qcow2 格式的内部参数开始
compat: 1.1 # qcow2 兼容版本 1.1(现代版本)
compression type: zlib # qcow2 内建压缩算法(很少实际触发)
lazy refcounts: false # 未启用延迟 refcount(更安全,性能略低)
refcount bits: 16 # refcount 位宽(默认值,没问题)
corrupt: false # 镜像未损坏(关键健康指标)
extended l2: false # 未启用扩展 L2 表(正常)
Child node '/file': # 底层文件节点信息
filename: vm-101-disk-1.qcow2 # 实际承载 qcow2 的文件
protocol type: file # 存储后端是普通文件(local dir)
file length: 100 GiB (107390828544 bytes) # 文件逻辑长度(不是实际占用)
disk size: 36.9 GiB # 文件系统层面真实占用空间

总结与建议

方法 额外空间需求 操作复杂度 风险 建议
PVE 移动存储 收缩后磁盘大小 空间足够时的首选,最安全。
qemu-img convert 收缩后磁盘大小 空间不足但可利用其他存储时。
先清零再转换 收缩后磁盘大小 追求极致压缩率,不嫌麻烦时。
virt-sparsify --in-place 几乎为零 中高 对额外空间要求最苛刻时的最佳选择。 操作前务必备份。

好的,我们经历了漫长而曲折的调试过程,最终找到了一个非常稳定、可靠且功能丰富的解决方案。现在,我将把这个最终的成功方法总结成一篇清晰的博客文章,省略所有错误的尝试,只保留最佳实践。


在概要页添加CPU温度、频率和硬盘健康监控

默认情况下,PVE 并不显示 CPU 温度、实时频率或硬盘的 S.M.A.R.T. 健康状况。

第一步:安装必备工具

首先,我们需要确保系统上安装了获取硬件信息所需的命令行工具。

在 PVE 的 SSH 终端中执行以下命令:

1
2
3
4
5
6
7
8
9
# 更新软件源
apt update

# 安装温度传感器、硬盘健康检测和系统状态工具
apt install -y lm-sensors smartmontools sysstat

# 自动检测系统中的硬件传感器
# 在交互过程中,对所有询问都默认即可
sensors-detect

第二步:修改后端 (Nodes.pm) - 统一数据采集

我们的目标是让 PVE 的 API 在被调用时,自动执行 sensors, smartctlcat /proc/cpuinfo 命令,并将所有结果打包。

  1. 备份原始文件(重要!)

    1
    cp /usr/share/perl5/PVE/API2/Nodes.pm /usr/share/perl5/PVE/API2/Nodes.pm.bak
  2. 编辑文件

    1
    nano /usr/share/perl5/PVE/API2/Nodes.pm
  3. 定位并插入代码

    • nano 编辑器中,按 Ctrl + W 搜索 $res->{pveversion} 来定位。
    • 紧接着 }; 之后,粘贴下面这个完整的代码块。请注意:您需要将 smartctl -a --json /dev/nvme0 中的 /dev/nvme0 替换为您自己硬盘的实际设备名(例如 /dev/sda)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # --- BEGIN: Custom Hardware Info (Final Unified Version) ---
    # 获取温度信息 (JSON 格式)
    my $sensinfo_json = `sensors -j`;
    if ($sensinfo_json =~ /^(.*)$/s) {
    $res->{sensinfo} = $1;
    }

    # 获取CPU频率信息 (Text 格式)
    my $freqinfo_text = `cat /proc/cpuinfo | grep "cpu MHz"`;
    if ($freqinfo_text =~ /^(.*)$/s) {
    $res->{freqinfo} = $1;
    }
    # 3. 获取 NVMe 硬盘健康信息 (JSON 格式)
    my $smartinfo_json = `smartctl -a --json /dev/nvme0`;
    if ($smartinfo_json =~ /^(.*)$/s) {
    $res->{smartinfo} = $1;
    }
    # --- END: Custom Hardware Info (Final Unified Version) ---
  4. 保存并退出 (Ctrl+X, Y, Enter)。

第三步:修改前端 (pvemanagerlib.js) - 创建显示面板

现在,我们来创建三个独立的面板,分别用于显示温度、频率和硬盘健康。

  1. 备份原始文件

    1
    cp /usr/share/pve-manager/js/pvemanagerlib.js /usr/share/pve-manager/js/pvemanagerlib.js.bak
  2. 编辑文件

    1
    nano /usr/share/pve-manager/js/pvemanagerlib.js
  3. 定位并插入代码

    • nano 中,按 Ctrl + W 搜索 textField: 'pveversion' 来定位。
    • pveversion 代码块的结束符号 }, 之后,依次粘贴下面这三个全新的代码块。

代码块 1:硬件温度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
itemId: 'temp_panel',
colspan: 2,
printBar: false,
title: gettext('硬件温度'),
textField: 'sysinfo', // 从统一的 sysinfo 读取数据
renderer: function(value) {
if (!value) { return 'N/A'; }
try {
const data = JSON.parse(value);
const sens_data = data.sensors; // 只使用 sensors 部分的数据

const cpu = sens_data?.["coretemp-isa-0000"]?.["Package id 0"]?.["temp1_input"];
const nvme_temp = sens_data?.["nvme-pci-0100"]?.["Composite"]?.["temp1_input"];

let temp_parts = [];
if(cpu) temp_parts.push(`CPU: <strong>${cpu.toFixed(1)}°C</strong>`);
if(nvme_temp) temp_parts.push(`NVMe: <strong>${nvme_temp.toFixed(1)}°C</strong>`);

return temp_parts.join(' | ');
} catch(e) { return '温度解析错误'; }
},
},

代码块 2:核心频率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
itemId: 'freq_panel',
colspan: 2,
printBar: false,
title: gettext('核心频率'),
textField: 'sysinfo', // 同样从 sysinfo 读取
renderer: function(value) {
if (!value) { return 'N/A'; }
try {
const data = JSON.parse(value);
const freq_text = data.freq; // 只使用 freq 部分的数据

const freqs = freq_text.match(/[\d\.]+/g) || [];
if (freqs.length > 0) {
const formattedFreqs = freqs.map(f => Math.round(parseFloat(f)) + ' MHz');
return formattedFreqs.join(' | ');
}
return '未能提取频率';
} catch (e) { return '频率解析错误'; }
},
},

代码块 3:NVMe 硬盘健康(可选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
itemId: 'smart_panel',
colspan: 2,
printBar: false,
title: gettext('NVMe 硬盘健康'),
textField: 'sysinfo', // 同样从 sysinfo 读取
renderer: function(value) {
if (!value) { return 'N/A'; }
try {
const data = JSON.parse(value);
const smart_data = data.smart; // 只使用 smart 部分的数据
let output = [];

if (smart_data?.smart_status?.passed) {
output.push('状态: <strong><span style="color:green;">良好</span></strong>');
} else {
output.push('状态: <strong><span style="color:red;">警告</span></strong>');
}

const percentUsed = smart_data?.nvme_smart_health_information_log?.percentage_used;
if (percentUsed !== undefined) {
output.push(`寿命已用: <strong>${percentUsed}%</strong>`);
}

const unitsWritten = smart_data?.nvme_smart_health_information_log?.data_units_written;
if (unitsWritten !== undefined) {
const tbWritten = (unitsWritten * 512000) / (1000 * 1000 * 1000 * 1000);
output.push(`总写入: <strong>${tbWritten.toFixed(2)} TB</strong>`);
}

return output.join(' | ');
} catch (e) { return 'SMART 解析错误'; }
},
},

保存并退出。

第四步:应用更改

  1. (可选但推荐)检查 Perl 语法

    1
    perl -c /usr/share/perl5/PVE/API2/Nodes.pm

    如果显示 ... syntax OK 则表示后端文件没有语法错误。

  2. 重启服务

    1
    systemctl restart pveproxy
  3. 强制刷新浏览器:

    回到 PVE 网页,按下 Ctrl + Shift + R (Mac 用户按 Cmd + Shift + R)。