ENZH
和 AI 讨论这篇文章
ChatGPTClaude

服务器又挂了,查下来是 cursor 的问题

📊 幻灯片

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

开发机又挂了这件事,讲道理挺离谱的。第三篇里那台 16GB 的 c3-standard-4 被 snap Chrome 的崩溃循环搞死过一次,之后我做了 12 项加固,机器换成 32GB 的 e2-highmem-4,还专门装了个内存看门狗,本来觉得这事翻篇了。结果这天又是一样的场面,我当时的原话是「server seems dead again wtf is going on??」——SSH 超时,卡在 banner exchange,跟第三篇一模一样:内核活着,用户态没人应门。

先处理了个小插曲,gcloud 认证过期了,得重新登一遍:

gcloud auth login

然后查 VM 状态:

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

RUNNING。跟上次一样,GCP 觉得机器好好的,实际上用户态已经冻住了。

去 serial console 里翻日志:

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

能看到两件事。第一件,GCP Ops Agent(otelopscol)在疯狂循环 PermissionDenied 错误,每一轮丢掉 1,800 多条 metrics,同时还在写大量错误日志。这个 agent 是第三篇装的,装它就是为了防崩溃,结果它自己在加速崩溃,挺讽刺的。第二件,日志最后一行是 systemd-resolved: Under memory pressure, flushing caches,然后就没了。内存压力,方向有了。


reset 重启:

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 在吃内存。之前见过它涨到 1-2GB,五个 session 各 2.7GB,好像也说得通。其实也不能说说得通,是我当时想当然了。看门狗日志里写的进程名是 node,不是 claude——claude code 的进程名就叫 claude,如果那五个真是 claude session,日志记的就会是 claude。它记的是 node。所以这五个进程是 Cursor 的。

第一反应怪罪 claude code,但看门狗日志记的进程名是 node 而非 claude,据此把真凶锁定为 Cursor第一反应怪罪 claude code,但看门狗日志记的进程名是 node 而非 claude,据此把真凶锁定为 Cursor

后来我就去查了一下 Cursor 的 SSH 远程模式是怎么跑的。它会在服务器上起一堆 node 子进程:

  1. server-main.js:缓存打开的文件、撤销历史、搜索索引,从不释放。
  2. extensionHost:所有扩展跑在一个进程里。V8 的 gc 很懒,不到内存压力大不做 major gc,所以内存单调递增。
  3. fileWatcher:在内存里维护整个文件树,随仓库大小和文件事件一直涨。
  4. ptyHost:终端滚动缓冲区,从不截断。
  5. tsserver:把整个项目的 AST 放内存里,会重新解析,不会收缩。

而且默认没设 --max-old-space-size,V8 默认的堆上限大概是 4GB。15 个小时里,每个进程从约 400MB 涨到 2-2.7GB,膨胀了六七倍。五个进程一起这么干,光 Cursor 就吃掉 12GB 以上,再叠上机器上其他服务,32GB 就不够用了。

五个 node 进程因 V8 懒惰 gc 而单调膨胀,15 小时从约 400MB 涨到 2.7GB,把 32GB 内存填满五个 node 进程因 V8 懒惰 gc 而单调膨胀,15 小时从约 400MB 涨到 2.7GB,把 32GB 内存填满


修了三个地方。

一是 Ops Agent 的权限。为了防崩溃装的监控 agent,IAM 权限不对,每次推 metrics 都失败,失败了还写错误日志,白白消耗磁盘 I/O 和内存。给 VM 的 service account 加上 monitoring.metricWriterlogging.logWriter,错误日志洪流就停了。

二是看门狗阈值。旧阈值太宽松了,85% 才预警,92% 才杀 Chrome,等它动手的时候系统已经在 swap 里挣扎了——放任 12GB 的 node 进程堆到 92% 才介入,等于没介入。新阈值改成:75% 预警记日志,80% 杀 Chrome,85% 杀任何 RSS 超过 1GB 的 node 进程,在 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 做了一次它自己懒得做的 gc。不是每个架构都有这个特性,有就用上。

cron 用 SIGTERM 杀掉膨胀的 Cursor node 进程,客户端秒级自动重连、工作不丢,等于替 V8 做了一次它自己懒得做的 gccron 用 SIGTERM 杀掉膨胀的 Cursor node 进程,客户端秒级自动重连、工作不丢,等于替 V8 做了一次它自己懒得做的 gc


回头看,这跟第三篇其实是同一个模式:长时间跑的进程,没有内存上限,慢慢把开发机的内存吃光。第三篇那次是 snap Chrome 的 fork 炸弹,机器还是 16GB。这次换成 Cursor 的 node 进程,机器都升到 32GB 了,还是被填满。所以 32GB 只是买了点缓冲时间,问题本身没变,感觉只要没有进程级的内存上限,长时间跑的进程早晚会把你给它的 RAM 填满。V8 的 gc 策略基本保证了这一点——不到堆压力大不做 major collection,而「压力大」的意思就是「快到上限了」,默认 4GB 上限加上懒惰 gc,每个 node 进程天生就是一个慢速内存泄漏。

还有个事这次也挺有感触的,监控本身也能变成问题。Ops Agent 装它是为了抓内存异常,结果权限没配对,它自己反而在消耗资源、生产错误日志——监控工具跟其他服务一样,需要正确的权限和资源限制。另外就是进程名,看门狗日志里 nodeclaude 的区别直接决定了追查方向对不对,我要是不看进程名、默认 node 就是 claude code,修的方向就全错了。

这些修完之后,开发机到现在一直挺稳的。稳不稳这个事我现在也不太敢下结论,毕竟第三篇修完那阵子它也稳过一段时间,先跑着看吧。

和 AI 讨论这篇文章
ChatGPTClaude

订阅更新

新文章发布时发到你的邮箱,不发别的。


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