永不入睡的节点
一、现状
此刻,ms-01-1 的二十个线程全部满载空转,CPU 温度稳定在七十七度。它没有在计算任何东西。它只是在忙着保持清醒。这些核心本该在空闲时睡去,此刻却被一个忙等循环占满,唯一的目的,是让 CPU 永远不进入休眠状态。
另一台机器则在网络的另一头盯着它。一旦它僵死,那台机器就远程切断它的电源,再重新上电。
ms-01-1 是我家里那个小集群中的一个节点,一台 Minisforum 的 MS-01 迷你主机。此刻它在正常服务:上面的虚拟机照常运行,它承载的数据在线,整个集群健康。那个忙等循环并不是我额外跑的程序,也不和虚拟机抢 CPU:它只占着本该空闲的时间,真正的负载一到就立刻让位。它同时也是一台我已经放弃真正修复的机器。
它的故障出在硬件上。能确定的是,不在内存条本身,而在机器某个无法更换的地方。我最接近的判断,是 CPU 和内存之间传递地址与数据的那条链路:它偶尔会在半路上把一位翻错。照这个判断,内存单元自始至终完好,错只错在传输的那一瞬,下一次再读,它便又自己「愈合
不让它睡觉,是把所有办法都试过之后,唯一还管用的一个。换内存条、换电源、刷 BIOS、降频率,逐一试过,没有一样奏效。修得动的话,我早修了;可它既过了保修期,坏的地方又没法单独更换,怎么算都不划算。于是它留在原地,靠这一行让它整日发烫的设置维持运转,再有对面那台机器上的看门狗作为最后的保险。
最后救了它的那个办法——别让 CPU 睡觉——我很早就想到了。只是没有第一时间去追它。
它怎么走到这一步,要从一个月前的一次跳闸讲起。
二、跳闸
一个月前的一个深夜,家里跳闸了。七台服务器同时硬断电。偏偏我那台 UPS 没有联网,报不了警,更谈不上触发优雅关机,我是第二天醒来才发现的。
这七台是我在家里搭的一个小集群:用 Proxmox VE 做虚拟化、Ceph 做存储,以超融合(计算和存储跑在同一批机器上)的方式部署,上面是我自己的 GitLab、三个 Kubernetes 集群、内网 DNS、单点登录,还有我 Mac 的 Time Machine 备份等一系列服务。
我跑这套东西,看重的不是上面的服务有多要紧。几乎一切都是 cattle——运维里所谓的「牲口而非宠物
ms-01-1 之前就偶尔出现段错误,零零星星,不影响使用,我也就没怎么在意。那次断电之后,症状重了一个量级:接下来那两周,段错误从偶尔一次变成每次开机几十次,它在存储里承担的那部分无法启动,整机也开始每隔几天就毫无征兆地僵死一次。崩掉的来来回回总是那几个进程,可那时这并不显眼。我没法证明是那次断电把它弄坏的。毕竟它本来就有毛病,我能确定的只有时间上的先后。我一直拖着没认真处理,直到半个月前才正式着手。
蹊跷就蹊跷在这里:这七台里有两台一模一样的 MS-01,CPU、BIOS、内存型号、配置全都相同,连购入批次都相邻。另一台叫 ms-01-2,自始至终一次没出过问题。那晚两台一同断电,型号一致,内存同样不带 ECC,出问题的却只有 ms-01-1。
三、存疑
我很早就猜到了大致的方向:C-state——CPU 闲下来时,单个核心各自进入的省电休眠档,越深越省电、唤醒也越慢。睡的是核心,不是整机:只要某个核心一时没活干,它就自行睡下,而机器照常运转、照常服务。
这台机器的崩溃有个规律:空闲或轻载时发作,满载反而几乎不出事。这种「越闲越容易出事」的模式,很自然指向这些省电休眠档。何况 MS-01 这款机器早有先例:早期 BIOS(1.22 前后)有过一个和 C-state 等省电机制相关的稳定性问题,后来的版本才修掉。两条迹象叠在一起,方向是一致的。
但知道一个可能的开关,和伸手去拨它,是两回事。我没有去拨。
道理很简单:某个改动恰好让它不再崩溃,并不等于我找到了原因。这改动完全可能只是压住了症状,真正出问题的地方原封不动地藏在底下。对发生率稳定的故障
所以,比起让它表面上不再崩,我更在意它究竟坏在哪里。归结成一句话:只要还没把其他可能逐一排除,这台机器在我眼里就是带病运转。严格说,这不是机器的病,而是我的心病,可只要这判断还在,我就当它有病。
这句话决定了后面整件事的走向。接下来一连串看似徒劳的测试,都是有计划的逐项排除。和那个我没去拨的省电开关不同,这些测试每一个都可能给出对我不利的答案:内存条换到双胞胎上,故障可能跟着内存条走;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、跑那些复现脚本,我都不必离开书房:这几台机器虽没有 BMC2,却各接了一台 IP-KVM(把画面和键鼠经网络接出来远程操控
第一件必须靠拆装才能分辨的,是内存条坏,还是机器本身坏。我把两台 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 漏掉的那一样。关键从来不在「指针多
所以最后的复现器,根本算不上内存测试。它无非是把这样一个 Perl 工具放进循环,一遍遍地调用,数它崩了几次:
i=0; while :; do i=$((i+1)); pveversion >/dev/null 2>&1 || echo "FAIL $i"; donepveversion 是那类 Perl 工具里最简单的一个,这段脚本不过是把它无限运行下去、一旦崩溃就打印一行。在 ms-01-1 上,它在第二轮就崩溃,此后稳定地反复崩溃,大约每两次就有一次失败。在健康的双胞胎 ms-01-2 上,连续调用三百次,一次未崩。比之前任何一项内存测试都快上好几个数量级。
同一个故障,后果却分作几档,取决于那个被翻错的指针落在何处。落在一个用户进程里,那个进程因段错误崩溃、退出,机器还活着,这是最常见的一档。落在内核的指针上,内核自身 oops 一次(内核态的一次崩溃
那种僵死还有个古怪的细节:机器明明已经死透,却还能 ping 通。我去 ping 它,ICMP 在一毫秒内便有回应;可与此同时,SSH 连不上,甚至直连它的 SSH 端口也超时,显示器上的画面也是凝固的。它还在响应 ICMP,再往上的一切却都没了反应。这底下究竟是哪一层还在动,我没有深究。
到这一步,它是什么、如何把它逼出来、它会造成什么,都已清楚。可这些,没有一样是修复。
六、跛行
最后真正压住它的,正是我一开始就指着的那个地方:C-state。绕了半个月,答案和第一小时的猜测落回了同一个点上。
但「猜到」和「能正确地动手」是两回事,这中间的距离,几乎就是整场排查本身。
最省事的做法,是直接禁掉那些深度休眠,把 CPU 钉在最浅的一档睡眠上。我试了。头两个小时,它表现得完美无缺,一次未崩。当时若就此收手,这篇文章便到此为止。可我让它继续跑,新的一轮里,它再度僵死。之前那两小时的「完美
再往下查,机制才清楚。把上限卡在最浅那一档,核心还是会睡,只是不睡深了,可它照样冻结。可见触发故障的,是核心「睡」这个动作本身,与睡多深无关。只要它还会阖眼,哪怕最轻的一盹,故障就仍有机会犯。能彻底避开它的,就只剩一条路:根本不让核心睡。我把内核的空闲策略换成 idle=poll,让每个核心闲下来也不休眠,而是空转一个忙等循环,整颗 CPU 一刻不停地醒着。这并没有修好什么,坏的硬件原封未动,只是把那个会触发故障的动作整个拿掉了。一颗 CPU 就这么永远空转发烫,谈不上优雅,但管用。这和前面那个复现器恰好相反:复现器靠不停 fork,让大半核心进进出出地空闲;这里是让所有核心始终保持清醒,一丝空闲都不留。
这一次,故障率降到了零。这个零,我比之前那两小时的太平更信得过:浅睡那次只是恰好没犯,核心照旧在睡,风险一直都在;而现在核心根本不再阖眼,那个会触发故障的动作从源头就没了。但跑再多次都是零,也只是没见它发生,证明不了它绝不发生。
代价是发热。让所有核心二十四小时空转,这台塞在小机箱里的 i9 便会发烫,于是我给整颗 CPU 的功耗设了上限,把稳态温度压在七十七度,离过热降频的红线留出十几度余量。满载,发烫,永不入睡。
正因为没法担保它绝不会再发生,才有了最后那道保险。一台真僵死的机器,需要有人把它的电源硬切一次才能救回。而这台机器没有带外管理2,唯一能从远端硬重启它的,是机柜里那个交换式 PDU。所以我在另一台机器上架了一个看门狗,盯着 ms-01-1。当初那个我没去深究的古怪细节,到这里成了一条设计约束:僵死的它还能 ping 通,可 ping 得通并不等于它还活着。看门狗也就不靠 ping,而是同时探两层:SSH 能否登入、22 端口能否裸连;两层都没有回应,才判定它真正死亡,再通过 PDU 切电、上电,把它救回。
到这一步,能做的也就这些。能 RMA,我早就 RMA 了,可它已过保;剩下唯一「真正的修法
七、确定
我从这半个月的弯路里,没有换来一个治好它的办法。换来的是确定:确定问题就在机器本身,不是别的;确定没有哪一次换件、刷写、降频会管用;确定这就是它真实的样子。对一台坏了也无所谓的机器而言,这份确定,恰恰是我自始至终真正想要的。
摊开来看,这件事是不成比例的:整整半个月的硬件取证(双胞胎对照、交叉换件、专门写的工具、十几次重启
换作别人的机器,我大概会劝他算了。
这台是我自己的。