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 的。

V8 的懒惰 GC + 远程服务器 = 慢性泄漏

Cursor 的 SSH 远程模式会启动多个 node 子进程:

  • server-main.js — 缓存打开的文件、撤销历史、搜索索引。从不释放。
  • extensionHost — 所有扩展跑在一个进程里。V8 的垃圾回收很懒——只有内存压力大的时候才做 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