搬完家的第一週,伺服器開始每天半夜自己關機。

不是當機、不是重開,是斷電式的死亡:回來開機一定跑 ext4 recovery,uptime 歸零,前一秒還在跑的東西沒留下任何遺言。而且它挑時間,幾乎都在清晨。

我第一時間想的是「線鬆了」,去壓緊,隔天照死。想到搬家後冷氣和熱水器可能跟它共用迴路,關掉試,照死。量溫度,50~60°C,正常。最後懷疑是搬家把 PSU 震壞了——那是一顆買不到一年的振華白金牌,我不太想相信。

問題是這些都是猜測,而每次驗證要等一整天。查了幾天之後我意識到真正缺的不是假設,是證據:我根本不知道死掉的那一瞬間,送進機器的電是什麼樣子。

所以我去買了一台 UPS,一半是為了保護,一半是把它當量測儀器用

這篇是後來的完整過程:UPS 怎麼選、NUT 從零設定到自動安全關機、BIOS 那個一定要設對的選項,以及最後靠 UPS 的 log 破了案——兇手是一條被貓咬過的電源線。

為什麼一台 UPS 能定案#

先講清楚它為什麼是儀器,因為這決定了整篇的邏輯。

UPS 量測的是它自己輸出端的狀態,而它會用幾個代號回報自己現在在幹嘛。整篇會一直看到,先講清楚:

狀態意思
OLOn Line,市電正常,UPS 只是在旁邊待命
OBOn Battery,市電沒了,正在放電池
LBLow Battery,電池快沒了,該關機了

有了這三個代號,只要 UPS 持續記錄,下次再死就能一刀切開兩種可能:

  • 死亡當下記到 OB 或電壓掉 → 問題在 UPS 上游,市電有事。
  • 死亡當下全程 OL、電壓穩、電池滿 → UPS 送出去的電是乾淨的,問題在它下游,也就是電源線、PSU、主機板這一段。

這條分界線把搜尋範圍砍掉一半,而且不用再靠猜。

選型:1500VA / 純正弦波 / 有 AVR#

這顆 CyberPower CP1500PFCLCD 有三個對這次診斷有意義的點:

PFC 純正弦波輸出

現在的主機 PSU 幾乎都是主動式 PFC。這類 PSU 遇到便宜 UPS 的,在切換到電池的瞬間可能直接關機——那會製造出跟我正在追的症狀一模一樣的新問題。診斷期間最不需要的就是這個。

內建 AVR

直接處理掉一個假設:如果清晨真的有市電電壓跳動,它會當場壓平。壓平之後如果還死,那市電這條就能排除。

NUT 完整支援

Linux 上的 usbhid-ups 驅動就能讀它,不需要廠商的專屬軟體。USB 插上去 lsusb 會看到:

console
Bus 001 Device 005: ID 0764:0601 Cyber Power System, Inc.

NOTE

網路線不用接 這台 UPS 同時有 USB 和序列埠,而 NUT 走 USB 就夠了。網路那條是給有網管卡的機種或多機共享用的,單機監控用不到。

NUT:從零到自動安全關機#

NUT 的設定檔散在好幾個地方,而且權限沒設對服務就起不來。我把整套寫成一支腳本,重灌或換機時直接重跑。

先搞懂 NUT 的三層#

設定檔之所以看起來很亂,是因為 NUT 不是一支程式,是三支各司其職的程式。搞懂誰是誰,設定檔就自己對號入座了:

  • driver(我這台是 usbhid-ups)——真正跟 UPS 講話的那支,把廠商的協定翻成 NUT 的資料。
  • upsd——把 driver 拿到的資料開成一個服務,讓別人來問。
  • upsmon——用戶端。它一直跟 upsd 要資料,看到電量低就執行關機。

所以那五個設定檔是這樣分的:

bash
/etc/nut/nut.conf      # 這台扮演什麼角色(單機還是主從)
/etc/nut/ups.conf      # driver:用哪個驅動、接哪台 UPS
/etc/nut/upsd.conf     # upsd:資料服務聽哪個位址
/etc/nut/upsd.users    # upsd:誰能讀、誰能下指令
/etc/nut/upsmon.conf   # upsmon:什麼情況下關機、怎麼通知

單機監控用 standalone:

ini
# /etc/nut/nut.conf
MODE=standalone
ini
# /etc/nut/ups.conf
maxretry = 3
pollinterval = 5

[cyberpower]
    driver = usbhid-ups
    port = auto
    desc = "CyberPower CP1500PFCLCD"
ini
# /etc/nut/upsd.conf —— 只聽 localhost,不對外
LISTEN 127.0.0.1 3493

WARNING

LISTEN 預設值要改 NUT 的 upsd 如果聽在 0.0.0.0,區網任何人都能查詢你的 UPS 狀態、甚至在權限沒設好時下指令。單機監控沒有任何理由對外,綁 127.0.0.1 就好。密碼我是用 openssl rand -hex 16 隨機產生後寫進檔案,反正不用人記。

權限也要對,不然 driver 讀不到設定:

bash
chown root:nut /etc/nut/*.conf /etc/nut/upsd.users
chmod 640 /etc/nut/*.conf /etc/nut/upsd.users

安全關機是怎麼發生的#

重點在 upsmon.conf 這幾行:

ini
MONITOR cyberpower@localhost 1 upsmon <password> primary
MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h +0"
POWERDOWNFLAG /etc/killpower
POLLFREQ 5
POLLFREQALERT 5
FINALDELAY 5

一行一行看:

指令在做什麼
MONITOR要監看哪台 UPS、用哪組帳密。結尾的 primary 代表這台機器有權在關機前叫 UPS 一起斷電
MINSUPPLIES至少要有幾組電源還活著才繼續跑。單電源主機填 1
SHUTDOWNCMD決定關機時執行什麼
POWERDOWNFLAG關機流程留下的旗標檔,下面會專門講
POLLFREQ / POLLFREQALERT平常幾秒問一次 upsd、出事後幾秒問一次
FINALDELAY送出關機指令前最後緩衝幾秒

實際流程是這樣,而最後一段是最多人漏掉的:

POWERDOWNFLAG 指到的是一個旗標檔upsmon 決定要關機時會先建立它,而系統關機腳本跑到最後會看一眼:檔案在,就順便叫 UPS 把自己的輸出也切斷。

不這樣做的話,機器關了但 UPS 還在放電,電池會被榨到 0,而鉛酸電池深度放電一次就折壽一次。

BIOS 自動開機那一格如果沒設,前面全部白做——機器會乾淨地關機,然後停在那裡等人按電源鍵。

關機門檻的取捨#

觸發點是 UPS 自己認定的低電量。我這台的原廠值是:

console
battery.charge.low: 10        # 電量剩 10%
battery.runtime.low: 300      # 或估計剩餘 300 秒

先用原廠值是刻意的,因為它最不會誤判。跑一陣子有真實數字之後再決定要不要調早。我現在的實測是:

12%
UPS 負載
額定 1000W
61分鐘
估計續航
113V
輸入電壓

負載 12%、續航一小時,剩 5 分鐘才開始關機的緩衝完全夠。但如果你的機器把 UPS 吃到 60%、續航只剩十幾分鐘,那 5 分鐘就有點緊,可以用 upssched 加一道更早的觸發(例如「吃電池滿 90 秒」就先關)。

BIOS:那個不能選錯的選項#

這是整套自動復原裡最容易踩雷的一格。

CAUTION

Restore after AC Power Loss 要選 On,不能選 Last State Last State 的意思是「回到斷電前的狀態」。而 NUT 低電量關機之後,機器的狀態就是關機——市電回來時 BIOS 會判定「它本來就是關的」,於是保持關機。你做了一整套自動復原,結果卡在最後一步。 選 On 才是「只要有電就開機」,不管之前是開是關。

我的板子是 MSI MAG X870E TOMAHAWK,路徑是:

text
開機按 Del → F7 切到 Advanced Mode
  → SETTINGS → Advanced → Power Management Setup
    → Restore after AC Power Loss = On

BIOS 在作業系統之前,SSH 和遠端桌面都看不到,一定要接實體螢幕鍵盤。想省去狂按 Del 可以先下:

bash
sudo systemctl reboot --firmware-setup

它會直接重開進 BIOS 設定畫面,但人還是得在螢幕前。

把 UPS 狀態接出來#

設定完之後 upsc 隨時查得到,但我不想每次都 SSH 進去打指令。

每分鐘留一筆紀錄#

這支腳本掛 cron 每分鐘跑一次,它同時做兩件事:寫一行人看的 log,和一份給程式讀的 JSON。

bash
DATA=$(upsc cyberpower 2>/dev/null) || exit 0
g() { grep -m1 "^$1:" <<<"$DATA" | cut -d' ' -f2-; }

echo "$(date '+%F %T') status=$(g ups.status) in=$(g input.voltage)V \
out=$(g output.voltage)V load=$(g ups.load)% batt=$(g battery.charge)% \
runtime=$(g battery.runtime)s" >> "$OUT"

那行 log 長這樣,而它就是後來破案的東西:

console
2026-07-05 00:12:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
2026-07-05 00:13:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
2026-07-05 00:14:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
                    ↑ 機器就在這一分鐘之後硬斷電

事件推到 Discord#

upsmon.conf 支援對每種事件掛外部指令。做法是加一行 NOTIFYCMD,再把要通知的事件旗標加上 +EXEC:

ini
NOTIFYCMD /path/to/ups-discord-notify.sh
NOTIFYFLAG ONBATT   SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG ONLINE   SYSLOG+WALL+EXEC

推的事件有六種:切換到電池、電量低即將關機、觸發系統關機、市電恢復、電池老化該換、與 UPS 失聯。每則附上即時電量、剩餘分鐘、負載和市電電壓。

Webhook 檔案要放在 upsmon 讀得到的地方,權限是 root:nut 640——這是我第一次弄的時候卡住的點,放在家目錄底下 upsmon 讀不到。

不想再開一個子網域#

原本想過用 Home Assistant 的 NUT 整合,或架個 peaNUT 之類的面板。但我的子網域已經一堆了,為了看一顆 UPS 再開一個網站很多餘

所以改成把它塞進我自己的 NAS 面板。這裡有個限制:後端跑在 docker bridge 網路裡,連不到 host 的 upsd:3493。與其把 upsd 對外暴露,不如讓 host 寫檔、容器讀檔:

JSON 就是上面那支腳本順手產的:

json
{
  "status": "OL",
  "online": true,
  "battery_charge": 100,
  "battery_runtime": 3675,
  "ups_load": 12,
  "input_voltage": 113.0,
  "model": "CP1500PFCLCDa TW",
  "updated_at": "2026-08-04T13:55:01+0800"
}

這個模式跟後端本來就在讀掛載檔案的做法一致,不用為了一個唯讀狀態去開網路權限。

NOTE

順帶發現的一件事 弄到一半才發現手機上看不到監控,不是 PWA 的問題——那個 app 根本沒有 PWA,沒有 manifest 也沒有 service worker。手機看到的是完全獨立的行動版版面,而監控頁只掛在桌面版的 Dock 裡。所以不是「PWA 沒帶到」,是行動版根本沒放。這種「以為是 A 問題結果是 B」在這整件事裡出現了不只一次。

破案:一條被貓咬過的線#

UPS 上線幾天後,它又死了一次。

我去翻 log,死亡前後全程 OL、市電 113~115V 穩、電池 100%、沒有任何 OB 事件,nut-monitor 也沒有任何紀錄。但重開機又是 ext4 recovery。

市電乾淨,機器卻硬斷電。問題確定在 UPS 下游。

範圍縮到電源線、PSU、主機板之後,我把機殼打開重壓所有接頭,順便換了一條電源線。原本那條是 12A 125V,而它上面有貓咬出來的洞。

那條線的位置正好是 UPS 輸出 → PSU 入口,完全在 UPS 的視線之外。一條銅芯半斷的線會間歇性接觸,一點震動或熱脹冷縮就通、斷、通——那正是「隨機、瞬間、無預兆」的斷電。

而且它解釋了所有先前講不通的地方:

現象貓咬線怎麼解釋
UPS log 全程乾淨斷點在 UPS 量測點的下游,它看不到
idle 也會死跟負載無關,是機械性接觸不良
搬家後才開始搬運的震動讓半斷的銅芯變得更容易分離
挑清晨溫度最低,熱脹冷縮讓接觸點收縮

換上的新線是 10A 125V。看起來規格降了,但 10A × 125V = 1250W,而整台滿載大約 350~400W、換算不到 3A,餘裕非常大。

~1
換線前
平均死一次的間隔
10.8
換線後
連續運行

從「幾乎每天死」變成「十天才死一次」,頻率掉了十倍。

那次差點被誤導的復發#

換線後第十一天,它又死了。而且我的遊戲機也重開了

兩台一起出事,結論很明顯:真的停電了。於是接下來的推論一路展開——真停電時 UPS 應該要撐住 server 才對,既然沒撐住,那一定是電源線插在「Surge Only」那排而不是「Battery Backup」那排,CyberPower 背後那兩排插座長得幾乎一樣,插錯太常見了。

整套推論寫得煞有介事,直到我去查遊戲機為什麼重開。

是 Windows Update。

前提垮掉,整串推論作廢。回頭重看 log,那次跟前面四次一模一樣:OL 全程、市電穩、電池滿、無 OB——不是停電,也不是插錯排,是同一種「UPS 下游瞬斷」,只是頻率已經低了十倍。

TIP

兩個獨立事件同時發生,是最容易讓人推錯的情況 「兩台一起掛」看起來是強得不能再強的共因證據,但它只是巧合。當一個新證據讓你的假設突然變得非常有說服力時,那正是最該回頭驗證那個證據本身的時候。我當時如果沒去查 Windows Update 的紀錄,大概會花一整晚去研究 UPS 的插座分組。

那這次到底是什麼?老實說沒有查出來。

當下能確定的只有排除法的結果:不是市電(UPS log 全程乾淨)、也不是過熱(死的當下 CPU 85% idle、load 1~3、零溫度告警、溫度正常)。剩下最可能的是新線某一端沒插到底,或某個內部接頭又鬆了一點——C13/C14 那種「感覺插好了其實沒到底」非常常見。我把兩端重壓了一次,然後就沒有下文了。

沒有下文的原因不是我放棄追,是四天後它又死了一次,而那次是完全不同的死法。從那之後注意力整個轉過去,這條殘留再也沒有復發到足以驗證的程度。所以誠實的結論是:貓咬線把頻率降了十倍,剩下的那一次至今無解。

不過那天我多做了一件事。既然「過熱」這條被排除得有點快,我順手加了一支溫度記錄器,每分鐘記一筆 CPU 溫度和 load,當作下次的保險。

四天後它立刻立功:

console
15:12  cpu=90.1C  load=8.65
15:22  cpu=90.0C  load=32.53
15:31  cpu=90.0C  load=84.42
15:43  cpu=90.0C  load=156.54
15:50  cpu=90.0C  load=196.01   ← cron 已經排不進去了

那次不是斷電,是系統被燒到爆炸:溫度頂在 90°C 硬撐四十分鐘,load 從 8 衝到 196,而 UPS 全程正常。跟電力完全無關,是另一個故事。

NOTE

每一次追查都留下一個量測工具,而下一個案子是被上一個留下的儀器破的 買 UPS 是為了查斷電,而它的 log 破了貓咬線這案。加溫度記錄器是為了排除過熱,而它四天後抓到了熱爆炸的現行犯。這不是運氣好,是因為每次「我不確定」的時候,加的不是猜測而是一支記錄器——那些記錄器會在你還不知道自己需要它的時候就開始累積證據。

寫這篇的時候順手查了現況#

journalctl --list-boots 可以看出每次重開機是「乾淨關機」還是「硬斷」——有 systemd-shutdown 收尾的是前者,什麼都沒留下的是後者。

console
07-14 → 07-15 20:21   無關機紀錄        ← 電力殘留那次
07-16 → 07-19 15:56   無關機紀錄        ← 熱爆炸那次
07-19 → 07-20 17:15   systemd-shutdown
07-20 → 之後八次       systemd-shutdown  ← 全部是我自己重開的

最後一次非計畫性死亡是 07-19,之後十六天沒有任何一次硬斷。

但這條電力殘留還不能算結案。這段期間我因為各種原因重開過八次,單次連續運行最長只有七天,而上次產生電力故障的間隔是十點八天。也就是說,到目前為止還沒有任何一段連續運行超過上次的發作間隔——它不是被證明修好了,只是還沒有機會出現。

要真的宣告結案,得先讓它安靜地連續跑超過十一天。這個標準是我自己訂的,寫出來是因為「後來就再也沒發生了」這句話如果不附上時間尺度,基本上不構成證據。

一頁式清單#

UPS + NUT 自架檢查清單
  1. UPS 選型看三件事:PFC 純正弦波(主動式 PFC 的 PSU 會挑波形)、有 AVR(電壓波動不用動電池)、NUT 支援(不必裝廠商軟體)。
  2. UPS 也是量測儀器:每分鐘記一次 ups.status / input.voltage / battery.charge,下次出事就能一刀切開「市電問題」和「UPS 下游問題」。
  3. upsd127.0.0.1,單機監控沒有理由對外。
  4. 設定檔權限 root:nut 640,權限不對 driver 起不來。
  5. POWERDOWNFLAG 不能漏,否則 OS 關機後電池會被榨到 0。
  6. BIOS 選 On 不是 Last State,選錯的話 NUT 乾淨關機後市電回來機器不會自己開。
  7. 關機門檻先用原廠值,跑出真實續航數字再決定要不要用 upssched 提早。
  8. Webhook 檔案放 /etc/nut 底下,upsmon 讀不到家目錄。
  9. 狀態要給容器看就用檔案,host 寫 JSON、bind-mount 進去,比把 upsd 對外暴露乾淨。
  10. 電線收進走線槽或套防咬管,這條跟伺服器無關,是為了貓。
  11. 排除掉一個假設之後順手加一支記錄器,下一次出事你就有它了。
參考連結