服务器又挂了(这次是 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.metricWriter 和 logging.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 装了是为了抓内存异常,权限不对,自己反而在消耗资源、生产错误日志。监控工具跟其他服务一样,需要正确的权限和资源限制。
进程名是诊断的关键。看门狗日志里 node 和 claude 的区别,决定了追查方向对不对。不看进程名就默认"node = Claude Code",修的方向全错了。
可重连架构是你的武器。Cursor 的远程服务端足够无状态,杀掉没有任何影响。客户端几秒钟就重连。这使激进的内存守卫可行。不是每个架构都有这特性,有就用。
这些修复之后,开发机一直很稳定。
不过第三篇之后也是这么说的。