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/create 和 session/resume 时 mcpServerCount 在 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)
复现步骤
- 安装 ZCode 3.2.5,配置若干 stdio MCP server(>=2 个)
- 打开一个工作区,等待 plugin-workspace 也初始化(观察
ps 出现两个 zcode-cli)
- 让客户端持续运行 30 分钟以上(期间触发多次 session create/resume,例如切换会话、模型、长时间空闲后恢复)
- 执行下面的检查命令
期望结果
每个工作区的每种 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
改进建议(供参考)
- 杀进程时 SIGTERM + 超时 SIGKILL 兜底:
node 进程默认不响应 SIGTERM,ZCode 在重启 MCP server 时应跟进 SIGKILL(例如 SIGTERM 后 2 秒无响应强制 KILL)
- 客户端启动时清理上次残留:ZCode 启动时可扫描以本应用为祖先的旧 MCP 进程并清理(或记录子进程 PID 到状态文件,启动时按文件清理)
- 优化
listMcpServerStatuses 性能:34 秒超时会触发不必要的 MCP 重新协商,加剧进程堆积
- MCP 配置作用域细化:目前用户级 MCP 配置会让每个工作区各起一套,可考虑 stdio MCP 在多工作区间复用(或仅主工作区拉起)
MCP server 进程在重连/重启时未回收旧实例,长期运行残留进程持续堆积
现象
ZCode 客户端长时间运行后,stdio 类型的 MCP server 进程会持续堆积:
zcode-cli重启后变成孤儿进程,被 systemd 收养,永不退出5 个膨胀到 30+,持续占用内存(每个 node 进程 50150MB)观察到最严重时,系统中有 30+ 个 MCP 相关 node 进程,内存占用超 3GB。
环境
~/.zcode/cli/config.jsonMCP 配置摘要(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进程。日志证据:进程树:
由于 MCP 配置写在用户级
~/.zcode/cli/config.json,每个工作区的 cli 启动时都会读同一份配置并各自拉起一套完整 MCP server。结果就是 MCP 进程数 = 工作区数 × stdio MCP 数。问题 2(核心 bug):MCP server 重启时旧子进程未被回收
这是导致进程持续堆积的根本原因。日志显示 MCP 状态查询频繁超时:
4 次 MCP 状态查询全部稳定超时 34 秒(不是偶发)。超时触发 cli 重新协商 MCP 连接,表现为
session/create和session/resume时mcpServerCount在 0/7 之间反复跳变(今日日志中跳变 63 次):每次 "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 报告采集时,系统仍在持续产生孤儿):
这两个孤儿进程的
ppid直接是 systemd(PID 6356),祖先链中已经没有任何zcode-cli:复现步骤
ps出现两个zcode-cli)期望结果
每个工作区的每种 stdio MCP server 只应有一个活跃进程副本,旧实例应被回收。
实际结果
每种 MCP server 存在多个副本,且包含被 systemd 收养的孤儿进程。
检查命令
影响范围
listMcpServerStatuses超时 34 秒,导致 MCP 状态面板长时间无响应临时缓解
我已编写清理脚本定时收敛残留进程(每小时执行):
改进建议(供参考)
node进程默认不响应 SIGTERM,ZCode 在重启 MCP server 时应跟进 SIGKILL(例如 SIGTERM 后 2 秒无响应强制 KILL)listMcpServerStatuses性能:34 秒超时会触发不必要的 MCP 重新协商,加剧进程堆积