Skip to content

[Bug] Docker devcontainer 连接部署被逐探针的 docker ps -a 放大为数十分钟卡顿(容器解析未缓存) #861

Description

@nightwares

问题类别 · Category: 性能 / 远程连接 · Performance / Remote (Docker)
Agent 框架 · Framework: ZCode Agent(自研)
严重程度 · Severity: 影响体验 · Major(功能可用但连接等待极长 / works but connection takes tens of minutes)
复现频率 · Reproducibility: 必现,时长随 Docker daemon 状态波动 · Always (duration scales with daemon latency)

问题描述 · Description

Docker devcontainer 连接时的部署阶段会对容器做一连串探针(exists() / readFile()),而每一个探针都会重新解析一次容器:execDirect()(packages/server/src/remote/docker-backend.ts)每次调用 resolveContainer() → listDockerContainers({ all: true }),即执行一次完整的 docker ps -a。只有流式 exec() 走了缓存的 resolveInfo(),探针路径完全没有缓存。此外每次 exec() 前还会执行一次 docker version(ensureAvailable()),且所有 docker 元数据调用都没有超时。

一次"所有组件已是最新、全部 skip"的连接会顺序执行约 27 个这样的探针:server 部署决策 ×2(docker 后端总是要付 pre-lock + post-lock 双重检查——只有 SSH 使用 deployLockMode: "caller-serialized")、node-pty、glm 的 9 个官方插件资产 exists() + 身份 marker 读取 + 2 个 exists()、以及 bfs/ripgrep/ugrep 各 2 个探针。在 docker ps -a 较慢的主机上(本机实测 40s–3min,约 170 个容器、daemon 偶发压力时段),27 × 40s–3min = 30–45 分钟的连接等待,且中间没有任何日志输出。同一主机上 VS Code Remote 连接同一容器是秒级的——它只解析一次容器。

Every exists()/readFile() probe during the Docker devcontainer deploy re-resolves the container by running a full docker ps -a (execDirect() → resolveContainer(), uncached; only the streaming exec() path uses the cached resolveInfo()), plus a docker version before every exec(), with no timeout on any of these. A fully up-to-date no-op connection issues ~27 such probes sequentially (server decision ×2 — the docker backend always pays the pre-lock + post-lock double check — node-pty, glm's 9 plugin-asset probes + identity reads, and 2 probes × 3 runtime tools). On a host where docker ps -a takes 40s–3min, that is a 30–45 minute "startup" with skip on every line and silence in between. VS Code Remote connects to the same container instantly on the same host.

复现步骤 · Steps to reproduce

  1. 准备一台 docker ps -a 较慢的主机(容器数量多或 daemon 处于压力时段;用 time docker ps -a 验证,>30s 即可明显复现)/ Use a host where time docker ps -a takes tens of seconds (many containers, or daemon under pressure)
  2. 用 ZCode 连接一个已完整部署过的 Docker devcontainer(预期所有组件 skip)/ Connect ZCode to an already fully deployed Docker devcontainer
  3. 观察 ~/.zcode/v2/logs/<date>.log 中相邻 [deploy] 行的时间间隔 / Watch the gaps between [deploy] log lines

实际表现 · Actual behavior

本机实测时间线(ZCode v3.14.3,全程所有步骤最终都是 skip):

13:55:48 [deploy] selected remote resource packages: server-bundle,node-runtime,node-pty,glm,bfs,ripgrep,ugrep
13:55:48 [deploy] checking if deploy needed...
13:57:43 [deploy] remote node exists: true              <-- 1m55s(首个探针含 3 次 docker ps)
13:58:22 [deploy] remote server exists: true            <-- +39s
13:58:22 [deploy] checking remote version...            <-- 秒级(走了缓存的 resolveInfo)
13:58:23 [deploy] checking if deploy needed...          <-- 整个决策第二轮
13:59:02 [deploy] remote node exists: true              <-- +39s
13:59:40 [deploy] remote server exists: true            <-- +38s
14:00:58 [deploy] skipped — remote version matches      <-- +78s
14:01:37 [deploy] node-pty prebuild already exists, skip
14:31:39 [deploy] [zcode-agent-deploy] glm: 制品 SHA … 已部署,跳过    <-- 30 分钟,中间无任何日志
14:33:25 [deploy] [tool-deploy] bfs: 远程版本 v4.1.1-2 已是最新,跳过   <-- +1m46s
14:35:28 [deploy] [tool-deploy] ripgrep: 远程版本 v14.1.1-1 已是最新,跳过 <-- +2m03s

其中 30 分钟的静默窗口是 glm 的 skip 路径:12 个顺序探针(9 个插件资产 exists() + 身份 readFile() + 2 个 exists()),路径中不涉及任何下载 / 解压 / 哈希——纯探针往返开销。另外 exists() 会吞掉所有异常返回 false,daemon 慢或超时会把"应 skip"静默误判为"需要全量重部署"。

The silent 30-minute window is glm's skip path: 12 sequential probes, no download/extract/hash involved — pure probe latency. Note exists() swallows all errors and returns false, so a slow/failing daemon silently turns a skip decision into a full redeploy.

期望表现 · Expected behavior

  • 探针类操作按连接缓存容器名(成功缓存一次;失败不缓存,devcontainer 仍在启动时可继续重试)
  • docker version 可用性探测同理按连接缓存
  • docker 元数据调用(version / ps)设置超时,daemon 卡顿时快速、显式失败,而不是无限期静默挂起
  • no-op 连接应只付 1 次 docker ps -a + 若干次 docker exec(与 VS Code Remote 同量级)

版本与环境 · Version & environment

  • ZCode v3.14.3(当前公开源码版本 / current public source)
  • Linux(Manjaro)桌面端,Docker 29.7.2(overlay2),主机约 170 个容器
  • 连接目标:Docker devcontainer;对照:同一主机同一容器,VS Code Remote 秒级连接

附:修复方案 · Proposed fix

已基于公开源码准备了一个小补丁(2 个文件,+43/−7),要点:

  1. execDirect() 改用按 backend 实例缓存的 resolveContainerName()(成功缓存、失败不缓存,容器启动中的重试语义不变)——单次连接从 ~27 次 docker ps -a 降到 1 次
  2. ensureAvailable() 的 docker version 探测同样按实例缓存(成功缓存、失败可重试)
  3. docker-detect.ts 的 version/ps 元数据调用增加 30s 超时

pnpm tsc -b packages/server 与 oxlint 已通过。如需要可提供补丁文件或 fork 分支 PR。后续可进一步优化(不在本补丁内):把 ~20 个存在性探针合并为一次 docker exec 脚本、用 docker inspect <name> 替代全量列表、docker 后端跳过 pre-lock 双重决策、exists() 区分"探针失败"与"文件不存在"。

感谢!/ Thank you!

Activity

  1. github-actions commented on Sep 28, 2026

    @github-actions

    👋 感谢你的反馈,我们已经收到。

    • 维护者看到后会尽快回复你。
    • 状态保持为 status: 待评估,你可以随时补充信息。
    • 信息不全时我们会打上 needs: 更多信息 标签并 @ 你。

    👋 Thanks — we've received your issue.

    • A maintainer will get back to you as soon as we can.
    • Status stays at status: 待评估 (Triage); feel free to add context.
    • If we need more details, we'll add needs: 更多信息 and ping you.
  2. nightwares commented on Oct 6, 2026

    @nightwares
    Author

    根因补充 / Root cause refinement(基于后续复现与最小化实验 / based on follow-up minimization):

    之前的分析把重点放在“每个探针重复执行 docker ps -a”上,这点仍然成立(缓存容器解析依然必要),但单次调用本身慢的真正原因现在定位到了格式模板:

    docker ps -a --format '{{json .}}' 会让 CLI 向 daemon 请求每个容器的磁盘占用(等价于 size=1)。在 overlay2 metacopy=true、Native Overlay Diff=false 的主机上,daemon 需要对每个容器的可写层做全量遍历——多 GB 的 devcontainer 会让一次“列表容器”退化到分钟级。

    同机同 daemon 实测 / measured on the same host and daemon:

    调用 耗时
    docker ps -a --format '{{json .}}' ~5m20s
    docker ps -a(表格)/ --format '{{.Names}}' 13–97ms
    GET /containers/json?all=1(raw API,无 size) 16ms
    GET /containers/json?all=1&size=1(raw API) ~4m37s
    --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}' 15ms

    期间 daemon 日志会出现 Can not stat .../overlay2/<id>/merged/...(对已删除文件的全量遍历签名)。

    修复建议更新:在容器解析缓存之外,listDockerContainers 应避免 {{json .}},改用只含所需字段的 TSV 模板——实测在受影响主机上把单次调用从 ~5 分钟降到 ~15ms。已在 fork(nightwares/ZCode,分支 fix/861-docker-deploy-probe-latency)实现:{{json .}} → ID/Names/Image/State/Status TSV + 对应解析器。

    In short: the {{json .}} template silently requests per-container sizes (size=1); with metacopy=true and no native overlay diff, each listing becomes a full walk of every container's writable layer (~5 minutes on multi-GB devcontainers, vs 16ms for the same API call without sizes). Switching to a field-only TSV format drops it to ~15ms. Implemented in the fork branch referenced above, alongside the container-resolution caching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions