ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent训练日建300万沙箱:代码沙箱架构与回收效率实战

Agent训练日建300万沙箱:代码沙箱架构与回收效率实战 做 Agent 训练的人应该都有同感训练跑不动的时候十有八九不是 GPU 不够而是没人给 agent 一个安全的执行环境。DeepSeek 一天创建 300 万个沙箱这个数字第一次看到时我不惊讶它大而是惊讶它已经能把“执行”这个环节规模化到这种程度。代码沙箱在 Agent 训练里的角色不是跑几个样本用的调试工具而是每一轮轨迹采样都依赖的流水线节点。这篇文章会把这件事完整拆开为什么 Agent 训练会爆发式地需要沙箱300 万/天这个体量到底意味着多大的容量压力隔离方案怎么选容量账怎么算以及真正把它撑住的关键并不是创建速度而是回收效率。内容偏工程落地适合正在搭 Agent 训练基建、做 RL 数据回放、或者被工具调用执行环境折磨过的工程师参考。准备自建沙箱体系的同学也能当一份避坑大纲用。1. 为什么 Agent 训练离不开“沙箱”这个角色很多从 SFT 转过来的同事最初都对“沙箱”这个概念很困惑预训练和 SFT 喂给模型的是静态文本数据从文件里读进来模型读完样本就结束了整个训练过程根本不需要“环境”。Agent 训练完全是另一套逻辑模型生成的不是最终答案而是一系列动作动作必须作用到真实环境上拿回观察结果才能继续生成下一步。最典型的就是代码类任务模型写出一段 Python如果没人替它执行stdout、报错类型、退出码全是空的那这个动作就没有任何监督信号。拿 DeepSeek 这类大规模 RL 训练来说每天跑出的轨迹里每一步工具调用都可能对应一个沙箱实例。一天 300 万个实际就是把“模型—环境—模型”这种回合制交互搬上了万台机器的规模。所以沙箱不是训练流程的附属品它就是训练数据的生产线。没有这条生产线Agent 在“调用工具”这件事上的能力根本没有办法被稳定评估。1.1 传统训练与 Agent 训练的本质区别两类训练对基础设施的要求差异极大先看一张对比对比维度传统预训练 / SFTAgent 训练数据来源静态语料提前准备好环境交互产生的轨迹边跑边生成训练单位输入-输出样本对回合Episode含多步动作失败样本直接进负例需要区分是策略错还是环境错主要瓶颈GPU 算力、数据吞吐沙箱创建/回收、工具执行延迟安全要求低数据可控高模型可能写出任意代码传统训练的样本是“已经发生的事实”训练过程只是让模型拟合它Agent 训练的样本则是“现场发生的事实”环境给什么反馈样本就是什么。这意味着环境本身必须是真实、可复现、并且能抵抗各种意外行为的。一个最简单的原因是模型生成的代码里除了正常逻辑还可能出现死循环、fork、写满磁盘、对外发流量这类行为。如果让每个动作都直接在训练机裸跑一次事故就能污染整个集群。沙箱要做的事情就是把“环境反馈”做成一等公民同时把“环境风险”隔离在训练主流程之外。1.2 训练 harness、Agent 框架和沙箱的关系很多刚入坑的同事会把 Agent 框架和训练 harness 搞混。Agent 框架解决的是“模型会不会调工具”它负责把 LLM 推理结果转成结构化工具调用、管理多轮消息训练 harness 解决的是“这种调用能力怎么被数据固化下来”它负责采样、执行、奖励计算、样本入库的闭环。两者都会用到沙箱但定位完全不同Agent 框架里的沙箱服务于在线产品训练 harness 里的沙箱服务于离线数据生产。在 DeepSeek 这类大规模训练体系里harness 是调度大脑沙箱是执行末端。一次典型交互长这样策略模型产出一个动作harness 把它解析成 exec 请求沙箱执行收集 stdout、stderr、exit code、是否超时再交给 verifier 计算奖励最后样本进入训练 buffer。每个动作服务完沙箱要么销毁、要么归还池子。这个流程跑起来一天百万级创建的流量特征就出现了。2. 先把容量账算清楚300 万/天的背后是什么300 万/天听起来很唬人但工程上第一件事不是惊呼而是把数字拆开算。这个体量下的设计决策几乎全部由一道乘法题决定。2.1 创建速率、稳态并发与生命周期300 万除以 86400 秒约等于每秒 34.7 个创建速率高峰按五倍算也就是每秒不到 200 个。单看这个数字任何容器编排系统都能做到真正难的是稳态并发。假设每个沙箱平均存活 60 秒稳态并发等于每秒创建数乘以平均存活时间也就是 34.7 × 60 ≈ 2083但实际训练任务里一个沙箱往往要服务同一 episode 里的多次环境交互一次 exec 可能带着多次小动作实测生命周期经常拉到 3 到 10 分钟。按 10 分钟算稳态并发就是 34.7 × 600 ≈ 2 万个。从容量规划看资源池规模等于稳态并发乘以单沙箱平均资源。按每沙箱 0.5 vCPU、512MB 内存、平均生命周期 120 秒估算稳态并发 7000 左右就需要 3500 vCPU、3.5TB 内存此外还要留磁盘给镜像缓存、网络带宽给 stdout 回传和日志、以及触发回报所需的队列容量。结论很反直觉想撑住 300 万最重要的不是压榨创建速度而是压平均生命周期和回收速率。生命周期少一半容量需求直接少一半这笔账最划算。2.2 隔离粒度怎么选不是所有沙箱都要拉满沙箱隔离强度从低到高大致是这几档进程级隔离cgroup、unshare、runC 容器、gVisor 用户态内核、Firecracker/Kata 这类微虚拟机、以及全虚拟化。方案隔离强度启动成本单机密度兼容性适用定位进程/cgroup低毫秒级最高高静态安全检查runC 容器中50-200ms高高常规 tool call 验证gVisor中高100-300ms中中性能损耗 20%-30%半信任代码Firecracker/Kata高125ms 起步低全系统兼容任意代码、高危任务选型逻辑要把 Agent 训练里的沙箱分成两类。一类是普通 tool call 验证模型让执行什么就执行什么恶意概率不高用加固过的 runC 加 seccomp 加资源限制就够了另一类是执行任意代码、甚至需要完整系统环境的高危任务必须用微虚拟机。我见过一些团队一上来就全量上微VM结果 200 台节点只能跑 2000 个并发镜像这边还没跑满资源先被虚拟化开销吃掉了反过来全用容器一晚上就被恶意 agent 拖垮宿主机。正确的做法是信任分级不同等级走不同资源池。3. 撑住一天 300 万的落地架构体量模型算清楚了剩下的就是工程架构。这里没有银弹核心就三件事别让镜像重复拉、别让沙箱赖着不走、别让短生命周期对象压垮控制面。3.1 预热池、镜像层级与 P2P 分发如果每个沙箱都从镜像完整拉起平均基础镜像哪怕只有 1GB300 万次创建就是 3PB 的重复 IO任何一个存储都扛不住。实践里靠三层解决。第一是节点本地预热把常用基础镜像提前发到所有节点第二是 overlayfs 只读共享所有沙箱共享底层镜像层每个沙箱只增加薄薄一层可写层第三是 P2P 镜像分发扛住峰值拉取避免几千个节点同时从一个 registry 拖镜像。另一个容易被忽略的优化是预热池。沙箱不销毁而是洗干净复用池子里常驻 500 到 1000 个空沙箱新请求来了直接分配创建时间从 100 毫秒降到 5 毫秒以下。但注意池子不等于永远保持同一套环境。训练代码迭代很频繁池子里的镜像版本会过期必须按版本分池、定时刷新不然线上会出现“这次训练的镜像跟上个任务混在一起”的脏状态。我见过一次奇怪的偶发失败查了一下午最后发现是预热池里混着两个版本的依赖包。3.2 生命周期管理真正的胜负手一天创建 300 万意味着一天也要回收 300 万。回收一慢僵尸沙箱占着资源后续任务排队训练吞吐肉眼可见地下降。我经历过最严重的事故回收线程卡在不可中断的 IO 上残留沙箱把宿主机磁盘写满整个集群半天产不出数据。从此之后生命周期协议被我放在了比创建速度更优先的位置。手工管理几百个并发可能还行百万级必须有明确的租约模型。沙箱创建时带 TTL训练端每次 exec 后心跳续租超时直接回收不商量。核心逻辑可以用一段伪代码说明class SandboxManager: def __init__(self, lease_ttl60): self.leases {} self.pool SandboxPool() async def acquire(self, task_id, image_version): sb self.pool.get(image_version) or await self.create(image_version) self.leases[sb.id] Lease(ownertask_id, expires_atnow() lease_ttl) return sb async def heartbeat(self, sandbox_id): lease self.leases.get(sandbox_id) if lease: lease.expires_at now() lease_ttl async def reap_loop(self): while True: for sb_id, lease in list(self.leases.items()): if lease.expires_at now(): await self.force_recycle(lease.owner, sb_id) await asyncio.sleep(5)这段代码里没有复杂的算法关键是强约束。回收动作必须按顺序执行摘掉网络、杀掉进程树、卸载挂载、清理 overlay 写层、归还节点资源。每一步都要幂等任何一步失败都要重试并上报绝不能静默吞掉。很多团队把回收做成一个后台低优任务结果资源泄漏积累一两天集群莫名变慢定位代价远大于提前写好的重试逻辑。3.3 节点 Agent 模式绕开 K8s API 上限如果 300 万个沙箱都是标准 K8s Pod那 API Server 和 etcd 一定会先扛不住每秒几百个 Pod 创建删除事件流和状态更新会把控制面拖垮。实际落地时大部分团队并不是用原生 K8s 承接短生命周期容器而是“K8s 管长期资源、节点级 Agent 管短命沙箱”的混合模式。中心控制器只下放策略集群有多少节点、每个节点跑什么镜像版本、全局配额是多少。节点上驻留的 agent 负责从本地池拿沙箱、配置网络、监听租约、执行回收。这样短命对象的生命周期被收敛在节点内部控制面只需要关心资源水位和异常审计。这个设计的收益是两个数量级的Pod 的 Create/Update 不再频繁打到 etcd沙箱的创建速率由节点数水平扩展不再被中心化 API 的吞吐卡死。4. 训练侧怎么把这个能力用好沙箱平台建得再好最终还是要被训练流程调用。这一层最容易出的问题不是沙箱本身而是接口协议设计不合理让 harness 和沙箱之间出现各种隐式约定。4.1 harness 与沙箱的接口协议设计训练侧其实不关心沙箱底层是 runC 还是 gVisor它只关心一件事把一个动作丢进去能不能快速拿到结构化结果。所以接口协议必须稳定。给一个典型的 exec 请求示例{ sandbox_id: sb_8f3a..., action: exec, command: [python, main.py], workdir: /workspace, env: {HARNESS_RUN_ID: run_001}, timeout_ms: 30000, max_stdout_bytes: 65536, network_policy: allow_dns_only }响应至少包含 exit_code、stdout、stderr、timed_out、oom_killed 这几个字段。工具调用有个硬约束需要即时结果。Agent 在推理过程中调起一个工具后面所有 token 生成都依赖这个结果所以 exec 请求的延迟直接进入训练关键路径。任何异步化包装都会让整个采样步骤变慢这也是沙箱要紧贴训练进程部署的原因。这里特别容易被忽略的是 max_stdout_bytes。Agent 可能写出打印一百万个日志行的脚本不限制会直接把训练进程内存打爆限制后还要把“结果被截断”这个状态明确返回不然 verifier 会拿残缺 stdout 去算奖励得出错误结论。我自己就在 eval 阶段被坑过一次某次任务得分突然偏离排查到最后是 eval 脚本打印了一个几 MB 的 JSON截断后奖励模型把关键字段读丢了。4.2 资源配额与安全边界配置给一份可以直接抄的 LimitRangeapiVersion: v1 kind: LimitRange metadata: name: sandbox-limit spec: limits: - type: Pod max: cpu: 1 memory: 1Gi default: cpu: 500m memory: 512Mi资源层只是开胃菜更重要的是进程能力和权限裁剪。推荐的默认配置包括Drop CAP_SYS_ADMIN、禁 mount/reboot 等危险 syscall、pids 限制 256、按需加 seccomp profile。不要指望模型只会写“好代码”prompt 里被人塞进“先 sleep 一小时再返回”这种逻辑完全可能发生没有 cgroup 兜底一个沙箱就能拖垮整台训练机。安全设计的核心不是把环境封死而是让它跑不死、跑不远编译器、网络、文件系统都要给判断恶意行为交给奖励模型在样本层处理工程层只负责防止它影响邻居和执行边界。5. 常见故障与排障实录百万级系统跑久了排障经验比架构设计更值钱。这里的故障往往不是“创建不出来”而是“资源悄悄消失”或者“延迟悄悄变高”。5.1 三个真实事故复盘镜像拉取风暴。某次凌晨全量新池冷启动几千个节点同时拉同一个镜像磁盘 IO 直接打满节点陆续变成 NotReady。根因是预热任务没覆盖新镜像版本生产池撞上了冷启动窗口。解法是三层镜像预热做成定时流水线、P2P 分发扛峰值、并且保证生产环境的池子在任何情况下都不从零开始。exec 超时但进程还在跑。框架把超时错误返回给模型看起来沙箱已经“完成”实际上进程仍然占着 CPU 继续执行。这个问题非常隐蔽因为从外面看沙箱的租约仍在续期。后来规定exec 超时返回的同时必须做 cgroup freeze 加 kill并且把该沙箱踢进强制回收队列任何超时状态都不允许继续服务下一个工具调用。DNS 抖动导致 exec 变慢。模型生成的网络代码频繁解析外部域名公共 DNS 一旦变慢exec 平均耗时从 200 毫秒涨到 3 秒训练吞吐直接掉一个量级。解决方式是 node-local DNS 缓存加可信 hosts 预注入同时把沙箱里 /etc/resolv.conf 统一指向本地缓存。5.2 问题速查表现象可能原因处置方式节点磁盘缓慢打满残留 overlay 写层、日志未截断强制回收流程补清理步骤限制 stdout 大小沙箱创建越来越慢本地池镜像版本过期触发冷启动拉取池子按版本分池预热流水线覆盖所有活跃版本exec 偶发超时公共 DNS 抖动、跨节点调度node-local DNS 缓存exec 请求绑定到本节点池训练吞吐周期性掉底回收线程被不可中断 IO 卡住回收线程与数据面分离失败重试带上告警节点 CPU 异常飙高沙箱内进程未随租约终止超时必须 cgroup freeze kill并进入强制回收偶发脏环境导致样本错乱预热池复用未彻底重置每次归还做完整性校验异常镜像直接销毁重建这张表不是从文档里抄来的是我在实际运维中一条条填进去的。很多问题单独看影响不大但 300 万/天的频率下任何小概率事件乘以百万都是高概率事件所以治理思路永远是让异常在毫秒级被识别、在秒级被强制回收而不是靠人工巡检去补救。我自己搭过两套沙箱平台最大的教训是第一天就要把回收、租约和资源记账做成一等公民。300 万/天听着吓人摊开就是每秒 35 个它真正杀死你的从来不是创建 API 扛不住而是回收不干净、镜像风暴、DNS 抖动这些“低频高损”问题。给正在搭 Agent 训练底座的团队一句建议先让沙箱活不过 60 秒再谈优化创建速度。生命周期管住了容量账自然就平了。
返回列表