DeerFlow 沙箱内存剖析:在 Kubernetes 上建立沙箱内存基线与候选运行时对比方法
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 类样本,覆盖沙箱从空闲到并发复用的完整生命周期:
- 沙箱就绪后的空载状态(empty);
- 执行一条简单 bash 命令之后;
- 运行导入常用包的 Python 任务之后;
- 预期存在 Node 负载时,运行 Node 任务之后;
- 在
/mnt/user-data/outputs下生成文件之后; - 释放(release)之后被温池复用(warm reuse)的状态;
- 目标并发规模下——例如 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,它只是写入报告的人类可读标签(如 empty、after-bash、after-python、after-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_continues 与 test_collect_process_samples_records_timeout_and_continues 两个用例验证——模拟 exec 被拒绝和超时两种场景,断言其余 Pod 的采样照常完成。
解析细节与报告结构
脚本对 kubectl 输出的解析同样有测试背书:
- 内存单位解析支持
Ki/Mi/Gi/Ti(1024 进制)与K/M/G/T(1000 进制),如512Ki、0.1Gi、100M,无法解析的值返回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 会体现在
summary的parsed_memory_count/unparsed_memory_count计数中,便于发现采集质量问题(test_build_report_counts_unparsed_memory_values)。
JSON 报告(schema_version: 1)的核心结构:
captured_at/namespace/selector/sample:采集元数据;summary:pod_count、total_memory_mib、average_memory_mib、max_memory_mib、total_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_file、write_file、二进制 update_file、list_dir、glob、grep |
| 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 沙箱、E2B、OpenSandbox、BoxLite 等,它们在 config.example.yaml 的 sandbox.use 配置项中按 模块:Provider 的字符串切换——例如 AIO 模式写作 use: deerflow.community.aio_sandbox:AioSandboxProvider,而 replicas、idle_timeout、image、container_prefix 等容量与生命周期参数在 SandboxConfig 中有完整定义(idle_timeout 默认 600 秒,replicas 为正的 Provider 容量,E2B 下会在 Gateway 间共享)。切换 Provider 做对比测试时,应保证这些配置在两侧对等。
矩阵中 Cleanup 一行"release、idle timeout、orphan cleanup 释放资源"正对应温池机制:_reap_expired_warm 按 idle_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 的沙箱内存剖析方法可以归纳为三条要点:
- 基线先于结论:
kubectl top的 working set 只是容量信号,必须配合容器内进程 RSS 采样和 cgroup 检查才能归因; - 工具足够轻:sandbox_memory_profile.py 零运行时依赖,纯标准库解析 kubectl 输出,JSON 报告结构(
schema_version: 1)稳定可审计,且其行为被 backend/tests/test_sandbox_memory_profile_script.py 中十余个单测覆盖(单位解析、Pod 合并、进程容错、错误报告等); - 对比必须同负载:候选运行时的验收以文档中的十维证据矩阵为准,内存数据只是其中 Capacity 维度的一部分,
Related to #3213的 PR 纪律防止无数据的性能论断进入代码库。
这套流程对任何在多租户沙箱上承载长时任务的 Agent 系统都有参考价值:先定义可重复的度量,再做运行时层面的替换决策。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00