服务器又挂了(这次是 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.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 的远程服务端足够无状态,杀掉它没有任何影响——客户端几秒钟就重连。这让激进的内存守卫成为可行方案。不是每个架构都有这个特性,但如果有,就用它。
这些修复之后,开发机一直很稳定。
不过第三篇之后我也是这么说的。