DSH plugin Web 打开老会话就卡死?约 50 分钟内存泄漏 OOM 崩溃排查与规避

故障排查发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginWeb 内存泄漏OOM大会话
DSH plugin Web 打开老大会话就卡死、GET / 超时,或运行约 50 分钟报 JavaScript heap out of memory?根因是冷历史全量物化叠加 4 处无界累积。物理移出大会话目录、保持单实例即可恢复。

DSH plugin Web 出现两类内存症状:打开一个老的大会话就让整个服务卡死(连 GET / 都超时),或连续使用约 50 分钟后报 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory——根因是冷历史全量物化叠加上 4 处无界累积点。 两者都发生在与 Web 服务共享的那个 Node 主线程上,所以症状往往是「整个界面没响应」,而不是单个会话坏掉。把大会话目录物理移出 sessions/、保持单实例运行是目前最有效的规避。

DSH plugin 的两种症状:开老会话卡死与约 50 分钟 OOM

同一个设计缺口在两个时间尺度上发作:打开历史时一次性物化,和长时间运行时持续累积。 社区两组实测记录:

  1. 冷历史把主线程占满dsh web 正常打印 http://127.0.0.1:3080、端口也在监听,但 HTTP 请求 10 秒级超时(HTTP_ERROR after 10185ms);期间 node 工作集约 163 MB、CPU 占用接近满载、3080 端口上堆积多条 CLOSE_WAIT。对照实验确认元凶是历史加载:把两个受影响的会话目录移出活动根目录并重启后,GET / 首次 243 ms、热请求约 17 ms、CPU 占用降到 0.02 CPU 秒(#1550)。
  2. 运行约 50 分钟堆顶到 4 GB 崩溃:崩溃前 GC 日志显示堆在约 3024 秒内从 4.0 GB 涨到 4067/4090 MB,单次 Mark-Compact 耗时 3.1 秒、期间服务无响应,退出码 134;0.1.0-rc.50.1.1-rc.2 都能复现(#3876)。
  3. 启动期同样会全量物化:15 个有效会话(每个压缩 1.8–2.7 MB、约 30 万 token)放在 sessions/<cwd>/ 时,dsh web 在 60–90 秒内爬到约 3 GB 堆,且没有任何客户端交互;CPU 采样约 60% 落在 structuredClone,堆快照显示 2160 万个存活对象(Object 526 MB、字符串 329 MB,其余是 assistant/chunkreasoning-delta 等事件对象)。
  4. Linux 侧的形态是 RSS 而非 JS 堆0.1.2-rc.1 重度使用时段 20 分钟内 5 连崩,活进程取证显示 V8 堆全程平稳(213→206 MB),但 RSS 从 430 MB 涨到 1.16 GB,约 737 MB 落在 glibc [heap],gdb 抓到的分配栈是 ZSTD_decompressStreamJsonStringifier::Extend;负载停止后 RSS 曾从 1190 MB 自发回落到 390 MB。

DeepSeek Harness 的 4 处无界累积与冷历史全量物化机制

写侧有 4 个只进不出的容器,读侧则把整份日志解到底才分页,两者共用同一个事件循环。 逐点对应源码:

  1. FrameQueue.buffer 无上限packages/host/apiproxy/src/api-proxy.ts):push() 无条件入队,没有背压也没有丢弃;每个 mux/host 订阅者一条队列,每个 session/event 帧要给每个订阅者复制一份。浏览器标签挂起或消费变慢(TCP 未断)时帧就无限堆积,且每帧持有完整事件引用。
  2. Session.log append-onlypackages/core/session/src/index.ts):事件数组只追加、不裁剪,长时间会话的全部事件永久驻留。
  3. 投影缓存行从不删除packages/session/session-projection-cache/src/index.ts):session/disposed 时只做 markClean(session)dirty.delete(session),没有删除底层表的行;KvTable 是常驻内存 Map,行数随历史会话数无界增长。
  4. SessionWriteBehind.pending 写失败无限保留packages/session/session-persistence/src/write-behind.ts):this.pending = batch.concat(this.pending) 在写失败后整批保留重试、无上限,且每个事件还各做一次 structuredClone
  5. 读侧的全量物化链#1550):historySourceFor()sessionPersistence.inspect(sessionId) 拿完整事件数组,之后才 paginate;底层 readPrefixreadZstdPrefix 解码每一个帧scanLog 解析整个缓冲;随后 prepareCoreadoptStoredEvents 对每个事件做迁移与深冻结;再由 Session.create/fromRestoreadoptSessionEvent(structuredClone(event)) 逐个克隆。于是一个事件在一次 prepare 里至少被深拷贝 2–3 次,再加上 seed: loaded.events.map((event) => structuredClone(event)) 的全量种子拷贝(dsh-session-persistence lib/index.js:1390:1586,0.1.2-rc.1 仍在)。
  6. 单次读取的代价可以量化:用最大会话(45 MB zstd → 128 MB 明文)在隔离进程复刻 readRaw,峰值 RSS 从 51 MB 涨到 647 MB(其中 Buffer.concat + toString("utf8") 一步从 296 MB 跳到 647 MB)——工作区里同时物化 2–3 个这种会话就足以击穿 2.1 GB 的默认上限。
  7. 同一条物化路径也解释了大日志内搜索崩溃:整份日志一次 stringify 超出 V8 字符串上限即报 RangeError: Invalid string length,同一家族的另一篇将单独展开。

DSH plugin 规避与补丁进展:物理移出大会话、保持单实例

目前最有效的规避是让物化面变小:把不用的大会话目录物理移出 sessions/,并确保只有一个 dsh 实例在跑。 按代价从低到高:

  1. 物理移出大会话目录(而不是只归档)。对照数据:15 个大会话从约 3 GB 堆降到约 306 MB、GET / 从超时回到 0.2 秒;Linux 侧把 106 个 GUI 已归档会话(共 385 MB,含 45/36/35 MB 的大会话)移出后,剩余最大会话仅 5 MB、服务恢复稳定。archivedSessionIds 不是物化边界,归档了也仍留在盘上、仍可被加载。

  2. 只保留一个写者。多进程写同一份会话会引来日志损坏(seq gap 家族),损坏的日志又会走进同一条全量扫描路径,把小问题放大成整站卡死;双开的排查与恢复见 会话损坏排查

  3. 先离线看清哪条会话有问题,再决定隔离谁。社区会话医生类工具走自己的 loopback API,只读 header、按需解码,不触发官方 history 的物化路径(例如 dsh-session-surgeon,安装方式与其它 DeepSeek插件一致):

    sh
    dsh plugin --profile web add github:xiaoshenming/dsh-session-surgeon
    

    装 DSH插件 推荐走 DSH Plugin Hub 的「设置 → 插件市场」,失败会回滚 manifest、不会把 profile 写坏。

  4. 慎用 --max-old-space-size。把上限从 2 GB 提到 4 GB 后 V8 GC 更「懒」,实测出现过 cgroup 峰值 7.3 GB——在内存受限、swap 紧张的机器上这只是把「当场崩溃」换成「更高水位」,不是治疗。

  5. 客户端也要留预算。服务端物化之外,前端会话视图是全量渲染、没有虚拟化,长时间重会话下浏览器渲染进程会独立膨胀(Safari/WebKit 实测 RSS 达 11.27 GB,杀掉进程后新渲染进程仅 0.55 GB,而会话日志只有 796 KB)——同样的事件既压服务端也压浏览器。

  6. 补丁进展:社区已有两类参考实现——分叉分支 fix/session-history-responsiveness(跨进程写者租约 + Zstd 解码调度切片从 500 ms 降到 16 ms + 按修订版本缓存确定性读失败,避免重复扫描同一坏文件),以及「有界冷历史读取」补丁(pageSurfaceMessagesreadHistoryWindowhistoryWindowMaxEvents,JSONL 后端改为两遍流式扫描只保留目标页事件)。截至 0.1.2-rc.1,冷启动的全量 structuredClone 种子仍未进入发布版。

DSH plugin 排查注意事项

先分清是「打开即卡」还是「用久了崩」,两者规避动作相同但定位证据不同——第一反应应该是找大会话,而不是怀疑网络与端口。 五条要点:

  1. 分清症状类型:打开即卡是冷历史物化,用久了崩是无界累积;前者看 GET / 延迟与 CPU 占用,后者看 GC 日志与堆曲线。
  2. 别把归档当隔离:真正的边界是文件在不在 sessions/ 目录里。
  3. RSS 涨不一定全是泄漏:gdb 与 malloc_trim 的证据显示相当一部分是分配器高水位;判断时同时看 V8 堆与 RSS 两条曲线。
  4. 单个坏会话可以拖垮全体:这是本问题的放大器,遇到「整站没响应」时先找大会话或损坏会话。
  5. 升级前先备份再移动:移出目录前确认会话已无进程持有,保留可回迁的路径记录。
DSH Plugin Hub 插件市场:安装会话诊断类插件、查看版本与更新

来源:Discussion #3876Discussion #1550Discussion #1859

常见问题

DSH plugin Web 运行约 50 分钟后报 JavaScript heap out of memory 是什么原因?

DSH plugin Web 端约 50 分钟后 OOM,实测定位到 4 处无界累积点:api-proxy.ts 的 FrameQueue.buffer 无上限且每个订阅者各复制一份帧、core/session 的 Session.log 只追加不裁剪、session-projection-cache 在 session/disposed 时只标记不删缓存行(底层 KvTable 全量常驻内存)、write-behind 的 pending 在写失败后整批无限保留重试并各做一次 structuredClone。两版发布(0.1.0-rc.5 与 0.1.1-rc.2)均可复现(来源:Discussion #3876)。

为什么打开一个老会话会让 DeepSeek Harness 的 Web 服务卡死、连 GET / 都超时?

DeepSeek Harness 打开冷会话时,historySourceFor() 会先调用 sessionPersistence.inspect() 取回完整事件数组,之后才 paginate——maxMessages 只限制返回给客户端的载荷,并不限制 Zstd 解压、JSON 解析、校验与事件物化,这些都在 Web 服务同一个 Node 主线程上跑完。实测一个 461,981 事件的会话解码后 16.5 MB,打开期间 GET / 直接超时、端口 3080 上堆积 CLOSE_WAIT(来源:Discussion #1550)。

内存涨上去之后 DSH plugin 怎么缓解,为什么把会话归档到 archivedSessionIds 没用?

DSH plugin 的 archivedSessionIds 只是显示层过滤,被归档的大会话仍留在 sessions/ 目录里、仍会被全量加载。对照实验:15 个大会话时启动堆约 3 GB、GET / 超时;把 11 个会话目录物理移出 sessions/<cwd>/ 后堆稳定在约 306 MB、GET / 回到 0.2 秒。把 preparedSessionCacheSize 调成 1 也无效,它是复用保留量的旋钮,不是物化闸门(来源:Discussion #1550)。

这个 DSH plugin 内存泄漏问题在最新版本修好了吗,调大 --max-old-space-size 有用吗?

截至 dsh 0.1.2-rc.1,DSH plugin 这个内存问题仍未修复:用 npm 拉包实核,dsh-session-persistence 里 seed: loaded.events.map((event) => structuredClone(event)) 的全量深拷贝仍在(lib/index.js:1390 / :1586);Linux 侧 0.1.2-rc.1 重度使用 20 分钟内 5 连崩。调大 --max-old-space-size 只是把当场崩溃换成更高的内存峰值(实测出现过 cgroup 峰值 7.3 GB),不是治疗;社区已有分叉补丁与有界冷历史读取补丁在推进(来源:Discussion #3876)。

相关术语

cold history(冷历史)
cold history 是不在当前进程内存里的历史会话。打开它需要从磁盘读取 Zstd 记录、解码、解析、校验并物化成事件对象,成本与日志体积成正比。https://github.com/deepseek-ai/deepseek-harness/discussions/1550
backpressure(背压)
backpressure 是当消费端(如浏览器标签)跟不上生产端(会话事件流)时,生产端主动减速、限长或合并的策略;公开讨论的 FrameQueue 没有背压,慢消费者会导致帧无限堆积。https://github.com/deepseek-ai/deepseek-harness/discussions/3876
allocator high-water mark(分配器高水位)
allocator high-water mark 是内存被释放后未归还操作系统的部分,表现为 RSS 只涨不落;实测 malloc_trim 后 RSS 从 5.48 GB 回落到 549 MB,说明相当一部分增长不是常驻泄漏而是高水位。https://github.com/deepseek-ai/deepseek-harness/discussions/3876

来源