🔑 关键洞察
✦ 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
还没有留言
✨ 成为第一个留言的人吧