news 2026/8/4 20:33:15

一场由 Cursor Remote 引发的生产服务器卡死:从 I/O 阻塞到根因定位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一场由 Cursor Remote 引发的生产服务器卡死:从 I/O 阻塞到根因定位

这次事故最麻烦的地方,不是服务器卡死本身,而是它留下的表象几乎都指向磁盘。

生产服务器先后出现两次严重卡顿。故障发生时,接口超时、数据库连接失败、Docker 命令迟迟不返回,连 SSH 都很难登录。重启后所有服务又恢复正常,磁盘检查也没有发现硬件错误。如果只看故障末期,很容易得出“云磁盘性能不够”或者“数据库把磁盘打满了”的结论。

但最后定位到的触发源既不是数据库,也不是业务容器,而是一组运行在宿主机上的 Cursor Remote 进程。有人连接服务器时把/当成了远程工作区,Cursor 的 Extension Host、File Watcher 和 Agent Worker 开始面对整台服务器的文件系统工作。它们的内存不断增长,在一台 16 GB、没有 Swap 的机器上,最终把系统拖进了页面回收和磁盘读取风暴。

下面记录完整的排查过程。文中的服务器地址、账号和业务名称都已脱敏,时间统一使用 UTC,需要换算北京时间时加 8 小时。

故障现象:所有服务一起变慢

第一次故障恢复后,我们最初怀疑的是某个业务任务瞬间放大了数据库压力。因为当时能看到的现象包括:

  • PostgreSQL 和 MongoDB 响应异常;
  • Docker 命令超时;
  • 应用接口大量超时;
  • SSH 连接困难;
  • 重启服务器后立即恢复。

几天后问题再次出现,而且症状几乎相同。第二次事故发生后,我们没有立即把重点放到某个应用日志上,而是先回答一个更基础的问题:服务器到底在等什么?

CPU 使用率高并不一定代表计算繁忙。如果大量 CPU 时间落在 I/O Wait,说明处理器本身没有工作,而是在等磁盘完成请求。通过sar查看历史 CPU 数据后,故障高峰的 I/O Wait 一度达到约 84%。对于一台只有 2 vCPU 的机器,系统负载却上升到 35~85,同时有 20~78 个任务处于阻塞状态。

这一组数据说明,卡顿的直接表现是磁盘 I/O 阻塞,而不是普通的 CPU 计算瓶颈。

sar-usar-q

磁盘确实被打满了,但这还不是根因

继续查看块设备历史数据,OS Disk 在故障期出现了非常明显的饱和:

  • 读取吞吐约 129 MB/s;
  • 磁盘队列深度约 180~405;
  • 平均等待时间约 193~527 ms;
  • CPU I/O Wait 超过 80%。
sar-d-p

如果停在这里,结论很可能是“磁盘规格不够”。但磁盘繁忙只说明有大量读写请求,无法解释这些请求为什么突然出现。尤其这次主要是读,而不是持续写入。数据库批处理、日志爆量和备份任务通常会留下比较明显的写入特征,这与现场数据并不完全一致。

于是排查方向从“谁在读磁盘”前移了一层:系统为什么需要在短时间内重新读取这么多数据?

真正的转折:匿名内存从 5 GB 涨到 15 GB

sar -r ALL保留了故障前完整的内存变化。第二次事故中,宿主机匿名内存出现了一条非常规律的上升曲线:

09:20 kbanonpg ≈ 5.12 GB 09:30 kbanonpg ≈ 6.07 GB 10:30 kbanonpg ≈ 8.22 GB 11:30 kbanonpg ≈ 10.80 GB 12:30 kbanonpg ≈ 13.17 GB 13:20 kbanonpg ≈ 15.09 GB

服务器总内存约 16 GB,而且当时没有配置任何 Swap。随着匿名内存上涨,系统可回收的文件缓存和 Slab 不断下降,可用内存最终只剩几百 MB。

sar-rALLfree-m

这解释了为什么最终表现为磁盘读满。匿名内存无法被直接丢弃,又没有 Swap 可以换出,内核只能尽可能回收 Page Cache。业务进程随后再次访问刚被回收的文件页时,只能从磁盘重新读取。内存越紧张,缓存回收越激进;缓存越少,重新读盘越频繁,最终形成页面回收、Major Page Fault 和磁盘读取相互放大的恶性循环。

系统日志里也反复出现了对应信号:

systemd-journald: Under memory pressure, flushing caches.

到这里可以确定:严重 I/O 阻塞是内存耗尽之后的结果。接下来真正要找的是,谁制造了这 10 GB 左右的匿名内存。

先排除 Docker:容器总内存不升反降

生产服务基本都运行在 Docker 中,因此最先检查的是各容器内存。我们部署了一个轻量黑匣子,周期性保存docker stats、容器 PID 映射和pidstat输出。把 Docker 数据与sar时间线对齐后,结果与最初预期相反。

在宿主机匿名内存从约 5.12 GB 增长到 15.09 GB 的同一时段,16 个容器的内存总量从约 4.86 GB 降到了 4.50 GB:

09:00 全部容器约 4.86 GB 09:30 全部容器约 4.87 GB 11:00 全部容器约 4.76 GB 12:00 全部容器约 4.63 GB 13:20 全部容器约 4.50 GB

业务 API、Elasticsearch、PostgreSQL 和 MongoDB 都没有出现与宿主机内存曲线相匹配的增长。

Celery Worker 曾在另一个时间点短暂达到约 1.99 GB,这确实是需要治理的资源风险,但它发生得更早,随后已经回落,无法解释后面持续增加的 10 GB 匿名内存。因此 Celery 不是这次事故的直接触发源。

这一步排除了整个容器层。问题进程一定运行在宿主机上。

从宿主机进程中找到 Cursor Remote

服务器重启后,历史进程的 RSS 已经消失,原来的黑匣子又只记录了pidstat -d,没有记录进程内存。因此无法直接回看“哪个 PID 占了多少 GB”。我们只能把剩余证据按时间重新拼起来:

  1. sar找到匿名内存开始增长的分钟;
  2. 从 Docker 数据证明增长不在容器内;
  3. 从宿主机持久日志查找同一时间启动的进程;
  4. pidstat验证这些进程当时是否持续读盘。

在宿主机进程列表中,除了 Azure Agent 和 Docker 守护进程,还存在一组位于用户目录下的 Node.js 进程:

~/.cursor-server/.../server-main.js ~/.cursor-server/.../bootstrap-fork --type=fileWatcher ~/.cursor-server/.../bootstrap-fork --type=extensionHost

这就是 Cursor 通过 Remote SSH 连接 Linux 服务器时安装的远程组件。Remote Server 负责连接和文件访问,Extension Host 运行远程扩展,File Watcher 监听工作区文件变化,Agent Worker 为 Cursor Agent 提供远程工作区能力。

真正关键的证据来自 Cursor 自己的日志。

Cursor 把整台服务器当成了工作区

第二次事故中,Cursor Remote 会话在 09:21 启动:

09:21:34 Extension host agent started 09:21:37 Launched Extension Host Process <2757769> 09:21:40 File Watcher: Permission denied (path: /) 09:21:53 [cursor-agent-worker] Workspace roots: /

匿名内存在 09:20 时还保持在约 5.12 GB,09:30 已经升到约 6.07 GB,之后一路增长到 15 GB。进程启动时间与内存曲线的起点几乎完全重合。

问题就出在这行日志:

Workspace roots: /

远程工作区不是某个代码目录,而是 Linux 根目录/。这意味着 Cursor 的文件监听和扩展宿主面对的是整台服务器,包括/etc/var/home/proc/sys,以及/var/lib/docker下的大量容器数据。

从日志看,Cursor Grep 因为/不是 Git 仓库而初始化失败,代码索引 Watcher 也没有成功建立,所以不能简单地把责任归给某一个“代码索引扩展”。真正持续存在的是 Extension Host、Universal File Watcher 和 Agent Worker 这一组进程。Agent Worker 每隔一段时间都会再次记录Workspace roots: /,File Watcher 则持续尝试处理根文件系统。

pidstat也记录到了对应 PID 的磁盘活动:

Extension Host PID 2757769 平均读取约 4.27 MB/s 峰值约 18.7 MB/s 另一个 Cursor Node 进程 PID 2757576 平均读取约 4.79 MB/s 峰值约 15.7 MB/s

在系统进入抖动后,Cursor 日志开始连续出现客户端重连。由于 Remote Server 为断线客户端保留数小时的重连窗口,即使本地 Cursor 窗口已经关闭,远端进程也不一定立即退出。

更早的日志还留下了一条直接证据:Cursor Extension Host 曾经在这台服务器上发生过 V8 Heap OOM。

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

第二条时间线验证:不是一次偶然巧合

只靠一次时间重合还不够。我们重新检查了第一次事故的sar和 Cursor 日志。

第一次事故中,匿名内存在 00:50 时约为 5.14 GB。Cursor Agent Worker 于 00:57 启动并记录Workspace roots: /;01:00 匿名内存升到约 5.83 GB,之后持续增长,04:50 达到约 15.16 GB,服务器随即进入严重阻塞。

00:50 匿名内存约 5.14 GB 00:57 Cursor 工作区根目录确认为 / 01:00 匿名内存约 5.83 GB 04:50 匿名内存约 15.16 GB

两次事故具有相同的起点、相同的增长曲线和相同的最终表现:Cursor Remote 打开/后,宿主机匿名内存开始线性增长,直到机器进入内存压力和 I/O 风暴。这使根因证据从“相关性”变成了可重复的共同模式。

完整故障链

最终还原出的过程如下:

Cursor Remote 将 / 作为工作区 ↓ Extension Host、File Watcher、Agent Worker 处理整台文件系统 ↓ 宿主机 Cursor Node 进程匿名内存持续增长 ↓ 16 GB 内存耗尽,且没有 Swap ↓ 内核大量回收 Page Cache 和其他可回收页面 ↓ 业务进程产生 Major Page Fault,反复从 OS Disk 重新读取 ↓ 磁盘吞吐、队列深度和延迟达到上限 ↓ Docker、数据库、SSH 和 journald 一起阻塞

这里需要区分三个概念:

  • 直接根因:Cursor Remote 错误地将/作为工作区,引发宿主机进程无边界增长;
  • 放大因素:服务器只有 16 GB 内存、没有 Swap、宿主机进程没有内存限制;
  • 故障表现:OS Disk 饱和、数据库超时、Docker 无响应和 SSH 失联。

如果只处理磁盘或数据库,只能暂时缓解症状,无法阻止问题再次发生。

处理方案

1. 生产服务器不再使用 Cursor Remote

最直接的处理是停止在生产环境通过 Cursor Remote 开发。普通 SSH、SCP、SSH 隧道和只读诊断脚本不会启动.cursor-server进程,可以继续使用。

如果确实需要远程编辑,也必须只打开明确的项目目录,绝不能选择//home/var/var/lib/docker

关闭本地 Cursor 后,还要确认服务器没有遗留进程:

ps-ef|grep'/.cursor-server/'|grep-vgrep

2. 增加 4 GB Swap 作为缓冲

我们在 Azure 临时资源盘/mnt上增加了 4 GB Swap,并将vm.swappiness设置为 10:

swapon--showfree-hsysctlvm.swappiness

最终状态为:

Swap: 4.0 GiB,当前使用 0 B vm.swappiness = 10

Swap 不是根因修复。它的作用是在未来再次发生异常内存增长时,为运维登录、告警和进程终止留出时间,避免机器直接进入不可操作状态。Azure 临时资源盘可能在 Stop/Deallocate 后清空,因此同时配置了 cloud-init per-boot 脚本,在启动时自动重建 Swap。

3. 改进黑匣子

原来的黑匣子记录了每进程 I/O,却没有记录 RSS,导致重启后无法直接看到历史内存责任进程。后续采集至少应包括:

pidstat-r-dps-eopid,ppid,rss,vsz,etime,comm,args--sort=-rssdockerstats --no-stream

每条记录都需要时间戳,同时保存 PID、PPID、完整命令行和 cgroup/容器映射。采集命令本身要设置超时,避免系统已经卡顿时监控进程反过来增加负担。

4. 增加资源边界

开发工具不应该和生产业务共享无限制的宿主机资源。即使以后偶尔需要远程工具,也应使用独立账号或 systemd slice,为其配置MemoryHighMemoryMax

业务容器同样需要逐步增加内存限制。虽然它们不是这次事故的根因,但 Celery Worker 出现过接近 2 GB 的峰值,没有边界的容器仍然可能制造下一次内存事故。

这次排查留下的几个经验

第一,不要把最后出现的指标当成最初原因。磁盘队列和延迟确实很高,但这次磁盘只是替内存问题“背锅”。从 I/O 阻塞继续向前追,直到解释请求为什么产生,才找到了真正的触发源。

第二,容器化环境不能只看容器。宿主机上的编辑器、监控 Agent、运维脚本和远程扩展都可能消耗大量资源。容器总内存稳定,是这次排查最重要的转折点。

第三,重启会消除最有价值的进程现场。服务器恢复后仍然可以依靠sysstat、持久日志和时间线完成定位,但如果黑匣子提前记录 RSS、命令行和 cgroup,整个过程会简单很多。

最后,生产服务器不是远程开发机。将/作为 IDE 工作区,看起来只是一次路径选择错误,实际却把整个操作系统、Docker 数据目录和运行时文件都交给了文件监听与扩展系统。在资源充足的开发机上,这可能只表现为编辑器变慢;在一台没有 Swap、同时承载数据库和业务容器的生产服务器上,它足以拖死整台机器。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/4 20:31:22

Factorio存档管理全攻略:备份、恢复与导入技巧

Factorio存档管理全攻略&#xff1a;备份、恢复与导入技巧 【免费下载链接】factorio-server-manager A tool to help manage Factorio multiplayer servers including mods and save games. 项目地址: https://gitcode.com/gh_mirrors/fa/factorio-server-manager Fact…

作者头像 李华
网站建设 2026/8/4 20:31:00

音视频开发常见的开源项目汇总

本文系统梳理了音视频开发领域最主流的开源项目,涵盖视频编解码器、流媒体服务器、WebRTC 框架、多媒体框架与播放器、音频编解码、前端播放器、工具类七大方向。每个项目都附有详细介绍、核心特性、适用场景与官方链接,可作为选型参考手册收藏。 目录 一、视频编解码器(Cod…

作者头像 李华
网站建设 2026/8/4 20:18:25

2026年广西发酵料行业:诚信厂家如何脱颖而出?

引言随着生态养殖理念的普及&#xff0c;发酵饲料以其“降本提质环保”的优势&#xff0c;成为解决传统养殖中饲料浪费、肠道问题频发、环境恶化等痛点的关键。在众多发酵料品牌中&#xff0c;南宁百珠汇生物科技有限公司凭借其独特的技术、原料把控、品控体系以及服务增值&…

作者头像 李华
网站建设 2026/8/4 20:15:43

终极指南:awesome-pagespeed-metrics如何彻底改变网页性能监控

终极指南&#xff1a;awesome-pagespeed-metrics如何彻底改变网页性能监控 【免费下载链接】awesome-pagespeed-metrics ⚡Metrics to help understand page speed and user experience 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-pagespeed-metrics awesome…

作者头像 李华
网站建设 2026/8/4 20:15:23

三步永久激活Windows和Office:KMS_VL_ALL_AIO智能激活工具终极指南

三步永久激活Windows和Office&#xff1a;KMS_VL_ALL_AIO智能激活工具终极指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为昂贵的软件授权费用而烦恼吗&#xff1f;KMS_VL_ALL_AIO智能…

作者头像 李华
网站建设 2026/8/4 20:14:39

leetcode刷题(2):链表

文章目录1. 两数相加1.1 解题思路1.2 python 实现1. 3 c 实现2 删除排序链表中的重复元素 ||2.1 解题思路2.2 c 实现3 旋转链表3.1 解题思路3.2 c 实现4 剑指 Offer 06: 从尾到头打印链表4.1 解题思路4.2 c 实现5 剑指 Offer 24. 反转链表5.1 解题思路5.2 c实现21. 合并两个有序…

作者头像 李华