ENZH

服务器又挂了(这次是 Cursor 干的)

📊 幻灯片

内存泄漏监控仪表盘内存泄漏监控仪表盘

"server seems dead again wtf is going on??"

SSH 连接超时,卡在 banner exchange。跟第三篇一模一样——内核活着,用户态冻住了。

第三篇里,那台 16GB 的 c3-standard-4 因为 snap Chrome 崩溃循环而死。做了 12 项加固,换成 32GB 的 e2-highmem-4,还装了内存看门狗。

本以为翻篇了。没有。


先是 GCP 认证过期,得重新登录:

gcloud auth login

小插曲。查 VM 状态:

gcloud compute instances list --filter="name~claude-devbox"

RUNNING。跟上次一样。内核还在,用户态没人应门了。

Serial Console 里看到了什么:

gcloud compute instances get-serial-port-output claude-devbox --zone=us-west1-b

两件事。

第一:GCP Ops Agent(otelopscol)在疯狂循环 PermissionDenied 错误。每一轮丢掉 1,800 多条 metrics,同时生成大量错误日志。第三篇装的监控代理,本来用来防崩溃,现在在加速崩溃。挺讽刺的。

第二:日志最后一行是 systemd-resolved: Under memory pressure, flushing caches。然后沉默。


看门狗日志讲了完整故事

gcloud compute instances reset claude-devbox --zone=us-west1-b

SSH 回来。看门狗一直在记录。

01:39 UTC(开机 8 小时):内存 87%,Claude Code 占 1GB。还没事。

09:12 UTC(15 小时):内存 89%。五个 node 进程,每个 2-2.7GB。合计约 12.8GB。

09:13 UTC:内存 94%,swap 88%。看门狗杀了 Chrome 和一个 node 进程。来不及了,系统进入了死亡螺旋。

32GB 的内存全景:

进程RSS
5x node(Cursor)~12.8 GB
Chrome~0.9 GB
openclaw-gateway(Docker)~0.6 GB
PM2 服务~0.8 GB
LiteLLM(Docker)~0.3 GB
GCP Ops Agent~0.5 GB
Docker + 内核开销~0.5 GB
合计~16.4 GB 内存 + 3.5 GB swap

32GB 的机器,进程就吃掉了约 20GB。五个 node 进程占了将近一半。


我以为是 Claude Code,结果不是

第一反应:Claude Code 在吃内存。之前见过它涨到 1-2GB。五个会话各 2.7GB——说得通。

看门狗日志写的是 node,不是 claude。Claude Code 的进程名是 claude。如果那些是 Claude 会话,看门狗记的会是 claude。它记的是 node。五个进程是 Cursor 的。

Cursor 的 SSH 远程模式会启动多个 node 子进程:server-main.js——缓存打开的文件、撤销历史、搜索索引。从不释放。extensionHost——所有扩展跑在一个进程里。V8 的 GC 很懒——只有内存压力大才做 major GC。内存单调递增。fileWatcher——维护内存中的文件树,随仓库大小和文件事件持续增长。ptyHost——终端滚动缓冲区。从不截断。tsserver——把整个项目的 AST 放在内存里。会重新解析,不会收缩。

默认没设 --max-old-space-size。V8 的默认堆上限大约 4GB。

15 个小时里,每个进程从约 400MB 涨到 2-2.7GB,膨胀了 6-7 倍。五个进程同时这么干,光 Cursor 就吃掉 12GB 以上。加上其他服务,32GB 不够用了。


三刀修复

一、Ops Agent 的权限问题。为了防崩溃装的监控代理,IAM 权限不对,每次推 metrics 都失败然后写错误日志,反而在吃磁盘 I/O 和内存。给 VM 的 service account 加了 monitoring.metricWriterlogging.logWriter。错误日志洪流停了。

二、看门狗升级。旧阈值太宽松了。85% 预警,92% 才杀 Chrome。等看门狗动手,系统已经在 swap 里挣扎了。新阈值:75% 预警记日志。80% 杀 Chrome。85% 杀任何占用超过 1GB RSS 的 node 进程。旧看门狗放任 12GB 的 node 进程堆积到 92% 才动手。新看门狗在 85% 介入——在 swap 被打满之前。

三、Cursor 内存守卫。每 5 分钟跑一次的 cron:

# 杀掉任何 .cursor-server 下超过 1GB RSS 的 node 进程
pgrep -f '.cursor-server' | while read pid; do
  rss=\$(awk '/VmRSS/{print \$2}' /proc/\$pid/status 2>/dev/null)
  if [ "\${rss:-0}" -gt 1048576 ]; then
    kill -TERM "\$pid"
    logger "cursor-guard: killed pid \$pid (RSS: \${rss}kB)"
  fi
done

SIGTERM 是温和的。Cursor 客户端检测到断开后自动重连,几秒钟的事。用户体验就是闪一下"Reconnecting...",然后一切恢复,不丢任何工作。

这是关键洞察:Cursor 的远程架构天生支持重连。杀掉一个膨胀的服务端进程不是破坏——是帮 V8 做了它自己不愿意做的垃圾回收。


同一个模式在重演

第三篇的模式在重演。长时间运行的进程,没有内存上限,在开发机上慢慢把内存吃光。

第三篇是 snap Chrome 在 16GB 上 fork 炸弹。这次是 Cursor 的 node 进程在 32GB 上静默膨胀。进程不同,失败模式完全一样——长时间无约束的内存增长。

32GB 只是买了缓冲时间,没改变根本问题。没有进程级内存上限,任何长时间运行的进程最终都会把你给它的 RAM 填满。

V8 的 GC 策略保证了这一点。不到堆压力大的时候不做 major collection,"压力大"的意思是"快到上限了"。默认 4GB 上限加上懒惰 GC,每个 node 进程天生就是一个慢速内存泄漏。


监控本身可以成为问题。Ops Agent 装了是为了抓内存异常,权限不对,自己反而在消耗资源、生产错误日志。监控工具跟其他服务一样,需要正确的权限和资源限制。

进程名是诊断的关键。看门狗日志里 nodeclaude 的区别,决定了追查方向对不对。不看进程名就默认"node = Claude Code",修的方向全错了。

可重连架构是你的武器。Cursor 的远程服务端足够无状态,杀掉没有任何影响。客户端几秒钟就重连。这使激进的内存守卫可行。不是每个架构都有这特性,有就用。

这些修复之后,开发机一直很稳定。

不过第三篇之后也是这么说的。


© Xingfan Xia 2024 - 2026 · CC BY-NC 4.0