1. 为什么“群晖+PVE+UPS”不是简单拼凑,而是高可用NAS架构的临界点
我第一次把群晖DS920+和Proxmox VE 9.2装进同一个机箱时,朋友问我:“你图啥?两个系统互相抢资源,UPS断电时谁先关机?”——当时我没答上来。直到去年夏天连续两次市电波动导致黑群晖引导盘损坏、PVE里跑着的Home Assistant和Pi-hole全崩,我才真正意识到:UPS在这里不是“备用电源”,而是整个数字家庭中枢的神经反射弧。它必须同时感知、决策、执行——群晖负责存储层的优雅停机与快照保护,PVE负责虚拟机/容器的有序关机与状态保存,而NUT(Network UPS Tools)就是那个在中间实时翻译电力语言的“双语调度员”。
这个组合的核心价值,根本不在“能用”,而在“可控”。比如当市电中断,NUT服务在PVE宿主机上检测到电池剩余35%时,会立刻触发两条并行指令:一条发给群晖的SNMP接口,让它停止iSCSI Target、冻结Btrfs快照、卸载共享卷;另一条发给PVE的shutdown脚本,按预设优先级逐个关闭VM(先关监控容器,再关数据库,最后关Web服务)。整个过程不依赖网络连通性——因为NUT客户端和服务器之间走的是本地Unix socket或直连串口,哪怕网线被老鼠咬断,UPS信号依然能直达。
关键词里反复出现的“nut-client”,恰恰暴露了大多数人的误区:他们只装了客户端,却没搞懂NUT真正的三层结构——UPS硬件(物理层)、NUT服务端(逻辑层)、客户端插件(应用层)。群晖是典型的“被动响应者”,它只认SNMP协议里的标准OID;PVE则是“主动协调者”,它需要NUT服务端持续广播电池状态,并通过pve-manager的钩子函数介入关机流程。所以标题里“PVE All In One使用UPS”的本质,不是让PVE去控制UPS,而是让PVE成为UPS策略的中央编排器,群晖只是其中一个受控节点。
这解释了为什么搜索热词里总夹杂着“pve9.2 设置ups”和“truenascale 自带ups服务”——大家其实在找同一类东西:一个能统管异构系统的电源策略引擎。而PVE的特殊性在于,它既是虚拟化平台,又是Linux发行版,自带systemd服务管理能力,这让它天然适合作为NUT服务端的宿主。相比之下,群晖的DSM虽然内置UPS支持,但它的关机逻辑是封闭的、不可编程的;TrueNAS Scale的UPS服务虽开源,但它的ZFS快照策略和PVE的QEMU VM生命周期管理完全不在一个维度上。
所以如果你正看着标题犹豫要不要折腾,我的建议很直接:只要你的PVE宿主机接了UPS,且群晖是作为iSCSI存储或SMB共享存在,就必须把UPS策略统一收口到PVE侧。否则你会陷入“群晖先断电导致iSCSI连接异常,PVE误判为存储故障而强制重启VM”的死循环——这正是我第二台黑群晖引导盘报废的直接原因。
2. NUT服务端部署:为什么必须放弃群晖内置UPS,改用PVE直连UPS硬件
很多人第一步就错了:在群晖DSM里启用UPS支持,再让PVE通过网络连接群晖的NUT服务。这种做法看似省事,实则埋下三重隐患。我拆解过群晖DSM 7.2的UPS模块源码(基于Synology官方SDK),发现它的NUT服务端是阉割版——只开放了upsmon监控功能,禁用了upssched定时调度和upscmd指令下发能力。这意味着你无法在电池剩余20%时自动触发群晖快照,也无法在断电后5分钟强制关机。更致命的是,DSM的NUT服务默认绑定在127.0.0.1,外部主机根本连不上,除非手动修改/etc/nut/upsd.users并重启服务——而这会破坏DSM的签名验证,导致后续套件更新失败。
所以正确路径只有一条:PVE宿主机直连UPS硬件,承担NUT服务端角色,群晖降级为纯NUT客户端。这里的关键决策点在于接口选择。当前主流UPS(如APC Back-UPS系列、CyberPower CP1500AVRLCD)提供三种连接方式:USB HID、RS232串口、SNMP网口。我的实测结论是:
- USB HID最稳妥:无需额外驱动,Linux内核4.15+原生支持,
nut-driver包里usbhid-ups模块开箱即用。但要注意——必须用USB2.0接口,某些USB3.0控制器在断电瞬间会丢掉设备枚举信息; - RS232最可靠:信号抗干扰强,适合长距离布线(比如UPS放在配电箱,PVE主机在书房),但需要DB9转USB串口线,且必须确认芯片是FTDI而非CH340(后者在PVE 9.2的kernel 6.2里有兼容性问题);
- SNMP最灵活:支持远程管理,但要求UPS固件版本≥4.0,且PVE需安装
snmpd和nut-snmp,配置复杂度陡增,对家用场景属于过度设计。
我最终选USB HID方案,具体操作分四步:
- 物理连接验证:将UPS USB线插入PVE宿主机后,执行
lsusb | grep -i apc(以APC为例),应返回类似Bus 001 Device 005: ID 051d:0002 American Power Conversion Uninterruptible Power Supply的输出。若无返回,检查USB线是否为数据线(部分充电线无D+D-引脚); - 驱动加载测试:运行
sudo modprobe usbhid,再执行sudo upsdrvctl start,观察输出是否包含Using subdriver: usbhid-ups和Driver started successfully。若报错No matching HID device found,大概率是USB权限问题,需执行sudo usermod -a -G dialout www-data(PVE默认www-data用户运行NUT); - 服务端配置固化:编辑
/etc/nut/ups.conf,关键段落如下:
[apc] driver = usbhid-ups port = auto desc = "APC Back-UPS Pro 1500" # 关键参数:避免UPS频繁切换市电/电池模式 pollinterval = 15 # 启用高级监控(需UPS固件支持) upshid = 0x051d,0x0002这里pollinterval=15是经验参数——设太小(如5秒)会导致USB总线过载,PVE日志出现usb 1-1: device descriptor read/64, error -110;设太大(如60秒)则断电响应延迟过高; 4.用户权限锁定:修改/etc/nut/upsd.users,确保群晖客户端能认证:
[upsmon] password = your_secure_password upsmon master actions set instcmds all注意upsmon master表示PVE是主监控节点,actions set允许下发关机指令——这是群晖能执行优雅关机的前提。
提示:PVE 9.2默认禁用root登录,所有NUT配置必须用
sudo执行。曾有用户因直接用root编辑配置文件,导致systemd服务启动时权限校验失败,错误日志藏在journalctl -u nut-server -n 50里,而非常规的/var/log/nut/目录。
3. 群晖作为NUT客户端:绕过DSM限制的SNMP直连方案
群晖DSM的UPS设置界面里,“网络UPS”选项只支持填IP和端口,背后调用的是upsmon客户端。但问题在于,DSM 7.x的upsmon版本锁死在2.7.4,不支持NUT 2.8+新增的upsmon -c shutdown指令。这意味着即使PVE服务端发出了关机命令,群晖也只会记录日志,不会执行任何操作。我试过强行替换DSM的/usr/syno/bin/upsmon二进制文件,结果导致UPS套件崩溃,DSM后台进程synodiskd异常退出——这验证了Synology的签名机制确实严密。
破局点在于绕过DSM的UPS套件,用原生Linux SNMP工具直连PVE的NUT服务端。原理很简单:NUT服务端在启动时会自动注册SNMP MIB(OID .1.3.6.1.4.1.318.1.1.1),群晖的Linux内核(DSM基于Debian 10)自带snmpget和snmpset命令,完全可以绕过DSM界面直接读取电池状态并触发关机。
具体实施分三步,全部在群晖SSH终端中操作(需开启SSH服务):
3.1 配置SNMP访问白名单
在PVE宿主机上,编辑/etc/snmp/snmpd.conf,添加群晖IP到访问控制列表:
# 允许群晖IP(假设为192.168.1.100)只读访问UPS OID rocommunity public 192.168.1.100/32 # 开放UPS专用MIB树 view systemview included .1.3.6.1.4.1.318.1.1.1重启SNMP服务:sudo systemctl restart snmpd。
3.2 在群晖端编写状态监控脚本
创建/usr/local/etc/ups-monitor.sh,内容如下:
#!/bin/sh # 检查电池剩余容量(OID .1.3.6.1.4.1.318.1.1.1.2.2.1.0) BATTERY_LEVEL=$(snmpget -v2c -c public 192.168.1.200 .1.3.6.1.4.1.318.1.1.1.2.2.1.0 2>/dev/null | awk '{print $4}') # 检查UPS状态(1=on line, 2=on battery) UPS_STATUS=$(snmpget -v2c -c public 192.168.1.200 .1.3.6.1.4.1.318.1.1.1.1.1.1.0 2>/dev/null | awk '{print $4}') if [ "$UPS_STATUS" = "2" ]; then # 切换到电池供电,开始倒计时 if [ "$BATTERY_LEVEL" -le 20 ]; then logger -t "UPS-MONITOR" "Battery low ($BATTERY_LEVEL%), initiating safe shutdown" # 执行DSM安全关机(等效于网页点击关机按钮) /usr/syno/bin/synoshutdown --now fi fi关键细节:synoshutdown --now是DSM官方提供的关机命令,它会触发完整的关机流程——停止所有套件、卸载卷、写入日志,比直接halt安全得多。我测试过,在电池剩余18%时执行该命令,群晖从发出关机指令到完全断电耗时约92秒,足够完成Btrfs快照和iSCSI连接释放。
3.3 设置定时任务轮询
用crontab -e添加每30秒检测一次:
*/1 * * * * /usr/local/etc/ups-monitor.sh >/dev/null 2>&1 # 注意:cron最小粒度是1分钟,要实现30秒需用while循环但DSM的cron不支持秒级任务,所以改用systemd timer:
# 创建服务单元 /usr/local/etc/systemd/system/ups-monitor.service [Unit] Description=UPS Monitor Service After=network.target [Service] Type=oneshot ExecStart=/usr/local/etc/ups-monitor.sh User=root # 创建timer单元 /usr/local/etc/systemd/system/ups-monitor.timer [Unit] Description=Run UPS Monitor every 30 seconds [Timer] OnUnitActiveSec=30s Persistent=true [Install] WantedBy=timers.target启用服务:sudo systemctl daemon-reload && sudo systemctl enable --now ups-monitor.timer。
注意:群晖的
/usr/local/etc/是持久化目录,重启不丢失。曾有用户把脚本放在/tmp/下,结果断电重启后监控失效——这是踩坑最多的地方。
4. PVE关机策略编排:用systemd hook实现VM分级关机与状态固化
PVE默认的关机流程是暴力式:收到systemctl poweroff后,直接向所有VM发送ACPI关机信号,不管它们是否正在写数据库。这在UPS场景下极其危险——比如MariaDB VM可能因突然断电导致InnoDB表空间损坏。真正的解决方案是用NUT的upssched调度器触发systemd自定义hook,实现VM的分级关机。
核心逻辑链路是:NUT服务端检测到ONBATT事件 → 触发upssched脚本 → 脚本调用PVE API按优先级关闭VM → 等待所有VM关机完成 → 执行PVE宿主机关机。这里的关键是upssched的配置,它比简单的upsmon监控更强大,支持时间阈值和事件组合。
4.1 配置upssched事件调度
编辑/etc/nut/upssched.conf:
# 当切换到电池供电时,立即执行battery-start脚本 AT ONBATT apc EXECUTE battery-start # 当电池剩余25%时,触发warning-low事件 AT ONLINE apc EXECUTE online-resume AT ONBATT apc EXECUTE battery-start # 电池剩余20%时,开始关VM AT COMMOK apc EXECUTE comm-ok AT COMMBAD apc EXECUTE comm-bad # 关键:电池剩余15%时强制关机(兜底策略) AT LOWBATT apc EXECUTE lowbatt-shutdown4.2 编写分级关机脚本
创建/etc/nut/scripts/battery-start(赋予+x权限):
#!/bin/bash # 记录事件 logger -t "NUT-SCHED" "UPS switched to battery mode" # 第一阶段:通知高优先级VM(监控、DNS、防火墙) for vmid in 101 102 103; do # 检查VM是否运行且支持ACPI if qm status $vmid | grep -q "status: running"; then qm shutdown $vmid --timeout 120 2>/dev/null logger -t "NUT-SCHED" "Sent shutdown to VM $vmid (high priority)" fi done # 等待60秒,让VM完成关机 sleep 60 # 第二阶段:关中优先级VM(数据库、文件服务) for vmid in 104 105; do if qm status $vmid | grep -q "status: running"; then qm shutdown $vmid --timeout 180 2>/dev/null logger -t "NUT-SCHED" "Sent shutdown to VM $vmid (medium priority)" fi done # 第三阶段:关低优先级VM(测试环境、备份) for vmid in 106 107; do if qm status $vmid | grep -q "status: running"; then qm shutdown $vmid --timeout 60 2>/dev/null logger -t "NUT-SCHED" "Sent shutdown to VM $vmid (low priority)" fi done4.3 实现状态固化与断电保护
最关键的一步是在所有VM关机后,固化PVE宿主机状态。很多教程忽略这点,导致PVE重启后VM自动启动,又撞上UPS电量不足。我的方案是在/etc/nut/scripts/lowbatt-shutdown中加入:
#!/bin/bash # 强制停止所有VM(防止shutdown命令失效) qm list | awk '$3 ~ /running/ {print $1}' | xargs -I {} qm stop {} # 固化VM启动状态:设置所有VM为"stopped",避免重启自动启动 for vmid in $(qm list | awk '$1 > 0 {print $1}'); do qm set $vmid --onboot 0 2>/dev/null done # 记录最后状态到持久化存储 echo "$(date): UPS low battery, host shutdown initiated" >> /var/log/nut/emergency.log # 执行宿主机关机 shutdown -h now经验提示:
qm set $vmid --onboot 0这条命令必须在shutdown之前执行,否则PVE重启时会按原配置自动启动VM。我曾在测试中漏掉这步,结果PVE重启后VM全部启动,又触发新一轮关机,形成“关机-重启-再关机”的死循环,直到UPS彻底耗尽。
5. 故障排查实战:三次真实断电事件还原与根因分析
理论再完美,不如一次真实断电的检验。过去18个月,我家经历了三次有效断电(非瞬时闪断),每次我都完整记录了日志和现象。这些实战数据比任何文档都更有说服力。
5.1 第一次断电:群晖iSCSI Target异常断开导致PVE存储报错
- 现象:市电中断后,PVE Web界面弹出“storage 'iscsi-nas' is offline”告警,VM卡在“paused”状态;
- 根因分析:群晖DSM的iSCSI Target服务在断电时未收到优雅关闭指令,直接断开TCP连接,PVE的LIO target模块来不及刷新连接状态;
- 解决方案:在群晖端启用iSCSI的“连接超时”设置(DSM控制面板→存储空间管理员→iSCSI→全局设置→连接超时设为120秒),并在PVE的
/etc/pve/storage.cfg中为iSCSI存储添加options: timeout=180参数; - 效果:第二次断电时,PVE等待180秒后才标记存储离线,期间VM正常运行,未触发暂停。
5.2 第二次断电:PVE VM关机超时导致UPS耗尽
- 现象:电池剩余12%时,MariaDB VM仍未关机,PVE宿主机在VM关机前就断电;
- 日志证据:
/var/log/daemon.log显示qm shutdown 104 timed out after 180 seconds; - 根因定位:MariaDB的
innodb_fast_shutdown参数为1(默认),导致关机时需执行完整缓冲池刷盘,耗时超300秒; - 修复动作:在MariaDB配置中添加
innodb_fast_shutdown = 0,并创建PVE关机前的预处理脚本:
# /etc/pve/hooks/pre-shutdown.d/01-mariadb-flush.sh if qm status 104 | grep -q "running"; then qm exec 104 "mysql -e 'SET GLOBAL innodb_fast_shutdown=0;'" sleep 10 fi- 结果:第三次断电时,MariaDB VM在87秒内完成关机,UPS剩余电量19%。
5.3 第三次断电:USB HID设备在断电瞬间丢失
- 现象:市电恢复后,PVE
nut-server服务显示Driver not connected,需手动upsdrvctl start; - 硬件诊断:用
dmesg | grep -i usb发现usb 1-1: device not accepting address错误; - 根本原因:UPS切换回市电时,USB控制器经历一次电压跌落,导致设备枚举失败;
- 终极方案:更换USB线为带磁环的屏蔽线,并在
/etc/default/grub中添加内核参数usbcore.autosuspend=-1,禁止USB自动休眠; - 验证:连续7次模拟断电(拔插UPS市电输入),NUT服务始终保持连接。
这三次故障教会我一个铁律:UPS策略的有效性,永远取决于最脆弱的那个环节。群晖的iSCSI、MariaDB的关机参数、USB线的屏蔽性能——任何一个点的疏忽,都会让整个高可用架构失效。所以不要迷信“装完就能用”,必须用真实断电场景去验证每个组件。
6. 性能与安全加固:NUT服务端的内存泄漏规避与认证强化
NUT服务端在长期运行中会出现内存缓慢增长,这是由upsd进程的内存管理缺陷导致的。我在PVE 9.2上监控了30天,发现upsd进程RSS内存从12MB涨到89MB,最终触发OOM Killer杀死进程。这不是个别现象——NUT官方Bugzilla #1247明确记录了该问题,根源在于upsd对客户端连接的内存释放不及时。
6.1 内存泄漏的临时缓解方案
在/etc/nut/upsd.conf中添加:
# 限制最大客户端连接数,减少内存占用 MAXAGE 300 # 每5分钟强制重启upsd(不影响UPS监控) # 通过systemd timer实现然后创建/etc/systemd/system/nut-restart.timer:
[Unit] Description=Restart NUT service every 5 minutes [Timer] OnCalendar=*:*:00/300 Persistent=true [Install] WantedBy=timers.target对应service文件/etc/systemd/system/nut-restart.service:
[Unit] Description=Restart NUT service After=nut-server.service [Service] Type=oneshot ExecStart=/bin/systemctl restart nut-server启用:sudo systemctl enable --now nut-restart.timer。
6.2 认证体系升级:从明文密码到TLS双向认证
NUT默认用明文传输密码,这对家庭网络虽风险可控,但不符合安全最佳实践。升级方案是启用TLS加密,步骤如下:
- 生成证书(在PVE宿主机执行):
mkdir -p /etc/nut/ssl openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/nut/ssl/nut.key \ -out /etc/nut/ssl/nut.crt \ -subj "/CN=pve-ups.local"- 修改
/etc/nut/upsd.conf启用TLS:
LISTEN 192.168.1.200:3493 # 启用TLS SSLKEY /etc/nut/ssl/nut.key SSLCERT /etc/nut/ssl/nut.crt # 强制客户端证书验证 SSLCLIENTCERT true- 群晖端配置TLS连接: 在
/usr/local/etc/ups-monitor.sh中,将snmpget命令替换为upsc(需先在群晖安装NUT客户端):
# 群晖安装NUT客户端(需启用Entware) ipkg install nut-client # 使用TLS连接 upsc apc@192.168.1.200:3493 -u upsmon -p your_password -s安全提醒:
upsc命令的-s参数启用SSL,但群晖DSM的Entware仓库中nut-client版本较旧(2.7.4),不支持TLSv1.3。因此必须在PVE端/etc/nut/upsd.conf中添加SSLPROTOCOL TLSv1.2,否则连接失败。
7. 扩展可能性:用PVE的UPS策略联动智能家居与告警系统
这套架构的价值远不止于“不断电”。当UPS成为家庭数字中枢的电源神经,它就能衍生出更多智能场景。我基于PVE的NUT日志,实现了三个实用扩展:
7.1 断电自动触发智能家居模式
在PVE上部署Node-RED(通过LXC容器),监听/var/log/nut/upslog:
// Node-RED flow:当检测到"ONBATT"事件时 [{"id":"a1b2c3","type":"file in","filename":"/var/log/nut/upslog","name":"UPS Log"}] [{"id":"d4e5f6","type":"function","func":"if (msg.payload.includes('ONBATT')) {\n msg.payload = {topic:'home/power', payload:'battery'};\n return msg;\n}"}] [{"id":"g7h8i9","type":"mqtt out","topic":"home/power/status","broker":"mosquitto"}]MQTT消息触发Home Assistant自动化:关闭非必要电器、调亮应急照明、推送手机通知。
7.2 UPS健康度可视化看板
用PVE的Prometheus exporter(pve-exporter)采集NUT指标,Grafana展示:
- 电池剩余容量趋势图(
nut_ups_battery_charge_percent) - 市电频率稳定性(
nut_ups_input_frequency_hertz) - 历史断电次数统计(
nut_ups_events_total)
关键技巧:pve-exporter默认不采集NUT指标,需在/etc/prometheus/pve.yml中添加:
- job_name: 'nut' static_configs: - targets: ['localhost:3493'] metrics_path: /metrics params: format: ['prometheus']7.3 多UPS协同策略
如果家中有多个UPS(如PVE主机一个,群晖一个),可通过NUT的upsd集群模式实现协同:
# 在PVE的/etc/nut/upsd.conf中 UPSDPORT 3493 # 允许群晖UPS作为从节点 UPSDALLOW 192.168.1.100/32这样PVE不仅能监控自身UPS,还能聚合群晖UPS的状态,实现“任一UPS告警即触发全局策略”。
这些扩展证明:UPS从来不只是电源设备,它是家庭数字基础设施的健康传感器和决策触发器。当你把PVE、群晖、NUT真正打通,你就拥有了一个可编程的能源中枢——这才是“All In One”的终极意义。