
Claude Desktop 每两天因 27 字节遥测响应卡死
来源标题:A 27-byte telemetry response nobody reads freezes Claude Desktop every two days
Claude Desktop 每两天就会卡死,表现为界面元素消失只剩骨架屏,或完全无响应。原因是渲染进程崩溃后 systemd-coredump 保存核心转储,拖垮磁盘 IO,负载均值一度达 46。作者称一个无人读取的 27 字节遥测响应是触发点,并借此撰文分析 Chromium 内部机制。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
几个月前,我从 ChatGPT 迁移到了 Claude Desktop,又是一个 Electron :love: 应用。每隔几天它就会挂掉,方式有两种:要么所有元素都消失,只剩下骨架占位符,要么直接卡死——你点击,什么反应都没有。这也不是特别规律,因为应用必须一直开着不重启才行。而且它更新很快,这让问题不那么容易被注意到。但当它卡住时,总会挑最“好”的时机 :)
于是我借这个机会,写了一篇关于 Chromium 内部机制的文章。
两个月的 monkey-patch
某一天,我注意到当 Claude 在后台构建什么东西时,整台机器卡得要命。我以为是构建导致的,就跑了 uptime。负载平均值 46,而且 uptime 本身启动就花了 20 秒。cat /proc/pressure/io:
full avg10=85.30 avg60=74.12 avg300=28.47 total=2187342911
什么鬼。什么时候一个构建能把 NVMe 压榨成这样?
切到 Claude 一看。只剩骨架了。我第一个想法是 LLM 干了什么蠢事把自己搞死了。但有什么东西还在疯狂读写磁盘。结果是 systemd-coredump 在保存 core 文件,把磁盘搞死了。渲染进程崩溃了 => SIGILL => core dump:内核把进程的整个内存写到一个文件里。因为是 Chromium,所以 dump 不大:只有 1.5 TB。
关于这个大家伙为什么这么大,后面再细说。而且这个 dump 基本上走的是 RPC,因为内核会启动 systemd-coredump,把内存通过管道传给它。管道不能 seek,所以那些从未被访问过的页面无法跳过,会以真正的零写出去。我没有多余的 1.5 TiB 空间,但 systemd-coredump 还是试了:它在五分钟内写了 92.4 GiB,撞上超时,然后什么文件都没留下。真是团队协作。
好吧。但渲染进程一开始为什么会崩溃?应用的 main.log 里只有每隔 20 到 40 秒出现一次的“Main webview is unresponsive, will kill and reload”。显然什么都没被重载。
于是我一点点地给 Claude Desktop 打补丁。我把 core dump 的大小限制在 4 GiB。崩溃本身没留下任何可读的东西,因为应用的 stdout 和 stderr 都去了 /dev/null,所以我把它们重定向到 journal,然后等。11 天后,应用确实杀掉了卡死的渲染进程,Chromium 给我留下了这个:
FATAL:content/child/child_thread_impl.cc:380] Crashing because hung
好极了。好日志。同样的故事——渲染进程死了,SIGILL,core。这次限制了大小,里面只有一个栈帧,而且仍然让笔记本卡了好几分钟。这没什么用,所以我关掉了应用的 core dump,挂了一个调试器,在应用判定渲染进程无响应时打印 JS 栈。等下一张明信片吧。
13 天后,窗口悄无声息地死了:同样的骨架,没有日志,调试器也没输出。耶。就这样持续了 18 分钟,Ctrl+R 没用,我重启了应用。
于是我加了一行日志,记录渲染进程的每一次死亡,包括原因和退出码。2 天后它又死了,日志写着“一切正常”:
[hang-stack] main webview renderer gone: { reason: 'clean-exit', exitCode: 0 }
调试器没东西可打印,因为没有任何东西卡住。就是一次退出,Chromium 自己决定退出的。应用把正常退出当成是有意为之,所以不重载窗口。那一次它就那样死了 15 分钟。
于是我让补丁去重载窗口。之后它就只是卡一分钟然后恢复。这让我极其烦躁,但没丢数据,应用也不需要重启,所以也算够用了。
我厌倦了一次只加一勺日志,于是加了一批:进程退出、控制台错误、页面加载失败,以及 Chromium 自己的详细日志。每次日志里看起来都不一样:futex 等待、应用杀掉卡死的渲染进程、GPU buffer 分配失败、正常退出。没有任何日志或栈显示原因。
8 天后它彻底卡死了。一个回复正在流式输出,停在打字动画中间:半个字母悬着,状态停在“6s · 246 tokens”。应用已经连续运行了 35 个小时。这次我运气不错:渲染进程卡住了但还活着,不像之前那些次已经死了。我手动跑了 gcore,dump 了渲染进程和浏览器进程,也就是 Chromium 对应用主进程的叫法。这次 dump 写到普通文件而不是走管道,所以文件是稀疏的:大小 1.5 TiB,磁盘占用 1.5 GiB。如果我把它邮件发给你,那就是一个 1.5 TiB 的附件。
现在说说这个大家伙为什么这么大。几乎全都是地址空间而不是内存,Chromium 在启动时预留了这些地址空间,基本上从不碰它。应用跑在 Electron 44 上,也就是 Chrome 152。这 1376 GiB 的地址空间是给 V8(JS 引擎)的沙箱预留的一整块。V8 把 JS 堆里的指针存成从沙箱起始位置算起的偏移量,这样即使指针被损坏也仍然落在沙箱内,无法触及进程的其他部分。为了做到这一点,V8 提前预留了整个范围:沙箱本身 1 TiB,外加周围 352 GiB 的 guard 区域。Chromium 的分配器又预留了两个 16 GiB 的池,C++ 对象的 GC 再预留 16 GiB。这些就是渲染进程 dump 里最大的预留。
指向沙箱内 buffer 的指针存储为从沙箱基址算起的偏移量。
243// Everywhere else use a 1TB sandbox.
244constexpr size_t kSandboxSizeLog2 = 40; // 1 TB
245#endif // V8_TARGET_OS_ANDROID
246constexpr size_t kSandboxSize = 1ULL << kSandboxSizeLog2;
247
248// Required alignment of the sandbox. For simplicity, we require the
249// size of the guard regions to be a multiple of this, so that this specifies
250// the alignment of the sandbox including and excluding surrounding guard
251// regions. The alignment requirement is due to the pointer compression cage
252// being located at the start of the sandbox.
253constexpr size_t kSandboxAlignment = kPtrComprCageBaseAlignment;
254
255// Sandboxed pointers are stored inside the heap as offset from the sandbox
256// base shifted to the left. This way, it is guaranteed that the offset is
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。