目录

「UPS」再也不怕拉闸了

缘起

前一段时间家里部署光伏发电系统,在并网时工作人员没有任何通知就断开了市电,导致服务器异常关机。另外一个就是夏天突发打雷可能会导致家里跳闸,之前都是靠家人日常关注天气情况,并在雷雨天提前关机,比较麻烦且操心。

因此不间断电源,必须整一个啦!

选型

选型的过程很艰难,成年人往往想全都要,奈何自古成年无两全!

初步了解了不间断电源(Uninterruptible Power Supply, UPS),大体分三类:在线式(Online)、在线互动式(Line-Interactive)、后备式(Back)。简单理解,

  • 在线式:即 UPS 时刻在线优化电信号,提供理想的电源输出
  • 在线互动式:即 UPS 时刻检查市电质量,在质量较差时介入矫正,提供较为理想的电源输出
  • 后备式:即 UPS 只在市电不稳定或断开时介入,提供备用电源输出

一般而言,上述三种类型输出电源质量递减、价格也递减,当然额外功耗也递减。

如今随着技术进步,后两种的差异正越来越模糊,一些高端的 后备式不间断电源 也开始具备自动稳压功能。就我的观察,按电池模式下输出是否为 纯正正弦波 来区分更为方便,能够在电池模式下输出 纯正正弦波 的往往质量比较好。

因为 纯正正弦波 对主动式 PFC 电源较为友好1,且 在线互动式 相比于 在线式 在日常的附加功耗及噪音都更小,因此我将目标瞄向了提供 纯正正弦波在线互动式 不间断电源。

对于不常停电且供电较稳定的地区,大部分时候直接使用市电,仅在供电不稳定时进行自动稳压,可以在大多数时刻获得较高的能量利用率,而偶尔的停电,可以在 10ms 内切换到电池模式,对于具有 16ms2Hold-up TimeENP-7025B 电源来说可以接受。

遍观淘宝、京东,稍有名气的牌子有3

  • 施耐德 APC
  • 硕天 CyberPower
  • 山特 SANTAK
  • 雷迪司 LADS
  • 普迪斯盾 PDSD

罗列它们热销或在售产品4的主要参数如下:

品牌型号拓扑规格输出波形防浪涌切换时间通信支持价格备注
APCBK650M2-CH后备式650VA/390W逼近正弦波$310J$Typ. $6ms$
Max. $10ms$
NAS¥450-
APCSUA750I-CH在线互动式750VA/500W纯正正弦波$340J$Typ. $2ms$-¥1000已退市
APCSMT750I-CH在线互动式750VA/500W纯正正弦波$540J$Typ. $6ms$-¥1300-
APCSMVS1000I-CN在线互动式1000VA/700W纯正正弦波$190J$5Typ. $6ms$
Max. $12ms$
-¥1100已停产
CyberPowerR1200ELCDOR在线互动式1200VA/720W逼近正弦波?Typ. $4ms$NAS¥600-
SANTAKS2200-PRO在线互动式2000VA/1200W逼近正弦波Typ. $4\sim 8ms$-¥600-
SANTAKTG-BOX 850后备式850VA/510W逼近正弦波-NAS¥450-
LADSH1000M后备式1000VA/600W逼近正弦波Typ. $6ms$
Max. $10ms$
NAS¥250-
PDSDBK1000后备式1000VA/600W方波-NAS¥250-

APC在线互动式 不间断电源是我纠结最久的,前前后后做了相当多的调查。它的电源参数能够比较好的满足我的需求,除了两点问题,一个是贵、另一个是对其与 NAS 通信的能力存疑。

比如,我在B站看到一个介绍视频,其用来演示的就是 SMVS 系列的 SMVS3000I-CN 不间断电源,且 UP 明确说其可以与 FreeNAS 文件系统通信,但询问卖家都是回复不能与 NAS 通信。

又比如,我找到一篇上古博文,其中老外使用的就是 Smart UPS 750 型号的不间断电源,但是考虑到不确定老外所处地区,中国地区是否有相匹配的型号,因此始终无法实锤。

最终,只能无奈联系 APC 客服,可遗憾的被告知需要网络通信卡,比如最低端的 AP9620 在某鱼也要 $200\sim 300$ 元。

https://store.yirami.xyz/review/ups/apc_customer_service.jpg

对于 CyberPowerR1200ELCDOR,从参数和商品描述上看似乎符合要求,但是我在其官网没有搜到这款型号,且商品的描述也不够详细,不能给人信任感,总是怀疑它数据不真实、或者在玩文字游戏。且这个价位,没有看到第二家有明确说支持 纯正正弦波 输出,因此更倾向于目前的技术还不能做到。

其实,对于没有这种通信能力的不间断电源,还有一个终极方案——基于网络连通性测试部署。将网关设备(如光猫或路由器)的供电独立于不间断电源,这样当市电中断后网关就会断开,而受保护的服务器就无法 ping 通网关,进而可以判断出电力中断,可以进行优雅的关机。

但这种方案也有不少缺点:

  • 若网络临时故障,可能会导致服务器误判
  • 没有维护良好的开源脚本,需要投入精力维护
  • 多设备协同、控制关机顺序等操作实现较为复杂

因此,为什么要花这么多钱,走这么荆棘的路呢?人生不值得!

思虑良久,我还是决定放弃 纯正正弦波 的要求,转而寻找直接支持 NAS 通信的型号,嚯,果然前路豁然宽敞~

考虑到经过这段时间的撩拨,我对 APC 品牌相对熟悉,且大量的教程向博客和视频都选择了 BK650,故而我也就从善如流,选择了其最新一代 BK650M2-CH

开箱

快递纸箱到手较为完整,除了边角有点磕碰痕迹,其它没有什么问题。

https://store.yirami.xyz/review/ups/apc_unpacking_0.jpg

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

https://store.yirami.xyz/review/ups/apc_unpacking_1.jpg

全家福如下。

https://store.yirami.xyz/review/ups/apc_unpacking_2.jpg

物品清单:

品名数量备注
电源主体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  # 刷新

注意:

  1. 此步中若 apcaccess status 的返回状态为 STATUS : COMMLOST,应是配置文件中 DEVICE 属性未留空,而是默认值,改为留空即可6
使用 NUT 作为 Master 接收 UPS 信息

因为需要多台设备共享 UPS 状态数据,需要使用 Network UPS Tools(NUT) 工具,所以前面的 apcupsd 就是非必须配置了。

  • 安装 NUT
apt update && apt install nut -y
MODE=netserver
[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
LISTEN 0.0.0.0 3493  # 无法设置子网
# 用于 `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
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
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 bk650m2
TrueNAS
  • System Settings -> Services -> UPS -> Configure7

https://store.yirami.xyz/review/ups/truenas_cfg.png

  • 检查通信
  • System Settings -> Services -> UPS
    • 勾选 Running
    • 勾选 Start Automatically
Debian LXC

因为 LXC 容器关闭时有时会卡住,为了提升其关闭成功率,使用 NUT 客户端进行提前关闭。

  • 安装 NUT
apt update && apt install nut -y
MODE=netclient
MONITOR [email protected] 1 upsmon fixmepass secondary

# 需要直接关机时被调用
SHUTDOWNCMD "/sbin/shutdown -h +0"

# 基于事件通知调度,可灵活自定义
NOTIFYCMD /sbin/upssched
NOTIFYFLAG ONLINE	SYSLOG+EXEC
NOTIFYFLAG ONBATT	SYSLOG+EXEC

FINALDELAY 0
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

或者拔插市电插头,以验证关机流程。

  1. 短期拔插市电,模拟短期停电(期望系统一切运行正常)

https://store.yirami.xyz/review/ups/short-term_power_outage.png

可以看到 UPS 中途报告了电池/市电模式的状态切换,系统运行如常。

  1. 断开市电 $80s$ 以上,再接通市电(期望系统顺序关闭,并最后切断电源)

https://store.yirami.xyz/review/ups/mains_power_outage.png

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


  1. 参考这里的探讨 ↩︎

  2. 电源数据手册参见这里 ↩︎

  3. 对于品牌名称相近的,仅取名气最大的,其余(未经调查)默认按蹭热度行为进行排除 ↩︎

  4. 数据截止 2025 年 4 月 ↩︎

  5. 该型号该数据缺失,借用 SMVS1000CAI 的信息 ↩︎

  6. 配置文件中有一段描述 “Most new UPSes are USB. A blank DEVICE setting enables autodetection, which is the best choice for most installations.” ↩︎

  7. 从模式下,延时控制相关设置似乎无效 ↩︎