首页
/ DeerFlow 沙箱内存剖析:在 Kubernetes 上建立沙箱内存基线与候选运行时对比方法

DeerFlow 沙箱内存剖析:在 Kubernetes 上建立沙箱内存基线与候选运行时对比方法

2026-09-06 11:15:30作者:凤尚柏Louis

DeerFlow(deer-flow)在 Kubernetes 部署时,单个沙箱 Pod 的内存占用曾接近 1 GiB(对应 Issue #3213),这直接决定了网关能同时托管多少个并发会话。仓库文档 SANDBOX_MEMORY_PROFILING.md 定义了一套可重复的沙箱内存剖析流程:在更换或引入新的沙箱运行时之前,先采集当前 AIO 沙箱的内存基线,再用完全相同的 DeerFlow 工作负载去对比候选后端。本文基于该文档与配套的剖析脚本 scripts/sandbox_memory_profile.py,完整讲解"测什么、怎么测、怎么解读、怎么对比"的整套方法论。

为什么需要一份可重复的沙箱内存基线

DeerFlow 的执行面依赖沙箱:bash、Python/Node 任务、文件读写、上传与产物输出都发生在沙箱容器内。沙箱由 AIO 沙箱供应商编排,其生命周期包括进程内缓存加速重复访问、空闲超时回收与优雅关闭,挂载路径则按线程与技能目录计算(见 AioSandboxProvider 的模块说明)。当 Issue #3213 报告 K8s 上每沙箱内存接近 1 GiB 时,团队面对的问题是:这个数字里有多少是运行时本身、多少是页缓存、多少是进程 RSS?在回答之前,文档给出了一条原则——任何"新 Provider 能降低高并发内存"的结论,都必须建立在同一工作负载下 AIO 与候选后端的双侧实测之上

这正是"基线先行"的工程意义:先固定度量方法,再谈优化与选型。

测什么:七个必须覆盖的采样阶段

文档要求至少测量以下 7 类样本,覆盖沙箱从空闲到并发复用的完整生命周期:

  1. 沙箱就绪后的空载状态(empty);
  2. 执行一条简单 bash 命令之后;
  3. 运行导入常用包的 Python 任务之后;
  4. 预期存在 Node 负载时,运行 Node 任务之后;
  5. /mnt/user-data/outputs 下生成文件之后;
  6. 释放(release)之后被温池复用(warm reuse)的状态;
  7. 目标并发规模下——例如 10、50 或 100 个沙箱。

这几个阶段与 DeerFlow 沙箱生命周期的真实机制一一对应。第 6 项"释放后的温池复用"对应的是温池生命周期机制:共享的 WarmPoolLifecycleMixin 定义了 idle_timeout(默认 600 秒,即 10 分钟,设为 0 表示禁用空闲回收)与 replicas(默认 3)两个容量参数,空闲检查线程每 60 秒扫描一次温池,回收过期条目或为副本软上限驱逐最旧的温池条目。换句话说,"释放"在 DeerFlow 中并不总是意味着容器销毁——被释放的沙箱可能进入温池等待复用,其内存仍会计入 cgroup,这正是必须单独采样"release + warm reuse"阶段的原因。

关于数字解读,文档特别强调 kubectl top 报告的是 Kubernetes/容器的工作集内存(working set),只能作为容量信号,不能当作独占的 RSS/PSS 使用。Pod 级内存包含 Pod 内所有容器,还可能包含计入 cgroup 的页缓存。因此文档要求:结果看起来异常时,先检查节点上的沙箱进程与 cgroup 指标,再下结论。剖析脚本在 JSON 报告的 notes 字段中也固化了这一提醒(见 build_report 的 notes 列表):进程 RSS 采样来自容器内 ps,不包含页缓存等 cgroup 内存,Pod 级与进程级数字不会精确相等。

采集快照:sandbox_memory_profile.py 的完整用法

剖析入口是仓库根目录下的 scripts/sandbox_memory_profile.py,这是一个刻意保持轻量的脚本:它只依赖标准库,通过 shell 调用 kubectl 输出 JSON 或 Markdown,让维护者无需任何运行时依赖即可对比不同沙箱后端与工作负载。

文档给出的基线采集命令(从仓库根目录执行):

python scripts/sandbox_memory_profile.py \
  --namespace deer-flow \
  --selector app=deer-flow-sandbox \
  --sample empty \
  --include-processes \
  --format markdown

其中 --namespace 默认就是 deer-flow--selector 默认是 app=deer-flow-sandbox(与脚本中 DEFAULT_NAMESPACE / DEFAULT_SELECTOR 常量一致),所以实际使用时往往只需指定 --sample--format。脚本内部依次执行两条 kubectl 命令:

  • kubectl top pod -n <ns> -l <selector> --no-headers —— 获取每个 Pod 的 CPU 与内存;
  • kubectl get pods -n <ns> -l <selector> -o json —— 获取 Pod 的 phase、启动时间、标签,以及每个容器的镜像、requests 与 limits。

两个结果按 Pod 名合并成报告行,这就是为什么 JSON 里能同时看到运行内存与声明资源,方便审计"1 GiB 是否超出 limits"这类问题。

为每个阶段使用有描述性的 --sample 标签

文档建议给每个采样阶段一个有语义的 --sample 值,例如:

python scripts/sandbox_memory_profile.py --sample after-bash --format json
python scripts/sandbox_memory_profile.py --sample after-python --format json
python scripts/sandbox_memory_profile.py --sample after-artifact --format json

--sample 默认值为 unspecified,它只是写入报告的人类可读标签(如 emptyafter-bashafter-pythonafter-artifact),不改变采集逻辑。文档同时要求:对比不同后端时保留原始 JSON,因为总量、Pod 名、镜像、requests/limits 与时间戳都要在事后审计。

进程级采样:--include-processes 与 --process-limit

--include-processes 会在每个沙箱 Pod 内执行 kubectl exec ... ps,具体命令是 ps -eo pid,ppid,rss,args --sort=-rss(失败时回退到不带排序的版本),把 RSS 最高的进程加入报告。这一步用于区分 Pod 级 cgroup 内存与进程 RSS——正如文档所说,两者不会精确相等,因为 cgroup 内存可以包含缓存等内核记账项。

完整参数一览(默认值来自 parse_args):

参数 默认值 说明
--namespace deer-flow Kubernetes 命名空间
--selector app=deer-flow-sandbox Pod 标签选择器
--sample unspecified 人类可读的采样标签
--kubectl kubectl kubectl 可执行文件路径
--format markdown 输出格式,可选 json / markdown
--include-processes 关闭 在每个沙箱 Pod 内执行 kubectl exec ps,附加 RSS 最高的进程样本
--process-limit 10 启用进程采样时,每个 Pod 最多保留的进程数(必须大于 0)
--kubectl-timeout 30 每次 kubectl 调用的超时秒数(必须大于 0)

进程采样是逐 Pod 容错的:单个 Pod 的 kubectl exec 失败或超时不会中断整体采集,错误会记入报告的 process_errors 字段,并在 Markdown 输出中单独列出 "Process Sample Errors" 一节。这一行为由 backend/tests/test_sandbox_memory_profile_script.py 中的 test_collect_process_samples_records_errors_and_continuestest_collect_process_samples_records_timeout_and_continues 两个用例验证——模拟 exec 被拒绝和超时两种场景,断言其余 Pod 的采样照常完成。

解析细节与报告结构

脚本对 kubectl 输出的解析同样有测试背书:

  • 内存单位解析支持 Ki/Mi/Gi/Ti(1024 进制)与 K/M/G/T(1000 进制),如 512Ki0.1Gi100M,无法解析的值返回 None 而非抛错(test_parse_memory_bytes_handles_kubernetes_units);
  • CPU 解析支持 29m 毫核与整数核(1 记为 1000m)(test_parse_top_pods_skips_header_and_preserves_raw_values);
  • 进程表按 RSS 降序排序并截断到 --process-limit,坏行(如 RSS 非数字)被静默跳过(test_parse_processes_sorts_by_rss_and_limits_results);
  • 无法解析的 Pod 会体现在 summaryparsed_memory_count / unparsed_memory_count 计数中,便于发现采集质量问题(test_build_report_counts_unparsed_memory_values)。

JSON 报告(schema_version: 1)的核心结构:

  • captured_at / namespace / selector / sample:采集元数据;
  • summarypod_counttotal_memory_mibaverage_memory_mibmax_memory_mibtotal_cpu_millicores,以及进程采样成功/出错的 Pod 数;
  • pods[]:每个 Pod 的 CPU/内存(raw 值、字节、MiB)、phase、启动时间、标签、各容器的镜像与 requests/limits、进程样本列表;
  • process_errors:按 Pod 名索引的采集错误。

Markdown 输出则给出汇总列表 + "Pod | Phase | CPU | Memory | Start Time" 表格,采样进程时追加每个 Pod 的 "Top Processes" 表(PID / PPID / RSS / Command),命令中的竖线会转义以保证表格渲染正确(见 test_render_markdown_escapes_process_command_pipes)。

候选运行时矩阵:对比 AIO 之外的后端时该记录什么

文档列出的候选包括 AIO、CubeSandbox、OpenSandbox、gVisor、Kata 或任何其他候选后端。对比必须使用同一工作负载,并记录以下证据——这张表是选型评审的最小证据集,完整继承自原文档:

维度 必须提供的证据
Capacity(容量) Pod 或实例数、总内存、平均内存、最大内存
Startup(启动) 1、10、50、100 并发沙箱下的就绪时延
Commands(命令) bash 输出、超时行为、失败形态
Files(文件) read_filewrite_file、二进制 update_filelist_dirglobgrep
Uploads(上传) 网关上传的文件在沙箱内可见
Artifacts(产物) 写入 /mnt/user-data/outputs 的文件可被后端产物 API 读取
Paths(路径) /mnt/user-data/workspace/mnt/user-data/uploads/mnt/user-data/outputs/mnt/acp-workspace 与技能路径保持预期语义
Isolation(隔离) 不同用户与线程之间不能互相读取数据
Cleanup(清理) release、空闲超时、进程重启与孤儿清理能够释放资源
Operations(运维) 部署前置条件、特权组件、网络、存储与升级路径

这张矩阵实际上把内存剖析纳入了更宽的沙箱验收标准:内存只是 Capacity 一行的一个子集。DeerFlow 仓库中已内置多个候选供应商实现可供实测,例如 AIO 沙箱E2BOpenSandboxBoxLite 等,它们在 config.example.yamlsandbox.use 配置项中按 模块:Provider 的字符串切换——例如 AIO 模式写作 use: deerflow.community.aio_sandbox:AioSandboxProvider,而 replicasidle_timeoutimagecontainer_prefix 等容量与生命周期参数在 SandboxConfig 中有完整定义(idle_timeout 默认 600 秒,replicas 为正的 Provider 容量,E2B 下会在 Gateway 间共享)。切换 Provider 做对比测试时,应保证这些配置在两侧对等。

矩阵中 Cleanup 一行"release、idle timeout、orphan cleanup 释放资源"正对应温池机制:_reap_expired_warmidle_timeout 回收过期温池条目,_evict_oldest_warm 在副本软上限被突破时驱逐最旧条目(见 warm_pool_lifecycle.py),AIO 供应商还通过 ownership 租约实现跨实例的孤儿对账,避免多副本部署时同一容器被重复释放或"释放不生效"。

面向 PR 的纪律:先有双侧数据,再谈"修复内存"

文档最后给出了针对贡献者的明确 PR 指引:

  • 不要声称新 Provider 修复了高并发内存问题,除非同一 DeerFlow 工作负载已经在当前 AIO 沙箱与候选后端两侧完成测量;
  • 对于实验性 Provider 的 PR,优先使用 Related to #3213;只有当 PR 同时附带可复现的 DeerFlow 工作负载数据、展示了目标内存降幅,并且保留上传、产物、artifacts 与隔离行为时,才适合更直接的关联方式。

结合剖析脚本的使用方式,一条完整的证据链可以概括为:用相同工作负载在 AIO 上按 7 个阶段各采一份带 --include-processes 的 JSON → 换候选 Provider、保持 replicas/idle_timeout/并发规模一致地重复同一组采样 → 用 summary 中的 total/average/max 内存与启动时延填矩阵 → 把两组原始 JSON 与对比结论一起提交。

小结

DeerFlow 的沙箱内存剖析方法可以归纳为三条要点:

  1. 基线先于结论kubectl top 的 working set 只是容量信号,必须配合容器内进程 RSS 采样和 cgroup 检查才能归因;
  2. 工具足够轻sandbox_memory_profile.py 零运行时依赖,纯标准库解析 kubectl 输出,JSON 报告结构(schema_version: 1)稳定可审计,且其行为被 backend/tests/test_sandbox_memory_profile_script.py 中十余个单测覆盖(单位解析、Pod 合并、进程容错、错误报告等);
  3. 对比必须同负载:候选运行时的验收以文档中的十维证据矩阵为准,内存数据只是其中 Capacity 维度的一部分,Related to #3213 的 PR 纪律防止无数据的性能论断进入代码库。

这套流程对任何在多租户沙箱上承载长时任务的 Agent 系统都有参考价值:先定义可重复的度量,再做运行时层面的替换决策。

登录后查看全文
热门项目推荐
相关项目推荐