---
title: "永不入睡的节点"
description: "家里集群里的一个节点，开始毫无规律地崩溃，有时只是一个进程，有时是整台机器。接下来的半个月，我想弄清的不是怎么让它别再崩，而是它到底坏在哪里。"
pubDate: "2026-06-27"
categories: ["故障排除", "硬件", "内核", "Linux", "Homelab", "随笔"]
---

## 一、现状

此刻，ms-01-1 的二十个线程全部满载空转，CPU 温度稳定在七十七度。它没有在计算任何东西。它只是在忙着保持清醒。这些核心本该在空闲时睡去，此刻却被一个忙等循环占满，唯一的目的，是让 CPU 永远不进入休眠状态。

另一台机器则在网络的另一头盯着它。一旦它僵死，那台机器就远程切断它的电源，再重新上电。

ms-01-1 是我家里那个小集群中的一个节点，一台 Minisforum 的 MS-01 迷你主机。此刻它在正常服务：上面的虚拟机照常运行，它承载的数据在线，整个集群健康。那个忙等循环并不是我额外跑的程序，也不和虚拟机抢 CPU：它只占着本该空闲的时间，真正的负载一到就立刻让位。它同时也是一台我已经放弃真正修复的机器。

它的故障出在硬件上。能确定的是，不在内存条本身，而在机器某个无法更换的地方。我最接近的判断，是 CPU 和内存之间传递地址与数据的那条链路：它偶尔会在半路上把一位翻错。照这个判断，内存单元自始至终完好，错只错在传输的那一瞬，下一次再读，它便又自己「愈合」。而这台机器用的是非 ECC 内存[^ecc]，本就没有检错、报告这套机制，于是这种错误连一行日志、一个计数器都不会留下，对系统彻底隐形。它要么什么都不发生，要么让一个进程因段错误骤然崩溃，要么让整台机器毫无征兆地僵死。

不让它睡觉，是把所有办法都试过之后，唯一还管用的一个。换内存条、换电源、刷 BIOS、降频率，逐一试过，没有一样奏效。修得动的话，我早修了；可它既过了保修期，坏的地方又没法单独更换，怎么算都不划算。于是它留在原地，靠这一行让它整日发烫的设置维持运转，再有对面那台机器上的看门狗作为最后的保险。

最后救了它的那个办法——别让 CPU 睡觉——我很早就想到了。只是没有第一时间去追它。

它怎么走到这一步，要从一个月前的一次跳闸讲起。

## 二、跳闸

一个月前的一个深夜，家里跳闸了。七台服务器同时硬断电。偏偏我那台 UPS 没有联网，报不了警，更谈不上触发优雅关机，我是第二天醒来才发现的。

这七台是我在家里搭的一个小集群：用 Proxmox VE 做虚拟化、Ceph 做存储，以超融合（计算和存储跑在同一批机器上）的方式部署，上面是我自己的 GitLab、三个 Kubernetes 集群、内网 DNS、单点登录，还有我 Mac 的 Time Machine 备份等一系列服务。

我跑这套东西，看重的不是上面的服务有多要紧。几乎一切都是 cattle——运维里所谓的「牲口而非宠物」，坏了不照料、直接重建——配置都在 Git 里，整个 GitLab 我都从零重建过一次，备份还另有备份。当初打的旗号当然冠冕堂皇：数据主权，把自己的数据握在自己手里。可几年下来，平心而论，真正让我着迷的是折腾基础设施这件事本身，而且是软件那一层：声明式、Nix、GitOps、Terraform 才是我的乐趣；至于那些本该承载「数据主权」的服务，真正上线、有人用的并没有几个。硬件则更不在我的兴趣之内。也正因为上面的负载坏了无所谓，我并不在意内存有没有 ECC——况且这类迷你主机本来也不支持——正因为它足够便宜，买这样一台机器才成了我能接受的取舍。

ms-01-1 之前就偶尔出现段错误，零零星星，不影响使用，我也就没怎么在意。那次断电之后，症状重了一个量级：接下来那两周，段错误从偶尔一次变成每次开机几十次，它在存储里承担的那部分无法启动，整机也开始每隔几天就毫无征兆地僵死一次。崩掉的来来回回总是那几个进程，可那时这并不显眼。我没法证明是那次断电把它弄坏的。毕竟它本来就有毛病，我能确定的只有时间上的先后。我一直拖着没认真处理，直到半个月前才正式着手。

蹊跷就蹊跷在这里：这七台里有两台一模一样的 MS-01，CPU、BIOS、内存型号、配置全都相同，连购入批次都相邻。另一台叫 ms-01-2，自始至终一次没出过问题。那晚两台一同断电，型号一致，内存同样不带 ECC，出问题的却只有 ms-01-1。

## 三、存疑

我很早就猜到了大致的方向：C-state——CPU 闲下来时，单个核心各自进入的省电休眠档，越深越省电、唤醒也越慢。睡的是核心，不是整机：只要某个核心一时没活干，它就自行睡下，而机器照常运转、照常服务。

这台机器的崩溃有个规律：空闲或轻载时发作，满载反而几乎不出事。这种「越闲越容易出事」的模式，很自然指向这些省电休眠档。何况 MS-01 这款机器早有先例：早期 BIOS（1.22 前后）有过一个[和 C-state 等省电机制相关的稳定性问题](https://forums.servethehome.com/index.php?threads/minisforum-ms-01-review-the-10gbe-with-pcie-slot-mini-pc.42827/)，后来的版本才修掉。两条迹象叠在一起，方向是一致的。

但知道一个可能的开关，和伸手去拨它，是两回事。我没有去拨。

道理很简单：某个改动恰好让它不再崩溃，并不等于我找到了原因。这改动完全可能只是压住了症状，真正出问题的地方原封不动地藏在底下。对发生率稳定的故障，「改完就好」还勉强算一点证据；可这台机器的故障率本身在漂移：同样的配置，这次开机不到百分之一，过些时候又能漂到一半以上。在这种背景噪声里，改完之后崩不崩，其实都说明不了问题：没崩，可能是修好了，也可能只是赶上一段低发期；崩了，读法同样有两种，谁也压不倒谁。一个任何结果都解释得通的测试，等于没做。

所以，比起让它表面上不再崩，我更在意它究竟坏在哪里。归结成一句话：只要还没把其他可能逐一排除，这台机器在我眼里就是带病运转。严格说，这不是机器的病，而是我的心病，可只要这判断还在，我就当它有病。

这句话决定了后面整件事的走向。接下来一连串看似徒劳的测试，都是有计划的逐项排除。和那个我没去拨的省电开关不同，这些测试每一个都可能给出对我不利的答案：内存条换到双胞胎上，故障可能跟着内存条走；memtest 可能真的报错；某一档频率可能正是病根。正因为它们能失败，结果才算数。

而这套排除之所以成立，靠的是那台健康的双胞胎 ms-01-2 作对照。一台坏、一台好，其余全同。它让我绕开那个答不上来的问题——我这一改到底有没有用——换上一个答得上来的：把一个部件在两台机器间对调，故障是跟着部件走，还是留在原机。这个答得上，因为我只看故障停在哪一边，与它此刻漂到多高无关。

## 四、逐项排除

最先怀疑的，自然是内存条本身。

排查内存的标准做法，是跑 memtest86+，让它在操作系统之外，把每一个内存单元反复读写、比对。光是把它启动起来，就耗去我一个通宵：关掉 Secure Boot，从 GRUB 把它引导起来，中间还卡在开机自检上好几次，屏幕一片黑，只能靠远程把电源硬切、重来。真正跑起来后，我不敢只跑两轮，经验上至少要十轮。于是它跑了二十一小时四十八分，整整十轮，温度一路顶在九十度上下。

结果是零错误。

这是整场排查里第一次落空。一项测试干干净净地通过，却什么也没排除，至多把「内存单元有硬损坏」这一种可能压得很低。memtest 反复读写、比对的，是数据有没有存对，而这台机器的问题不在那里。

同一类工具我还试了第二把：stressapptest，把内存带宽压满，专门逼高速信号上的毛病现形。四个小时，搬运七百多 TB 数据，又是零事件。两把工具，同一种干净。

干净，机器却照旧崩。我退后一步，开始怀疑前提本身：也许根本不是硬件，而是软件，内核的某个 bug。这台机器当时用的内核版本，确实背着几个和内存管理相关的已知回归。我把它升到了修掉那些回归的新版本。那天傍晚，复现脚本跑了两百多轮，一次未崩，我几乎以为就此结束，便让它过夜接着跑。第二天早上，脚本跑到第一千两百多轮、连续两小时五十分之后，整机毫无征兆地僵死在那里。

后来才明白，这一步和别的排查项不是一类。新内核确实修掉了一个真实存在、却完全是另一回事的软件 bug。它发作得又快又频，把所有崩溃的账都揽了过去，我便始终以为崩溃是软件的事；它一被修掉，藏在它后面、发作慢得多的硬件故障才显出原形。这个软件 bug 从头到尾都不是故障本身，而是遮在硬件故障前面的一道烟幕。升级并没有修好任何东西，只是把烟幕吹散了。那两百多轮干净，正是我一直最警惕的那种「改完不崩」，而且是最难识破的一种：别的排查项至多给我一个没用的干净，这一次却差点成了一个假的终点。少了那道烟幕，又恰好撞上硬件故障的一段低发期，两样凑在一起，险些让我就此收手、认定它已经修好。

当时我自然没看这么透。内核既然没能了结，我的怀疑还在硬件之外打转，而固件也属于这一类。我把 BIOS 刷到最新版。前面那条 1.22 的 C-state 旧事，本来也指着这个方向。刷完，依旧冻结；更新说明里和内存、DDR5、内存控制器相关的条目一条都没有。那条旧事也跟着不成立：社区那个固件级、影响所有机器的 C-state bug，和我这台的问题不是一回事；双胞胎用着同一版 BIOS，却安然无恙。到这一步，键盘前不拆机能做的，都做尽了：软件、固件都已排除，验内存的几把工具也跑干净了。故障确实长在这台机器的硬件里，而硬件不是一整块。

要分清坏的究竟是哪一块，剩下的只能动手。在此之前，刷 BIOS、引导 memtest、跑那些复现脚本，我都不必离开书房：这几台机器虽没有 BMC[^bmc]，却各接了一台 IP-KVM（把画面和键鼠经网络接出来远程操控）；硬断电重启则靠机柜里一个交换式 PDU（可联网远程开关的电源插排）。这样人坐在键盘前就能做完。拆装却是头一件 IP-KVM 帮不上的事。它能把键盘和画面送到我面前，却没法替我拧一颗螺丝。这一步我一直拖到最后，因为它在我这儿的成本高得惊人。这台机器和另外三台迷你主机，挤在同一副我自己 3D 打印的机架套件上，每台背后是四根光纤、一根电源、一根网线、一根 HDMI、一根 USB。要动其中任何一台的硬件，我得打开机柜侧面板、戴上头灯，先把每一根线的位置记下来，再一根根拔掉，然后用电动螺丝刀把整副套件从机架上卸下来。套件卡得很紧，装回去比拆下来更费力。这样一趟下来，不止一个小时。

第一件必须靠拆装才能分辨的，是内存条坏，还是机器本身坏。我把两台 MS-01 的内存对调了过来：ms-01-1 的内存条插进健康的 ms-01-2，ms-01-2 的插进 ms-01-1。逻辑很简单：故障若是跟着内存条去了 ms-01-2，便是内存条的问题；若留在 ms-01-1，便是机器本身的问题。结果，故障留在了 ms-01-1：换上那对健康内存条，它依旧崩溃。所以不是内存条，是机器本身：板子上的供电、CPU 里的内存控制器、CPU 插槽这一类东西，要么焊死在板上，要么无法靠换件解决。既然拆装如此折磨，这次对调我索性没再换回，就一直保持原样。

电源是下一个。按说查到机器本身，它也算其中一部分，可它还没单独验证过，而怀疑它不无道理：故障专挑空闲时发作，恰好和「从空闲骤然转入活跃、供电没跟上」的毛病吻合。于是我又拆了一次，把 ms-01-1 接到 ms-01-2 那个确认无碍的电源适配器上，照原样再测。它仍以几乎一样的频率崩溃，电源没问题。这一拆本可以省下，反正已经查到是机器本身；可若是省了它，电源就始终没真正验过，那这台机器在我眼里仍是带病运转。

最后是内存频率。我把它从 5600 一路降到 4400，又试了 5200。表面上各档的数字有高有低，可一旦把那个会自己漂移的故障率算进去，几档之间分辨不出真实差别。频率也不是能拨动结果的那个旋钮。

数据存得对不对、是不是硬件之外的软件与固件、又是机器里的哪一块，这三层我逐一问过。但每一层至多问出「可能性很低」，问不出「不可能」：测试只把某一种坏法压到可以搁置，并没有谁被真正排除。唯一的例外是换内存条：故障没跟着它走，这一条是确凿的。剩下还立着的，是一处确实长在机器本身、却没有哪一项测试真正「看见」过的故障。

## 五、隐形的故障

那些给出「零错误」的测试，之所以一个接一个干净通过，是因为它们本是同一类测试，而这一类测试，天生就难以看见这种故障。

要看清这一点，先得分开两种坏法。一种，是值存进内存之后，存储单元本身出了错，写进去的和读出来的对不上；常规内存测试设想的，正是这一种。这台机器不属于这种：内存单元自始至终是好的。我能给出的最好解释，是错出在「搬运」的那一程，而不在「存放」的那一处：值在 CPU 与内存之间往返，偶尔在途中有一位走样，可只要落定，存着的就是对的，再读也读得回。若真如此，这个错便只发生在往返的途中，两头都干净：写下去的对，读回来的也对。

memtest、stressapptest 这一类工具，本质上都在做同一件事：写一个值进去，过一会儿再读回来，比对前后一致不一致。可这种「写进去、读回来、对一遍」的做法，恰好会漏掉一个传输瞬间出错、读回时已经复原的故障：等回头一比对，它早已恢复，数据里一丝痕迹都不留。它们还有个共同点：都让内存和 CPU 满负荷运转，而这故障偏偏在机器空闲时才最活跃。一种天生看不见它、又恰好压住了它触发条件的测试，自然什么都查不到。

我甚至用 Claude Code 专门写了一个小工具，并将其命名为 memchase，想把它逼出来。这工具别无他用：在内存里建起一条巨大的指针环，再以最高的密度一路追着走。每读出一个指针，当即拿它跳到下一跳，是真正的「读一个、用一个」；每一跳都和一张独立的表即时核对，在途中翻错一位，就会被当场逮住。为凑齐「空闲才发作」的条件，它还在每段追逐之间留出空隙，让核心睡下去再醒来。指针密度顶满、读了就用、空闲也有，它跑了八亿多步，一次没响。这一次「什么都没抓到」反而最有分量：连一个专门冲它来、又确实睡过觉、把追指针做到极致的工具都逼不出它。它没能证明什么，却把「坏在存储单元」这条路压到极窄：故障多半不在存住的数据里，而在数据送达的那一路上。至于为什么把指针追到极致仍不够，要等我想起另一件事才明白。

最后让它现形的，不是又一件工具，是我回想起的一个细节。

我注意到，崩溃似乎总发生在我运行某一类命令时：Proxmox VE（命令行下简称 PVE）那套管理工具，`qm`、`pct`、`pveversion` 之类。它们有一个共同点：全是 Perl 脚本。

这正是 memchase 漏掉的那一样。关键从来不在「指针多」，那一点 memchase 已经做到顶，也没用；要紧的是 Perl 每跑一次，都要把自己那套结构从头重建一遍。Perl 解释器本就一刻不停地解引用指针、顺着往里跳；而这套 PVE 工具每调用一次，都要重新 fork（开一个新进程）、重新加载一大批模块，在刚分配的内存里现搭起大片指针结构，转头就用。memchase 追的是一条建好就再不变的环，Perl 却在永不停歇地制造新的、转瞬即用的指针：值刚送到、还没来得及自愈，它就已经跳了进去。指针只要在这一跳被翻错一位，它立刻冲进一个垃圾地址，当即崩溃。这样一种负载，不断新建、即刻就用，其间又被一次次 fork 切出空闲，恰好同时踩中了故障的触发条件和它最致命的用法。

所以最后的复现器，根本算不上内存测试。它无非是把这样一个 Perl 工具放进循环，一遍遍地调用，数它崩了几次：

```sh
i=0; while :; do i=$((i+1)); pveversion >/dev/null 2>&1 || echo "FAIL $i"; done
```

`pveversion` 是那类 Perl 工具里最简单的一个，这段脚本不过是把它无限运行下去、一旦崩溃就打印一行。在 ms-01-1 上，它在第二轮就崩溃，此后稳定地反复崩溃，大约每两次就有一次失败。在健康的双胞胎 ms-01-2 上，连续调用三百次，一次未崩。比之前任何一项内存测试都快上好几个数量级。

同一个故障，后果却分作几档，取决于那个被翻错的指针落在何处。落在一个用户进程里，那个进程因段错误崩溃、退出，机器还活着，这是最常见的一档。落在内核的指针上，内核自身 oops 一次（内核态的一次崩溃），留下一个怎么也杀不掉的任务。若落得再深一些，落在调度器或某个全局锁上，整台机器就毫无征兆地僵死，没有日志，没有 panic，什么都不留。

那种僵死还有个古怪的细节：机器明明已经死透，却还能 ping 通。我去 ping 它，ICMP 在一毫秒内便有回应；可与此同时，SSH 连不上，甚至直连它的 SSH 端口也超时，显示器上的画面也是凝固的。它还在响应 ICMP，再往上的一切却都没了反应。这底下究竟是哪一层还在动，我没有深究。

到这一步，它是什么、如何把它逼出来、它会造成什么，都已清楚。可这些，没有一样是修复。

## 六、跛行

最后真正压住它的，正是我一开始就指着的那个地方：C-state。绕了半个月，答案和第一小时的猜测落回了同一个点上。

但「猜到」和「能正确地动手」是两回事，这中间的距离，几乎就是整场排查本身。

最省事的做法，是直接禁掉那些深度休眠，把 CPU 钉在最浅的一档睡眠上。我试了。头两个小时，它表现得完美无缺，一次未崩。当时若就此收手，这篇文章便到此为止。可我让它继续跑，新的一轮里，它再度僵死。之前那两小时的「完美」，不过是赶上了故障率自己漂到的一段低谷，正是开头我拒绝踏入的那个陷阱。这一次，它原样上演了一遍。

再往下查，机制才清楚。把上限卡在最浅那一档，核心还是会睡，只是不睡深了，可它照样冻结。可见触发故障的，是核心「睡」这个动作本身，与睡多深无关。只要它还会阖眼，哪怕最轻的一盹，故障就仍有机会犯。能彻底避开它的，就只剩一条路：根本不让核心睡。我把内核的空闲策略换成 `idle=poll`，让每个核心闲下来也不休眠，而是空转一个忙等循环，整颗 CPU 一刻不停地醒着。这并没有修好什么，坏的硬件原封未动，只是把那个会触发故障的动作整个拿掉了。一颗 CPU 就这么永远空转发烫，谈不上优雅，但管用。这和前面那个复现器恰好相反：复现器靠不停 fork，让大半核心进进出出地空闲；这里是让所有核心始终保持清醒，一丝空闲都不留。

这一次，故障率降到了零。这个零，我比之前那两小时的太平更信得过：浅睡那次只是恰好没犯，核心照旧在睡，风险一直都在；而现在核心根本不再阖眼，那个会触发故障的动作从源头就没了。但跑再多次都是零，也只是没见它发生，证明不了它绝不发生。

代价是发热。让所有核心二十四小时空转，这台塞在小机箱里的 i9 便会发烫，于是我给整颗 CPU 的功耗设了上限，把稳态温度压在七十七度，离过热降频的红线留出十几度余量。满载，发烫，永不入睡。

正因为没法担保它绝不会再发生，才有了最后那道保险。一台真僵死的机器，需要有人把它的电源硬切一次才能救回。而这台机器没有带外管理[^bmc]，唯一能从远端硬重启它的，是机柜里那个交换式 PDU。所以我在另一台机器上架了一个看门狗，盯着 ms-01-1。当初那个我没去深究的古怪细节，到这里成了一条设计约束：僵死的它还能 ping 通，可 ping 得通并不等于它还活着。看门狗也就不靠 ping，而是同时探两层：SSH 能否登入、22 端口能否裸连；两层都没有回应，才判定它真正死亡，再通过 PDU 切电、上电，把它救回。

到这一步，能做的也就这些。能 RMA，我早就 RMA 了，可它已过保；剩下唯一「真正的修法」，是把整块主板送去第三方维修，并不划算。于是它就停在这个状态里：一个仍在生产中服役的节点，旁边另一台机器随时准备切断它的电源、再重新接通。

## 七、确定

我从这半个月的弯路里，没有换来一个治好它的办法。换来的是确定：确定问题就在机器本身，不是别的；确定没有哪一次换件、刷写、降频会管用；确定这就是它真实的样子。对一台坏了也无所谓的机器而言，这份确定，恰恰是我自始至终真正想要的。

摊开来看，这件事是不成比例的：整整半个月的硬件取证（双胞胎对照、交叉换件、专门写的工具、十几次重启），花在一个彻头彻尾是 cattle 的节点上。可这种不成比例，大概正是某一类 homelab 玩家的常态：较真的一向是折腾本身，方法上一丝不苟，至于上面跑什么、坏不坏，反倒无所谓。也正是这种偏好，让我先挑了这台不带 ECC 的迷你主机、给它埋下病根，又让我事后甘愿花上半个月，只为把这病根查到底。

换作别人的机器，我大概会劝他算了。

这台是我自己的。

[^ecc]: ECC：Error-Correcting Code，带纠错的内存。它在每次读写时校验数据，能发现并纠正单比特错误，再向系统报告。消费级平台和这台迷你主机用的都是不带 ECC 的普通内存：没有这套校验，错误既不会被纠正，也不会被记录，对上层完全隐形。
[^bmc]: BMC（Baseboard Management Controller）/ 带外管理：一块独立于操作系统、单独供电的管理芯片，机器死了也能远程开关机、看控制台、装系统。正经服务器大多带，这几台迷你主机没有，所以它们一旦僵死，只能从电源那头硬切。
