「UPS」再也不怕拉闸了
缘起
前一段时间家里部署光伏发电系统,在并网时工作人员没有任何通知就断开了市电,导致服务器异常关机。另外一个就是夏天突发打雷可能会导致家里跳闸,之前都是靠家人日常关注天气情况,并在雷雨天提前关机,比较麻烦且操心。
因此不间断电源,必须整一个啦!
选型
选型的过程很艰难,成年人往往想全都要,奈何自古成年无两全!
初步了解了不间断电源(Uninterruptible Power Supply, UPS),大体分三类:在线式(Online)、在线互动式(Line-Interactive)、后备式(Back)。简单理解,
在线式:即UPS时刻在线优化电信号,提供理想的电源输出在线互动式:即UPS时刻检查市电质量,在质量较差时介入矫正,提供较为理想的电源输出后备式:即UPS只在市电不稳定或断开时介入,提供备用电源输出
一般而言,上述三种类型输出电源质量递减、价格也递减,当然额外功耗也递减。
如今随着技术进步,后两种的差异正越来越模糊,一些高端的 后备式不间断电源 也开始具备自动稳压功能。就我的观察,按电池模式下输出是否为 纯正正弦波 来区分更为方便,能够在电池模式下输出 纯正正弦波 的往往质量比较好。
因为 纯正正弦波 对主动式 PFC 电源较为友好1,且 在线互动式 相比于 在线式 在日常的附加功耗及噪音都更小,因此我将目标瞄向了提供 纯正正弦波 的 在线互动式 不间断电源。
对于不常停电且供电较稳定的地区,大部分时候直接使用市电,仅在供电不稳定时进行自动稳压,可以在大多数时刻获得较高的能量利用率,而偶尔的停电,可以在 10ms 内切换到电池模式,对于具有 16ms2 的 Hold-up Time 的 ENP-7025B 电源来说可以接受。
遍观淘宝、京东,稍有名气的牌子有3:
- 施耐德 APC
- 硕天 CyberPower
- 山特 SANTAK
- 雷迪司 LADS
- 普迪斯盾 PDSD
罗列它们热销或在售产品4的主要参数如下:
| 品牌 | 型号 | 拓扑 | 规格 | 输出波形 | 防浪涌 | 切换时间 | 通信支持 | 价格 | 备注 |
|---|---|---|---|---|---|---|---|---|---|
| APC | BK650M2-CH | 后备式 | 650VA/390W | 逼近正弦波 | $310J$ | Typ. $6ms$ Max. $10ms$ | NAS | ¥450 | - |
| APC | SUA750I-CH | 在线互动式 | 750VA/500W | 纯正正弦波 | $340J$ | Typ. $2ms$ | - | ¥1000 | 已退市 |
| APC | SMT750I-CH | 在线互动式 | 750VA/500W | 纯正正弦波 | $540J$ | Typ. $6ms$ | - | ¥1300 | - |
| APC | SMVS1000I-CN | 在线互动式 | 1000VA/700W | 纯正正弦波 | $190J$5 | Typ. $6ms$ Max. $12ms$ | - | ¥1100 | 已停产 |
| CyberPower | R1200ELCDOR | 在线互动式 | 1200VA/720W | 逼近正弦波? | 有 | Typ. $4ms$ | NAS | ¥600 | - |
| SANTAK | S2200-PRO | 在线互动式 | 2000VA/1200W | 逼近正弦波 | 有 | Typ. $4\sim 8ms$ | - | ¥600 | - |
| SANTAK | TG-BOX 850 | 后备式 | 850VA/510W | 逼近正弦波 | 有 | - | NAS | ¥450 | - |
| LADS | H1000M | 后备式 | 1000VA/600W | 逼近正弦波 | 有 | Typ. $6ms$ Max. $10ms$ | NAS | ¥250 | - |
| PDSD | BK1000 | 后备式 | 1000VA/600W | 方波 | 有 | - | NAS | ¥250 | - |
APC 的 在线互动式 不间断电源是我纠结最久的,前前后后做了相当多的调查。它的电源参数能够比较好的满足我的需求,除了两点问题,一个是贵、另一个是对其与 NAS 通信的能力存疑。
比如,我在B站看到一个介绍视频,其用来演示的就是 SMVS 系列的 SMVS3000I-CN 不间断电源,且 UP 明确说其可以与 FreeNAS 文件系统通信,但询问卖家都是回复不能与 NAS 通信。
又比如,我找到一篇上古博文,其中老外使用的就是 Smart UPS 750 型号的不间断电源,但是考虑到不确定老外所处地区,中国地区是否有相匹配的型号,因此始终无法实锤。
最终,只能无奈联系 APC 客服,可遗憾的被告知需要网络通信卡,比如最低端的 AP9620 在某鱼也要 $200\sim 300$ 元。

对于 CyberPower 的 R1200ELCDOR,从参数和商品描述上看似乎符合要求,但是我在其官网没有搜到这款型号,且商品的描述也不够详细,不能给人信任感,总是怀疑它数据不真实、或者在玩文字游戏。且这个价位,没有看到第二家有明确说支持 纯正正弦波 输出,因此更倾向于目前的技术还不能做到。
其实,对于没有这种通信能力的不间断电源,还有一个终极方案——基于网络连通性测试部署。将网关设备(如光猫或路由器)的供电独立于不间断电源,这样当市电中断后网关就会断开,而受保护的服务器就无法 ping 通网关,进而可以判断出电力中断,可以进行优雅的关机。
但这种方案也有不少缺点:
- 若网络临时故障,可能会导致服务器误判
- 没有维护良好的开源脚本,需要投入精力维护
- 多设备协同、控制关机顺序等操作实现较为复杂
因此,为什么要花这么多钱,走这么荆棘的路呢?人生不值得!
思虑良久,我还是决定放弃 纯正正弦波 的要求,转而寻找直接支持 NAS 通信的型号,嚯,果然前路豁然宽敞~
考虑到经过这段时间的撩拨,我对 APC 品牌相对熟悉,且大量的教程向博客和视频都选择了 BK650,故而我也就从善如流,选择了其最新一代 BK650M2-CH。
开箱
快递纸箱到手较为完整,除了边角有点磕碰痕迹,其它没有什么问题。

拆开纸箱,内部有两个包装,一个是主体,另一个是赠品。

全家福如下。

物品清单:
| 品名 | 数量 | 备注 |
|---|---|---|
| 电源主体 | 1 | |
| 三孔电源线 | 1 | |
| RJ45-USB线 | 1 | |
| 使用说明 | 1 | |
| 合格证 | 1 | |
| 10A-16A转换插头 | 1 | 赠品 |
部署
部署方案为:UPS 连接物理机并与 PVE 通信,PVE 作为 NUT 主设备,TrueNAS 作为 NUT 从设备,在收到 UPS 掉电信号后,TrueNAS 主动关机,PVE 会同时关闭其它虚拟机,最后再关机。
硬件
连接
直通
因为 APC BK650M2-CH 需要通过 USB 连接通信,因此需确保其通信对象具有对 USB controller 的访问权,如果已经将 USB controller 直通到某虚拟机,需要先修改直通配置。我这里使用宿主机作为主设备,因此保证 USB controller 未被直通给其它虚拟机即可。
软件
PVE
PVE 8.3.0 PASSED;
[可选] 使用 apcupsd 接收 UPS 信息
- 安装
apcupsd
apt update && apt install apcupsd -y- 编辑
/etc/apcupsd/apcupsd.conf
UPSNAME UPS1
UPSCABLE usb
UPSTYPE usb
DEVICE # 注意此处留空
LOCKFILE /var/lock
ONBATTERYDELAY 6 # 掉电后等待多少秒再认为是“断电状态”,防止电源闪断
BATTERYLEVEL 50 # 当电池电量低于这个百分比时执行关机
MINUTES 15 # 预估 `UPS` 还能支撑多长时间时执行关机
TIMEOUT 0 # UPS 断电后最多运行多少秒后强制关机(0 表示不启用)
ANNOY 300 # 用户登录后每隔多长时间提醒一次电源问题,设 0 禁用
ANNOYDELAY 60 # 掉电后多久开始提醒用户
NOLOGON disable # 掉电期间是否允许新用户登录
KILLDELAY 0 # 关机信号后多长时间强制断电
NETSERVER on # 启用 `apcupsd` 网络服务(供其他从设备查询 UPS 状态)
NISIP 0.0.0.0 # `apcupsd` 监听的 `IP` 地址
NISPORT 3551 # `apcupsd` 网络监听端口- 启动服务
systemctl enable apcupsd
systemctl start apcupsd- 确认状态
apcaccess status- 停止并卸载
apcupsd
systemctl stop apcupsd # 停止
apt-get remove --purge apcupsd -y # 卸载
systemctl daemon-reload # 刷新注意:
- 此步中若
apcaccess status的返回状态为STATUS : COMMLOST,应是配置文件中DEVICE属性未留空,而是默认值,改为留空即可6。
使用 NUT 作为 Master 接收 UPS 信息
因为需要多台设备共享 UPS 状态数据,需要使用 Network UPS Tools(NUT) 工具,所以前面的 apcupsd 就是非必须配置了。
- 安装
NUT
apt update && apt install nut -y- 编辑
/etc/nut/nut.conf以启用功能
MODE=netserver- 编辑
/etc/nut/ups.conf以配置主设备
[bk650m2]
driver = usbhid-ups
port = auto
desc = "APC UPS"
override.battery.charge.low = 50 # 电量低于该值认为低电量
override.battery.runtime.low = 300 # 剩余时间低于该值认为低电量
override.input.sensitivity = medium # 输入切换(市电/电池)敏感度
# override.input.transfer.low = 180
# override.input.transfer.high = 266
# override.input.voltage.nominal = 220
override.ups.delay.shutdown = 20 # 触发低电量延迟关机时间
override.ups.beeper.status = enable- 编辑
/etc/nut/upsd.conf以配置服务
LISTEN 0.0.0.0 3493 # 无法设置子网- 编辑
/etc/nut/upsd.users以配置权限
# 用于 `PVE` 接入认证
[admin]
password = apcbk650m2
upsmon primary
actions = SET FSD
instcmds = ALL
# 用于 `TrueNAS` 接入认证
[upsmon]
password = fixmepass
upsmon secondary- 创建
PVE关机脚本
tee /usr/local/bin/nut_shutdown.sh <<'EOF'
#!/bin/bash
LOG_FILE="/var/log/nut_shutdown.log"
TRUENAS_VMID="101" # TrueNAS will turn it off on its own
TRUENAS_BLOCKING_TIME=240
NEED_WAIT=false
{
echo "===== 准备关机 [$(date)] ====="
echo "排除的虚拟机ID: $TRUENAS_VMID"
echo "=== 阶段1: 逐个发送优雅关闭信号 ==="
# 1.1 关闭所有容器
for ct in $(pct list | awk '$2 == "running" {print $1}'); do
if pct exec "$ct" -- bash -c "shutdown -h now" >/dev/null 2>&1; then
echo "[容器] 已发送关机信号: $ct"
NEED_WAIT=true
else
echo "[容器] 警告: $ct 内 shutdown 命令失败"
fi
done
# 1.2 关闭所有虚拟机(排除 TrueNAS)
for vmid in $(qm list | awk -v exclude="$TRUENAS_VMID" '$3 == "running" && $1 != exclude {print $1}'); do
if qm shutdown "$vmid" --timeout 60 >/dev/null 2>&1; then
echo "[虚拟机] 已发送ACPI信号: $vmid"
NEED_WAIT=true
else
echo "[虚拟机] 警告: $vmid ACPI不可用"
fi
done
echo "=== 阶段2: 按需等待80秒 ==="
if $NEED_WAIT; then
echo "等待中 ..."
sleep 80
fi
echo "=== 阶段3: 逐个强制关闭 ==="
# 3.1 强制关闭未停止的容器
pct list | awk '$2 == "running" {print $1}' | while read ct; do
if pct stop "$ct" --overrule-shutdown --skiplock >/dev/null 2>&1; then
echo "[容器] 已强制关闭: $ct"
else
echo "[容器] 错误: $ct 强制关闭失败"
fi
done
# 3.2 强制关闭未停止的虚拟机(排除 TrueNAS)
qm list | awk -v exclude="$TRUENAS_VMID" '$3 == "running" && $1 != exclude {print $1}' | while read vmid; do
if qm stop "$vmid" --skiplock >/dev/null 2>&1; then
echo "[虚拟机] 已强制关闭: $vmid"
else
echo "[虚拟机] 错误: $vmid 强制关闭失败"
fi
done
echo "=== 阶段4: 等待TruNAS关机(最多 $TRUENAS_BLOCKING_TIME 秒) ==="
TRUENAS_STATUS=$(qm status "$TRUENAS_VMID" | awk '{print $2}')
if [[ "$TRUENAS_STATUS" == "running" ]]; then
echo "[TruNAS] 仍在运行,等待关机..."
START_TIME=$(date +%s)
TIMEOUT=$(( START_TIME + TRUENAS_BLOCKING_TIME ))
while [[ $(date +%s) -lt $TIMEOUT ]]; do
TRUENAS_STATUS=$(qm status "$TRUENAS_VMID" | awk '{print $2}')
if [[ "$TRUENAS_STATUS" != "running" ]]; then
echo "[TruNAS] 已关机"
break
fi
sleep 5 # 每5秒检查一次
done
if [[ "$TRUENAS_STATUS" == "running" ]]; then
echo "[TruNAS] 错误: 等待超时(${TRUENAS_BLOCKING_TIME}秒)仍未关闭!"
fi
else
echo "[TruNAS] 未运行,无需等待"
fi
echo "===== 准备完成,即将关闭宿主机 [$(date)] ====="
systemctl poweroff # 使用 `shutdown -h now` 可能只关机不断电
} | tee -a "$LOG_FILE"
EOF
chmod +x /usr/local/bin/nut_shutdown.sh
chown nut:nut /usr/local/bin/nut_shutdown.sh注意:
halt: 停止所有CPU及系统操作,但通常不断电poweroff: 先执行halt再断电shutdown -h now依据发行版实现不同,可能会调用halt或者poweroff
- 创建
PVE紧急关机脚本
tee /usr/local/bin/nut_emergency_shutdown.sh <<'EOF'
#!/bin/bash
LOG_FILE="/var/log/nut_emergency_shutdown.log"
NEED_WAIT=false
{
echo "===== 准备紧急关机 [$(date)] ====="
echo "=== 阶段1: 逐个发送优雅关闭信号 ==="
# 1.1 关闭所有容器
for ct in $(pct list | awk '$2 == "running" {print $1}'); do
if pct exec "$ct" -- bash -c "shutdown -h now" >/dev/null 2>&1; then
echo "[容器] 已发送关机信号: $ct"
NEED_WAIT=true
else
echo "[容器] 警告: $ct 内 shutdown 命令失败"
fi
done
# 1.2 关闭所有虚拟机
for vmid in $(qm list | awk '$3 == "running" {print $1}'); do
if qm shutdown "$vmid" --timeout 60 >/dev/null 2>&1; then
echo "[虚拟机] 已发送ACPI信号: $vmid"
NEED_WAIT=true
else
echo "[虚拟机] 警告: $vmid ACPI不可用"
fi
done
echo "=== 阶段2: 按需等待80秒 ==="
if $NEED_WAIT; then
echo "等待中 ..."
sleep 80
fi
echo "=== 阶段3: 逐个强制关闭 ==="
# 3.1 强制关闭未停止的容器
pct list | awk '$2 == "running" {print $1}' | while read ct; do
if pct stop "$ct" --overrule-shutdown --skiplock >/dev/null 2>&1; then
echo "[容器] 已强制关闭: $ct"
else
echo "[容器] 错误: $ct 强制关闭失败"
fi
done
# 3.2 强制关闭未停止的虚拟机
qm list | awk '$3 == "running" {print $1}' | while read vmid; do
if qm stop "$vmid" --skiplock >/dev/null 2>&1; then
echo "[虚拟机] 已强制关闭: $vmid"
else
echo "[虚拟机] 错误: $vmid 强制关闭失败"
fi
done
echo "===== 准备完成,即将关闭宿主机 [$(date)] ====="
systemctl poweroff
} | tee -a "$LOG_FILE"
EOF
chmod +x /usr/local/bin/nut_emergency_shutdown.sh
chown nut:nut /usr/local/bin/nut_emergency_shutdown.sh- 编辑
/etc/nut/upsmon.conf以配置监控
MONITOR bk650m2@localhost 1 admin apcbk650m2 primary
# 需要直接关机时被调用
SHUTDOWNCMD "/usr/local/bin/nut_emergency_shutdown.sh"
# 基于事件通知调度,可灵活自定义
NOTIFYCMD /sbin/upssched
NOTIFYFLAG ONLINE SYSLOG+EXEC
NOTIFYFLAG ONBATT SYSLOG+EXEC- 编辑
/etc/nut/upssched.conf以配置调度计划
CMDSCRIPT /etc/nut/upssched-cmd.sh
# 打开如下被注释的配置(涉及被攻击风险,最好读一下相应的配置说明)
PIPEFN /run/nut/upssched/upssched.pipe
LOCKFN /run/nut/upssched/upssched.lock # 自 v1.2.1 引入
AT ONBATT * START-TIMER shutdown 80 # 市电断开,启动 80s 定时器
AT ONLINE * CANCEL-TIMER shutdown # 市电恢复,取消定时器注意,对于 Debian 系的系统 /run/nut/ 属于 tmpfs 文件系统,重启会丢失目录,导致出现权限问题,因此参考该博客建议改为如下配置:
PIPEFN /run/nut/upssched.pipe
LOCKFN /run/nut/upssched.lock- 创建调度入口
# PVE 默认用户都操作 root,因此未安装 sudo
apt update && apt install sudo -y
# 让 nut 用户具备无密码提权执行特定脚本的
echo "nut ALL=(ALL) NOPASSWD: /usr/local/bin/nut_shutdown.sh" | tee /etc/sudoers.d/nut_shutdown
# sudoers 文件必须只读,否则 sudo 将拒绝加载
chmod 440 /etc/sudoers.d/nut_shutdown
cat > /etc/nut/upssched-cmd.sh <<'EOF'
#!/bin/sh
# set -ex # 出错退出且命令回显
case $1 in
shutdown)
logger -t upssched-cmd "当前用户: $(whoami)"
logger -t upssched-cmd "UPS市电断开超时,触发关机"
/usr/bin/sudo /usr/local/bin/nut_shutdown.sh
;;
*)
logger -t upssched-cmd "未知指令: $1"
;;
esac
EOF
chmod +x /etc/nut/upssched-cmd.sh注:
sudoers是一个用来控制用户、组等是否能提权执行特定命令的配置工具,而作为对比
su可直接切换到root(需密码),权限控制较弱sudo常用于赋予该组按需提权的能力,直观便于审计,较为安全
- 启动服务
systemctl enable nut-server
systemctl start nut-server
# systemctl status nut-server
# 用于本地监控
systemctl enable nut-client
systemctl start nut-client
# systemctl status nut-client- 重启服务
systemctl restart nut-server nut-client- 测试输出
upsc bk650m2TrueNAS
System Settings->Services->UPS->Configure7

- 检查通信
upsc [email protected]System Settings->Services->UPS- 勾选
Running - 勾选
Start Automatically
- 勾选
Debian LXC
因为 LXC 容器关闭时有时会卡住,为了提升其关闭成功率,使用 NUT 客户端进行提前关闭。
- 安装
NUT
apt update && apt install nut -y- 编辑
/etc/nut/nut.conf以启用功能
MODE=netclientMONITOR [email protected] 1 upsmon fixmepass secondary
# 需要直接关机时被调用
SHUTDOWNCMD "/sbin/shutdown -h +0"
# 基于事件通知调度,可灵活自定义
NOTIFYCMD /sbin/upssched
NOTIFYFLAG ONLINE SYSLOG+EXEC
NOTIFYFLAG ONBATT SYSLOG+EXEC
FINALDELAY 0- 编辑
/etc/nut/upssched.conf以配置调度计划
CMDSCRIPT /etc/nut/upssched-cmd.sh
PIPEFN /run/nut/upssched.pipe
LOCKFN /run/nut/upssched.lock
AT ONBATT * START-TIMER shutdown 10
AT ONLINE * CANCEL-TIMER shutdown- 创建调度入口
# 若无 sudo 需安装
apt update && apt install sudo -y
# 让 nut 用户具备无密码提权执行特定脚本
echo "nut ALL=(ALL) NOPASSWD: /sbin/shutdown" | tee /etc/sudoers.d/nut_shutdown
# sudoers 文件必须只读,否则 sudo 将拒绝加载
chmod 440 /etc/sudoers.d/nut_shutdown
cat > /etc/nut/upssched-cmd.sh <<'EOF'
#!/bin/sh
# set -ex # 出错退出且命令回显
case $1 in
shutdown)
logger -t upssched-cmd "当前用户: $(whoami)"
logger -t upssched-cmd "UPS市电断开超时,触发关机"
/usr/bin/sudo /sbin/shutdown -h now
;;
*)
logger -t upssched-cmd "未知指令: $1"
;;
esac
EOF
chmod +x /etc/nut/upssched-cmd.sh- 启动服务
systemctl enable nut-client
systemctl start nut-client测试
在 PVE 终端执行如下命令,可以模拟发出强制关机指令以关闭整套系统。
upsmon -c fsd # forced shutdown或者拔插市电插头,以验证关机流程。
- 短期拔插市电,模拟短期停电(期望系统一切运行正常)

可以看到 UPS 中途报告了电池/市电模式的状态切换,系统运行如常。
- 断开市电 $80s$ 以上,再接通市电(期望系统顺序关闭,并最后切断电源)

可以看到正常触发关机,且依次关闭 LXC 容器、虚拟机以及最后的 PVE 宿主机并断电。