Skip to content

MCP server 进程在重连/重启时未回收旧实例,长期运行残留进程持续堆积 #89

Description

@lichaoAndLzq

MCP server 进程在重连/重启时未回收旧实例,长期运行残留进程持续堆积

现象

ZCode 客户端长时间运行后,stdio 类型的 MCP server 进程会持续堆积:

  • 同一种 MCP server 会出现 4~6 个进程副本(正常应为 1 个)
  • 部分"旧副本"在父级 zcode-cli 重启后变成孤儿进程,被 systemd 收养,永不退出
  • 进程数从正常的 5 个膨胀到 30+,持续占用内存(每个 node 进程 50150MB)

观察到最严重时,系统中有 30+ 个 MCP 相关 node 进程,内存占用超 3GB。

环境

版本/值
ZCode 3.2.5
OS Ubuntu 26.04 LTS (kernel 7.0.0-27-generic)
Node v24.15.0 (nvm 管理, MCP server 实际运行时)
MCP 配置 7 个 MCP server(5 个 stdio + 2 个 http),全部配在用户级 ~/.zcode/cli/config.json

MCP 配置摘要(stdio 类型的会拉起本地进程):

{
  "mcp": {
    "servers": {
      "mysql-mcp-server": { "type": "stdio", "command": "npx", "args": ["-y", "mysql-mcp-server"] },
      "chrome-mcp":       { "type": "stdio", "command": "npx", "args": ["-y", "chrome-devtools-mcp@latest"] },
      "dev-mysql-mcp":    { "type": "stdio", "command": "npx", "args": ["-y", "mysql-mcp-server"] },
      "dev-v2-mysql":     { "type": "stdio", "command": "npx", "args": ["-y", "mysql-mcp-server"] },
      "zai-mcp-server":   { "type": "stdio", "command": "npx", "args": ["-y", "@z_ai/mcp-server"] },
      "web-reader":       { "type": "http",  "url": "..." },
      "yank-note-mcp":    { "type": "http",  "url": "..." }
    }
  }
}

根因分析

通过进程树和客户端日志,我定位到三个相互叠加的问题:

问题 1:多工作区会拉起多套 MCP server(进程数翻倍)

ZCode 启动时,会为每个工作区 spawn 一个独立的 zcode-cli 进程。日志证据:

08:09:03.597 [zcode-agent] ZCode agent process started
           {"workspaceKey":"/home/lichao/project/post_matrix_old","pid":9100}

08:09:39.922 [zcode-agent] ZCode agent command resolved
           {"workspaceKey":"/home/lichao/.zcode/plugin-workspace"}    ← 第二个工作区
08:09:39.931 [zcode-agent] ZCode agent process started
           {"workspaceKey":"/home/lichao/.zcode/plugin-workspace","pid":9742}

进程树:

ZCode(8169)
└─ zcode-host-local-1(8395)
   ├─ zcode-cli(9100)  ← 服务 post_matrix_old 工作区
   │   └─ npm exec @playwright/mcp → sh -c → node playwright-mcp
   │   └─ npm exec mysql-mcp-server → sh -c → node mysql-mcp-server
   │   └─ ... (5 个 stdio MCP server)
   └─ zcode-cli(9742)  ← 服务 plugin-workspace (插件工作区)
       └─ ... (又一套 5 个 stdio MCP server)

由于 MCP 配置写在用户级 ~/.zcode/cli/config.json,每个工作区的 cli 启动时都会读同一份配置并各自拉起一套完整 MCP server。结果就是 MCP 进程数 = 工作区数 × stdio MCP 数。

问题 2(核心 bug):MCP server 重启时旧子进程未被回收

这是导致进程持续堆积的根本原因。日志显示 MCP 状态查询频繁超时:

08:10:14.552 [rpc:call] zcode-agent.listMcpServerStatuses OK (34629.8ms)   ← 34.6 秒!
08:26:02.959 [rpc:call] zcode-agent.listMcpServerStatuses OK (34077.0ms)
08:27:20.955 [rpc:call] zcode-agent.listMcpServerStatuses OK (34071.1ms)
08:36:16.833 [rpc:call] zcode-agent.listMcpServerStatuses OK (34070.2ms)

4 次 MCP 状态查询全部稳定超时 34 秒(不是偶发)。超时触发 cli 重新协商 MCP 连接,表现为 session/createsession/resumemcpServerCount 在 0/7 之间反复跳变(今日日志中跳变 63 次):

08:09:23 session/create  mcpServerCount:7    ← 首次正常
08:11:27 session/create  mcpServerCount:0    ← MCP 全部"丢失"
08:11:38 session/create  mcpServerCount:7    ← 重新拉起
08:11:46 session/resume  mcpServerCount:0    ← 又丢失
08:13:12 session/resume  mcpServerCount:7    ← 又拉起
...

每次 "0 → 7" 的重新拉起,都会 spawn 一批新的 stdio MCP server 进程,但旧的进程没有被 kill。node 进程对 SIGTERM 不敏感(默认不注册 handler),如果 ZCode 只发 SIGTERM 而不跟进 SIGKILL,旧进程会一直存活。

问题 3:孤儿进程无法被 ZCode 回收

zcode-cli 自身重启时,它名下的 stdio MCP server 子进程的父进程会变成 PID 1(systemd)。这些孤儿进程的进程链已经断开,ZCode 永远无法再触达它们。

实测证据(本次 bug 报告采集时,系统仍在持续产生孤儿):

归属                          et(s)   PID     名称
9100 (cli)                  1665    11232   mysql-mcp-server      ← 正常
9100 (cli)                  1665    11264   context7-mcp          ← 正常
9742 (cli)                   221    52002   mysql-mcp-server      ← 正常
ORPHAN(被systemd收养)         753    49844   mysql-mcp-server      ← ★ 残留孤儿
ORPHAN(被systemd收养)         831    44464   mysql-mcp-server      ← ★ 残留孤儿

这两个孤儿进程的 ppid 直接是 systemd(PID 6356),祖先链中已经没有任何 zcode-cli:

$ cat /proc/49844/status | grep PPid
PPid: 6356   (systemd --user)

复现步骤

  1. 安装 ZCode 3.2.5,配置若干 stdio MCP server(>=2 个)
  2. 打开一个工作区,等待 plugin-workspace 也初始化(观察 ps 出现两个 zcode-cli)
  3. 让客户端持续运行 30 分钟以上(期间触发多次 session create/resume,例如切换会话、模型、长时间空闲后恢复)
  4. 执行下面的检查命令

期望结果

每个工作区的每种 stdio MCP server 只应有一个活跃进程副本,旧实例应被回收。

实际结果

每种 MCP server 存在多个副本,且包含被 systemd 收养的孤儿进程。

检查命令

# 统计每种 MCP server 的实体进程数(>=2 即存在残留)
ps -axo pid,ppid,etimes,command \
  | grep -E '/(context7-mcp|playwright-mcp|mysql-mcp-server|zai-mcp-server)$|chrome-devtools-mcp$' \
  | grep -v 'npm exec' | grep -v 'sh -c'

# 查找孤儿进程(ppid 不是 zcode-cli 链路上的)
ps -axo pid,ppid,command | grep -E 'mysql-mcp-server|context7-mcp' | grep -v grep
# 如果某进程的 ppid 是 systemd(PID 1 或 systemd --user)即孤儿

影响范围

  • 内存:每个残留 node 进程 50~150MB,堆积到 30+ 个时占用超 3GB,严重时导致系统卡顿
  • 稳定性:孤儿 mysql-mcp-server 仍持有数据库连接,可能引发连接数耗尽
  • 可观测性:listMcpServerStatuses 超时 34 秒,导致 MCP 状态面板长时间无响应

临时缓解

我已编写清理脚本定时收敛残留进程(每小时执行):

# ~/.local/bin/zcode-mcp-cleanup.sh
#   - 每个 cli 下,每种 MCP server 只保留最新(运行时间最短)的实体
#   - 孤儿进程(被 systemd 收养)直接清理
#   - SIGTERM 无效时升级 SIGKILL(node 进程对 SIGTERM 不敏感)
# crontab:
#   17 * * * * ~/.local/bin/zcode-mcp-cleanup.sh --apply

改进建议(供参考)

  1. 杀进程时 SIGTERM + 超时 SIGKILL 兜底:node 进程默认不响应 SIGTERM,ZCode 在重启 MCP server 时应跟进 SIGKILL(例如 SIGTERM 后 2 秒无响应强制 KILL)
  2. 客户端启动时清理上次残留:ZCode 启动时可扫描以本应用为祖先的旧 MCP 进程并清理(或记录子进程 PID 到状态文件,启动时按文件清理)
  3. 优化 listMcpServerStatuses 性能:34 秒超时会触发不必要的 MCP 重新协商,加剧进程堆积
  4. MCP 配置作用域细化:目前用户级 MCP 配置会让每个工作区各起一套,可考虑 stdio MCP 在多工作区间复用(或仅主工作区拉起)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions