🔑 關鍵洞察
✦ AI·GEN一台跑著三十幾個容器、同時被 VS Code Remote-SSH 連進去開發的家用伺服器,溫度釘在 90°C 撐了四十分鐘、load 從 8 衝到 196,最後失去回應。而把它燒掉的不是那些服務,是編輯器。這篇記錄整個追查過程:三條看起來很有說服力但都是錯的線索(SMU 當掉後感測器說謊、重開機就好、CPU 只有 30 到 50%),怎麼用 sysstat 的 sar 和 netdata 的 apps.plugin 回溯一個已經結束的事件,以及最後抓到的三個兇手——rust-analyzer 對一個 GTK 依賴全缺、永遠編不過的 Tauri 專案無限重跑 cargo check、Foam 擴充讓 ripgrep 開出兩百多個行程、還有只是雜訊的 RustDesk 輪詢。文末附上防線設定與事後補的兩道保險。
我那台家用伺服器上跑著三十幾個容器:部落格、NAS 面板、監控、幾個自架服務。除此之外我平常也直接用 VS Code Remote-SSH 連進去寫程式,工作區 36 GB、四十萬個檔案。
七月中某個下午,它被燒到失去回應。
查了三天,兇手是編輯器,不是那三十幾個容器裡的任何一個。
症狀:溫度釘在 90°C 撐了四十分鐘#
我在追一個更早的斷電問題時,順手加了一支每分鐘記一次 CPU 溫度和 load 的腳本。那支腳本記到了這個:
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,連每分鐘一次的 cron 都排不進去,撐了四十分鐘之後整台失去回應。
死前的 journal 每四秒噴一次:
amdgpu 0000:79:00.0: amdgpu: SMU: I'm not done with your previous command是晶片裡管電源和溫度的那顆微控制器。連它都調度不出來,代表 CPU 已經忙到底了。
電力可以先排除:我那台接著 UPS,而 UPS 全程 OL、市電穩、電池滿。這不是斷電。
先講結論#
三個兇手,全部來自 VS Code Remote-SSH 那一包:
| 兇手 | 佔用 | 處置 |
|---|---|---|
rust-analyzer 對一個編不過的專案無限重跑 cargo check | 8.3 顆核心 | 限制 jobs、關掉 allTargets |
| Foam 擴充週期性叫 ripgrep 掃四十萬個檔案 | 尖峰 245 個 rg | 停用擴充 |
RustDesk 每秒 fork ps aux 輪詢 | 整機 1.5% | 留著,只是雜訊 |
下面是怎麼查出來的。
這件事難查,是因為線索會騙你#
溫度不是突然爆的,是一路爬上來的:
| 日期 | 超過 85°C 的分鐘數 | 最高溫 |
|---|---|---|
| 07-16 | 0 | 83.9°C |
| 07-17 | 167 | 96.2°C |
| 07-18 | 397 | 96.4°C |
| 07-19 | 81 | 94.0°C |
| 07-20 | 617 | 96.1°C |
07-16 完全沒事,07-17 開始就變了。有東西在 07-17 那天加進來,而且從此沒走。
問題是接下來三條線索,每一條看起來都很有說服力,而每一條都是錯的。
陷阱一:感測器在說謊#
當下讀到的是這組數字:
Tctl: 94.6°C PPT: 5-22W load: 10-138700G 在 5 到 22W(等於閒置)應該是 45 到 55°C,而這裡是 94°C。功耗那麼低卻那麼燙,結論只有一個方向:熱傳不出去,散熱膏乾了或散熱器沒壓緊。時間點還配得上——七月初我為了追斷電問題開過機殼,很可能那時候碰掉了 CPU 風扇的線。
所以當時的判斷是要重上散熱膏。我沒有動手,純粹是懶——要拆的是一顆,雙塔的東西拆下來再裝回去很麻煩,而且還得去翻散熱膏放在哪。
結果沒動手反而是對的。重開機之後,同樣的機器、同樣的散熱器:
| 重開前 | 重開後 | |
|---|---|---|
| CPU | 94.9°C | 54.5°C |
| GPU | 61°C | 38°C |
| NVMe | 57°C | 42°C |
而且是帶著負載(load 12、容器全部在跑)還維持 55°C。
WARNING
感測器故障的時候,讀到的數字是假的
那個「PPT 只有 5 到 22W」根本不成立,因為當時 SMU 已經當掉了,amdgpu 正在每四秒抱怨拿不到回應。SMU 掛掉之後所有經由它回報的數值都不可信,包括功耗。實際上 CPU 是真的在滿載發熱,不是熱傳不出去。
用一個壞掉的儀器讀出來的數字去推論,比沒有數字更危險,因為它看起來像證據。
陷阱二:重開機就好了#
重開之後回頭看行程表,抓到一隻吃了好幾天 CPU 的:
msedge 395% CPU ← 四顆核心因果鏈當場就成立了:Edge 某個分頁跑飛 → CPU 長期滿載 → 頂到 Tjmax 狂降頻 → 工作積壓 → load 8、32、84、196 → SMU 被搞當 → 系統卡死。而且重開機把 Edge 清掉之後一切正常,看起來像驗證過了。
但我後來自己進去看過。用 RustDesk 連進 server,沒有開 Edge,CPU 使用率也只有 30 到 50%。
如果沒開 Edge 也燙,那 Edge 就不是主因。「重開機就好了」只能證明兇手不是常駐服務,不能證明兇手是誰——把它當成因果驗證,是我在這件事上最接近走錯路的一次。
陷阱三:CPU 只有 30 到 50%,看起來不像過熱#
這條最反直覺,而它其實是答案。後面用 sar 拆開來看。
怎麼追一個已經結束的事件#
事件過去了,行程也死了,而我不想等它重現一次再抓。好在有兩個東西替我留了歷史。
top 只能看現在,但這台裝了 。對比平靜的 07-16 和爆炸的 07-20:
07-16(沒事) %user 7% %system 3% %idle 87%
07-20(94°C) %user 46% %system 18% %idle 32% runq-sz 20-24 blocked 0我看到的 30 到 50% 就是 %user 的 46%。加上 %system 的 18%,CPU 實際有 68% 在燒,而且持續好幾小時。
在 8700G 上,68% 持續負載大約等於十一條執行緒不停在算。這個數字感覺不高,但對持續負載來說已經足夠把 APU 頂到溫度上限——瞬間 100% 撐十秒沒事,68% 撐三小時就會頂到 Tjmax。
iowait 0.04%、blocked 0、runq-sz 20-24 這三個一起看,還能確定它不是卡在 I/O:如果行程卡在 ,load 也會很高,但那種高 load 的 CPU 是閒的。這裡是真的有二十幾條執行緒在排隊。
重開機之後三十五個容器全部回來了,而 %user 只有 6.9%,跟平靜那天一模一樣。
吃掉那 46% 的東西不是任何一個容器,而是某個重開機後不會自己回來的東西——也就是桌面 session 裡的程式。
這台跑著 netdata,而它的 保留了 per-process 的歷史。把「熱窗」和「靜窗」的每個 app group 相減:
差值 熱窗 靜窗 app
+829.3 834.7 5.4 sshd ← 8.3 顆核心
+5.0 45.5 40.4 dockerd
+4.7 9.0 4.3 rustdesk答案早就躺在硬碟上,不用等它重現。
sshd 從 5.4% 暴衝到 834.7%,但這不代表 SSH 服務本身有問題。apps.plugin 會把認不出名字的行程歸到它父行程的群組,而我是用 VS Code Remote-SSH 連進去的:
sshd
└─ code-server
├─ bootstrap-fork --type=fileWatcher
└─ bootstrap-fork --type=extensionHost
├─ tsserver.js ×2
├─ typingsInstaller.js
├─ jsonServerMain
└─ claude ×2整包 VS Code Remote-SSH 在 netdata 眼裡全部算 sshd。 依使用者分組看也對得上:我的帳號從 74% 變成 928.7%,root 幾乎沒動。時間軸也吻合:
| 時間 | sshd 群組 | 對應事件 |
|---|---|---|
| 07-16 18:54 | 12.7% | 平靜日,63°C |
| 07-17 13:18 | 227.8% | 溫度第一次破 85°C 是 13:05 |
| 07-18 16:25 | 380.1% | 這天 397 分鐘超過 85°C |
| 07-19 15:10 | 308.7% | 15:43 那次 load=196 |
| 07-19 16:38 | 11.5% | 重開後降下來 |
| 07-20 17:48 | 0.0% | 關機後歸零 |
這兩支我都是很久以前隨手裝的,平常完全不會去看。這次能不等重現就把事件還原出來,靠的就是它們——sysstat 留 CPU 組成,netdata 的 apps.plugin 留 per-process 歷史。
兇手一:rust-analyzer 無限重跑 cargo check#
縮到 VS Code 這包之後,直接翻它自己的 log。最大的那個檔案是 rust-analyzer,裡面滿滿都是:
ERROR Flycheck failed to run: cargo check --workspace
error: failed to run custom build command for `soup3-sys`
error: failed to run custom build command for `javascriptcore-rs-sys`
error: failed to run custom build command for `gobject-sys` / `glib-sys` / `gio-sys`完整的指令是:
cargo check --workspace --keep-going --all-targets \
--manifest-path ~/Server/tauri-plugin-sidecar/Cargo.toml那是一個 外掛專案,而 Tauri 在 Linux 上是靠系統的 GTK 和 WebKit 畫視窗的。編譯的時候,那些 *-sys crate 會去系統上找對應函式庫的開發檔案。
問題是這台是無頭伺服器,從來沒裝過桌面環境,所以那些東西一個都不在。log 裡缺的是這些:
| 缺的函式庫 | 它負責什麼 |
|---|---|
gdk-3.0 | 視窗與繪圖的底層 |
javascriptcoregtk-4.1、libsoup-3.0 | WebKit 的 JS 引擎與 HTTP 層 |
glib-2.0、gobject-2.0、gio-2.0 | GTK 的物件系統與 I/O |
cairo、pango | 向量繪圖與文字排版 |
gdk-pixbuf-2.0、atk | 圖片解碼與無障礙介面 |
而同一組失敗在 log 裡重複了三十五次——那就是它重試的次數。
換句話說,這個 cargo check 不是編很久,是永遠編不完:每一輪都在同一個地方倒下,倒完再從頭來。迴圈是這樣成立的:
CAUTION
--keep-going --all-targets 是致命的一組
--keep-going 是「遇到錯誤不要停,繼續編其他所有東西」,--all-targets 是「lib、bin、test、bench、example 全部都編」。
兩個加起來就是:明知道編不過,還是把整個 workspace 的每一種 target 從頭到尾跑一遍。而 build script 失敗不會產生快取,所以每一次重試都是完整重編,rustc 直接吃滿全部核心。
這也解釋了兩件先前講不通的事:為什麼只有 07-17 之後才發生(那天開始弄那個 Tauri 專案),以及為什麼一重開機就好(VS Code session 沒了,rust-analyzer 也跟著沒了)。
怎麼修#
根治是裝那些系統依賴,或者不要在伺服器上開那個專案。但更重要的是加一道「就算再迴圈也燒不掉機器」的上限:
# ~/.cargo/config.toml
[build]
jobs = 6// rust-analyzer:不要編 test/bench/example,並限制執行緒
{
"rust-analyzer.check.allTargets": false,
"rust-analyzer.numThreads": 4
}jobs 那條是唯一能保證上限的東西,其他都是治標。
順帶一提,files.watcherExclude 我原本就設得不錯,已經排除了大型資料夾。所以檔案監看不是這次的問題,純粹是 cargo——這點值得講,因為「VS Code 很吃資源」的討論裡,檔案監看常常被當成唯一嫌疑。
兇手二:Foam 擴充讓 ripgrep 開出兩百多個行程#
以為結束了,但幾天後 CPU 又開始上上下下。連續採樣 的行程數量:
16 → 36 → 6 → 5 → 15 → ... → 245追來源,每一個都是這種指紋:
rg (--files ... .foam ...)
└── PID 1220855 = VS Code 剛才自動重開的新 ext host是 在週期性地叫 rg --files 掃整個工作區。而我的工作區是這個規模:
WARNING
砍行程治不好這個
我砍掉載著 Foam 的 ext host,VS Code 立刻自動補一個新的,Foam 跟著復活,rg 又開始噴。這個迴圈在我眼前重現了一次。
唯一有效的是停用擴充本身。而且磁碟上把資料夾改名之後還要 Reload Window,已經載進記憶體的那份不會因為檔案改名就消失。
停用之後 rg 歸零,溫度從 90°C 掉回 69°C。
這個症狀不是我獨有的。VS Code 有 rg 吃到 900% CPU 和 rg 長期高 CPU 的 issue,Cursor 社群也有一串 Cursor spawns hundreds of rg processes,症狀跟我的 245 個幾乎一樣。
那些討論的共同點是沒有人指出根因,底下多半在猜是不是 ripgrep 自己變慢了。但 rg 沒壞,它只是被一遍又一遍叫起來。在燒 CPU 的是 rg,一直按下去的手是別的東西——這類問題要找的是呼叫者,不是被呼叫的那支程式。
兇手三:RustDesk(其實只是雜訊)#
順手做了一次三十秒的行程側錄,把新冒出來的行程全部記下來:
11× sh ← 由 rustdesk 開的
7× ps ← 由 rustdesk 開的RustDesk 1.3.1 每秒 fork 一到兩個 sh 去跑 ps aux,用來管理自己的子行程和偵測使用者 session。四天累積燒掉十九小時 CPU,沒有任何人連線也照跑,重啟也沒用。
換算成整機是十六核裡的 1.5%。這個量級不足以把 CPU 煮沸,我需要它來遠端進機器看畫面,所以留著。寫出來只是因為它在 netdata 的差值表裡排第三,而排除它花掉的時間比它值得。
事後補的兩道保險#
溫度上限壓到 85°C#
我進了一次 BIOS,把 CPU 的溫度上限從預設的 95°C 附近壓到 85°C。
理由不是散熱不夠,而是這次的事故已經證明「某個東西無限迴圈把全核吃滿」是會發生的。溫度上限的作用是在那種情況下提早降頻:機器會變慢,但不會全核滿轉硬撐在 90°C 以上四十分鐘。
設定位置在 。正常使用完全感覺不到差別,因為平常根本碰不到 85°C——處理完之後量到的基準是溫度 72.6°C、尖峰 83°C,功耗平均 40W、尖峰 64W(上限約 88W),全核 4.6GHz、單核 4.9GHz,一切正常。
再往下還可以做 Curve Optimizer 降壓,更涼更省而效能不掉。但這是存資料的機器,降壓不穩的表現是隨機當機和 segfault,求穩優先於求極致,這條我不做。
遠端開發改了用法#
修完之後我還是調整了工作方式,因為這兩個兇手都不是意外,是遠端開發這個架構的必然結果——語言伺服器、檔案索引、擴充套件、搜尋,全部都跑在那台散熱餘裕遠不如桌機的伺服器上。工作區越大越吃,而我的是 36 GB、四十萬個檔案,正好是最壞的組合。
現在的分法是:在家走內網用 VS Code Remote-SSH,人就在機器旁邊,真的燒起來我看得到也關得掉;在外面長時間連就換 Zed,因為上面那三個兇手全都是「連著連著它自己燒起來」的類型,人不在現場正是最糟的狀況。
- rust-analyzer —— check(flycheck)相關設定rust-analyzer.github.io
- Cargo —— config.toml 的 build.jobs 與平行度doc.rust-lang.org
- Cargo —— --keep-going 與 --all-targets 的語意cargo check
- Tauri —— Linux 上的系統依賴清單tauri.app
- ripgrep —— 專案首頁GitHub
- VS Code —— rg 高 CPU 的兩串 issue#186279#248666
- Cursor 社群 —— 大量 rg 行程forum.cursor.com
- netdata —— apps.plugin 行程分組與歷史learn.netdata.cloud
- sysstat —— sar 的欄位說明man sar
還沒有留言
✨ 成為第一個留言的人吧