ARTICLE DETAIL

资讯详情

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

大规模Agent训练沙箱体系:调度、镜像与状态恢复实践

大规模Agent训练沙箱体系:调度、镜像与状态恢复实践 第一次在后台看到那条告警时我第一反应是“又有人把公共环境搞坏了”。巡检日志显示某个 Agent 训练 worker 里跑起来的 Python 进程正在反复尝试读取宿主机的系统敏感文件还想把文件系统挂到自己新建的临时目录下。单看这一条记录它和一次真实的入侵行为几乎没有区别。但这是模型自己生成的代码在沙箱里执行——而这种执行恰恰是大规模 Agent 训练里每天都在发生的、最普通的操作。我当时所在的团队一直在支撑大模型 Agent 相关训练任务内部代号 DSec 的这套沙箱调度与安全模块就是为了应对这类问题逐步搭起来的。它的核心不是做一个“更严格的容器”而是把隔离、调度、镜像供给、状态恢复放在同一个体系里统一解决。如果你正在做 Agent 训练需要让模型为了工具调用而执行真实代码或者你只是想理解为什么 Agent 训练不能直接放在普通容器环境里跑这篇文章里关于沙箱调度、镜像加载和状态恢复的经验应该能帮你少走不少弯路。1. 为什么 Agent 训练需要多出这一层“独立沙箱”1.1 Agent 任务的核心特点是动态且不可信传统的模型推理任务我们可以把它看成一条静态流水线输入进来模型算一遍输出出去中间没有任何外部动作。但 Agent 训练不是这样。模型为了完成一个目标会自己规划步骤生成要执行的代码调用工具读取文件甚至安装新的软件包。训练过程中我们还会让模型与环境反复交互通过 rollout 采样出一条条轨迹再用轨迹去更新模型参数。这里有个很反直觉的事实模型生成的东西不能因为它“是我们训练出来的”就默认可信。模型可能因为指令错误、上下文误导甚至单纯能力不足写出完全超出预期的命令。它可能去读不该读的文件可能循环拉取外部接口可能把自己跑进死循环。在训练阶段这些行为不是 bug而是需要被采集和分析的样本。所以我们不能“禁止”它乱来只能让它在乱来的时候也破坏不了任何东西。这就像一个公司里你不能因为实习生是自家招来的就把他单独放在放满公章和账本的房间里办公。你需要给他一个独立工位给一套标准软件环境让他只能碰到自己该碰的东西。DSec 沙箱干的就是这件事模型生成的代码可以在里面自由执行但它永远碰不到宿主机和其他任务。1.2 普通容器隔离为什么不够用很多人会问Docker 不是已经能做隔离了吗为什么还要整套沙箱方案容器确实是很好的基础但它解决的是“常规隔离”不是“对抗隔离”。普通容器和宿主机共享一个内核Namespace、Cgroup 提供了进程、文件系统、网络层面的隔离可一旦内核出现漏洞或者容器被赋予了过多的 Capability进程就可能逃逸出来。在大规模训练集群里这个风险会被放大。一个节点上可能同时跑着几十个 Agent 沙箱同一批节点上可能还有别的训练任务、模型权重、用户代码。一旦有一个 Agent 生成的内核级破坏代码找到漏洞逃逸影响是一整片不是一个沙箱。而且训练集群经常是内网互通横向移动的成本非常低。所以 DSec 的思路是在容器之上再加一层“加固沙箱”。具体展开说我们做了几件事启用用户命名空间把沙箱内 root 映射成宿主机上的普通用户用 seccomp 做系统调用白名单Agent 代码里大部分高风险的调用直接拒绝Capability 按白名单给比如 net_raw 默认不给挂载操作默认不给根文件系统只读只有 /workspace 和 /tmp 可写/tmp 用 tmpfs 挂载避免脏数据落到磁盘上。这一层做下来即使模型真的生成了恶意代码它面对的也是一个几乎没有可利用面的环境。我们不是要防住一个专业攻击者而是要防住“模型随机犯傻”的小概率事件这两者的目标完全不同但手段可以共用。1.3 调度、镜像、恢复为什么必须放在一起最初我们尝试过把沙箱、调度器、镜像仓库拆成三个独立系统去维护结果发现它们之间耦合太深。调度一个新沙箱时必须知道节点上有没有对应镜像缓存镜像拉取慢会拖累调度排队沙箱崩溃后要恢复恢复逻辑又得重新找节点、重新加载镜像、重新对齐状态。后来我们干脆把这三件事统一成 DSec 的三大核心能力调度器负责分配资源和排队镜像服务负责让环境快速就位状态恢复负责让跑了一半的任务能接上。它们共享同一份任务描述、同一套心跳协议、同一个元数据库。这样设计的根本原因是在超大规模训练里这三个环节任何一个出现缺口都会直接变成训练吞吐的天花板。2. DSec 沙箱调度的资源模型与排队策略2.1 资源模型Agent 任务需要哪些配额维度调度最基础的问题是资源由哪些维度构成。Agent 训练任务和普通 Web 服务不一样消耗的资源很不规律。一段模型生成的代码可能立刻把内存打满另一个任务可能趴在网络层疯狂发包第三个可能只是在磁盘上写了几个大文件。所以我们在常规的 CPU、内存、GPU 之外给每个沙箱增加了一批“行为配额”。下面的表是我们的默认配置不一定适合所有场景但可以当个参考起点资源维度单沙箱默认配额说明CPU200% 2 核用 CFS 配额控制不绑核内存4GBcgroup v2 memory.max临时磁盘8GB / 每个沙箱可写目录的 tmpfs 上限磁盘写带宽100MB/s防止日志把宿主盘打爆并发 TCP 连接128限制连接跟踪表占用出网带宽50Mb/s避免单个沙箱抢占出口带宽单任务运行时30 分钟超时自动优雅回收大多数 Agent 任务都用不到配额上限但设了上限之后一个失控样本不会拖累整个节点。配额的实现可以用 cgroup v2 加 systemd scope 来做启用很方便。我贴一段我们节点侧初始化沙箱时实际用到的核心约束模版systemd-run --scope \ --propertyCPUQuota200% \ --propertyMemoryMax4G \ --propertyTasksMax256 \ --propertyIOReadBandwidthMax/dev/nvme0n1 100M \ --propertyIOWriteBandwidthMax/dev/nvme0n1 100MGPU 资源也是同理通过显存分片或算力比例去限制。早期我们不敢给 Agent 任务配 GPU后来发现很多 Agent 会调用本地视觉模型就专门划分了一个小资源池所有沙箱按需申请用完立刻释放。2.2 全局调度器和队列从提交到运行的完整链路DSec 调度器采用中心调度加节点代理的结构。中心调度器维护全局资源池、待执行队列和运行队列每个节点上跑一个轻量节点代理负责创建沙箱、上报状态、执行回收命令。两者的职责边界要非常清楚中心只做决策不做具体操作节点只执行操作不做全局判断。一次任务从提交到运行完整的链路是这样的任务提交携带资源需求、镜像哈希、运行时长上限。调度器校验配额写入待执行队列。分配节点时优先选择“目标镜像缓存命中”的节点次选负载最低的节点。节点代理检查镜像层缓存补齐缺失层初始化网络命名空间启动沙箱。sidecar 进程被拉起开始上报心跳。调度器确认心跳正常把任务标记为运行中开始计时计费。任务正常结束或被回收节点代理清理沙箱释放配额。排队策略上我们按任务类型区分优先级训练主任务最高评测任务次之人工调试最低。同一优先级内部做严格的 FIFO不搞小聪明。为什么不用抢占因为 Agent 沙箱是有状态的强行杀掉一个正在执行工具调用的低优任务丢的不只是那几秒计算而是一整条轨迹样本。所以当高优任务需要资源时我们对低优任务发起“优雅回收”通知 sidecar 先打检查点再结束进程之后等资源空出来重新排队恢复。这样看起来稍微慢一点但整体收益更高。2.3 单机并发、端口分配和“旧数据污染”单机到底能跑多少个沙箱这个问题不能只看内存。每个沙箱都是独立的网络命名空间会占端口会建 iptables 规则会占连接跟踪表条目。我们实测下来一个 32 核、128GB 的节点维持“训练稳定不抖动”的极限大约是 64 个同时存活的沙箱。超过这个数节点负载开始剧烈波动沙箱的启动和回收都会变慢。所以在调度器里我们硬编码了每节点最大活动沙箱数留出约 10% 的资源给系统进程和日志采集。端口分配是另一个容易踩的坑。沙箱需要回连模型服务也需要接收工具回调因此必须分配动态端口。我们在每个节点上维护了一个端口池范围为 30000-60000由节点代理统一分配。沙箱启动后会把“本沙箱对外的入口地址和端口”通过环境变量注入进去这样 Agent 内部的回调用服务时不需要自己瞎猜地址。还有一个很隐蔽的问题回收沙箱后旧连接可能残留在连接跟踪表里新沙箱如果复用同一个端口会收到上一次会话的残留数据。我们处理的办法很简单粗暴——每次回收强制销毁整个网络命名空间重新建立回环和路由表。这样旧沙箱的任何状态都带不过来彻底断绝了数据串扰的可能性。3. 镜像加载把最耗时的环节变成最快的一环3.1 镜像加载为什么能成为吞吐瓶颈训练平台上线初期的数据很难看。我们统计过一个训练任务从提交到真正开始执行的总耗时结果发现镜像拉取占了四成左右。为什么这么慢一个 Agent 训练镜像不只是 Python 运行时它还包含模型依赖的 torch 全家桶、一整套工具二进制、测试数据集快照、自定义的国产包源配置加起来轻松上 GB。如果只是单个任务慢也就算了麻烦的是所有任务几乎都是“一波一波”提交的。每次训练版本迭代几百个节点同时开始拉新镜像镜像仓库的网卡瞬间被打满随后 registry 进程内存飙升最后连锁导致一批任务因为拉取超时直接失败。那段时间我们挂在嘴边的口头禅就是训练是卡在镜像里不是卡在算力里。3.2 分层、内容寻址和节点级缓存解决这个问题的核心思路是借鉴镜像领域的成熟经验但做得更彻底。我们把镜像拆成三层基础层、应用层、运行层。基础层放 Python 运行时、系统库、底层依赖这一层基本不变。应用层放训练代码和固定依赖每次代码迭代时重新构建但基础层不失效。运行层放每个任务特定的工具和数据集变化最频繁体积却最小。这样的结构保证了 90% 的节点在绝大多数情况下只需要拉取很小的增量层而不是全量重新下载。镜像仓库是基于 OCI Registry 标准自建的但我们在存储上用了严格的内容寻址每一层用 sha256 哈希命名镜像 Tag 只是一个 manifestmanifest 里记录各层哈希和大小。这样能避免一个经典问题两个同学先后推了同名 Tag导致某部分节点拉到的镜像漂移。层不可变之后拉取、缓存、校验全部以哈希为基准链路清晰。节点级缓存是提速的关键。我们在每台节点上保留一个镜像层缓存目录只有缓存中不存在的层才会回源 registry 拉取。拉取到新的层后先校验哈希再链接到本地存储目录。调度器做节点选择时也会优先选目标镜像缓存命中的节点进一步跳过拉取环节。实测数据可以给你们一个体感全新冷节点拉 1.8GB 的全量镜像网络正常情况下要 40 秒左右如果目标节点已有基础层和应用层缓存只需要拉运行层那几十 MB沙箱从开始创建到 ready 的 P95 时间大约 1.6 秒。这个差距在千级并发场景下就是“跑不动”和“随便跑”的区别。3.3 压缩层、共享层和启动加速镜像存储我们都用 zstd 压缩压缩比和和解压速度都不错。节点缓存里直接存压缩格式创建沙箱时边解压边写入 overlay 层。后来的版本还尝试过基础层共享——基础层在节点上只保留一份只读数据所有沙箱通过只读绑定叠加引用不再为每个沙箱单独复制一份全量数据。效果非常直观一百个沙箱在磁盘占用上几乎等于一个基础层加各自的小写层省了大量存储空间也减少了垃圾回收的压力。这里有一个操作细节供参考当节点磁盘空间受限时千万不要急着清缓存。我们用的是引用计数加 LRU 的双层策略——先看某个层有没有被存活沙箱引用如果还引用着即使长期没被使用也不能删确认无引用后再按最近访问时间从旧到新回收。我曾见过同事图省事直接docker rmi -f结果一批沙箱的基础层文件瞬间被清掉后续任务全部重建整体反而更慢。3.4 镜像安全与离线回退考虑到 Agent 训练沙箱代码的不可信特性镜像本身也需要做安全收敛。我们每次构建镜像后都会做漏洞扫描高危漏洞组件会直接被阻断部署。同时在镜像构建时不装任何非必需的高风险工具减小攻击面。镜像仓库内部还有一道隔离模型生成的代码运行在沙箱里沙箱内无法访问镜像仓库的管理接口。离线回退是整个镜像链路最后的保险。因为训练集群通常在内网一旦 registry 服务挂了所有依赖镜像的新任务都会卡死。我们为此保留了一个离线兜底镜像平时不更新只存在各节点本地。registry 出问题时调度器自动切到兜底镜像保证存量任务可以继续跑已经跑起来的任务不受影响。虽然功能受限但训练中断的损失远比功能受限大。4. 状态恢复从一次节点宕机说起4.1 Agent 训练过程中到底要恢复哪些状态做状态恢复之前得先想明白“状态”到底指什么。Agent 训练任务不是无状态计算它会产生四类状态。环境状态是文件系统层面的包括沙箱里安装的依赖包、下载的数据、改动过的配置文件。执行状态是进程层面的包括当前执行到第几步、子进程 PID、正在运行的脚本路径。会话状态是逻辑层面的包括与模型服务的完整消息序列、已执行完的工具调用及其结果、模型规划出的下一步动作。外部资源状态是容易被忽略的比如临时令牌、文件锁、外部租约、回调地址。恢复的本质是在新沙箱里重建这四类状态并且做到与崩溃前“对外可观测一致”。注意我们追求的从来不是内部字节级一致而是模型和外部工具感知到的结果一致。只要会话里看到的消息顺序、文件内容、工具返回值是一致的训练就能正常继续。这个认知极其重要否则你会钻进“全内存快照”的死胡同成本和收益完全不成正比。4.2 心跳、快照和回放机制每个沙箱里我们都会放一个 sidecar 进程它做四件事采集执行状态、维护心跳、接收调度器指令、生成检查点。心跳是秒级的每次携带当前步骤、CPU 内存占用、子进程列表、资源使用量。某个中控侧超过 10 秒没收到心跳就会启动异常判断流程。检查点不要做太频繁否则对磁盘和执行的干扰很大。我们的节奏是文件系统的增量快照每 5 分钟打一次同时在“步骤切换点”——比如一次工具调用结束、模型准备生成下一步的时候——额外打一次。步骤切换点往往是相对安静的时刻对正在执行的任务影响最小。当恢复发生时新沙箱先拉取最近一次快照恢复文件系统然后重放 sidecar 记录的增量日志补齐文件变更最后重放会话消息序列已经完成的工具调用直接返回原结果。写入侧我们用本地 NVMe 加远端异步复制本地保证低延迟远端保证节点级故障时可恢复。4.3 一次节点宕机后的完整恢复时间线拿一次真实事故来说一千个 Agent 并发训练中一台节点因为硬件问题突然宕机上面有 86 个活跃沙箱。恢复的完整时间线是这样的T0 秒节点心跳中断调度器将该节点标记为疑似失联。T5 秒另一台监控节点也确认该节点不可达双节点同时判定后正式触发恢复流程。T10 秒调度器从恢复队列取出任务优先调度到镜像缓存命中的新节点。T15 秒新沙箱创建完成sidecar 从远端存储拉取最近快照。T20 秒文件系统恢复完毕开始重放执行状态和消息序列。T30 秒Agent 重新跑起来从崩溃前那一步继续执行。最终结果86 个沙箱里有 81 个成功恢复并继续完成轨迹剩下的 5 个是因为崩溃点正好落在非幂等工具调用之后无法判断外部副作用是否已经发生我们选择标记失败并重跑该轨迹。整体恢复时长控制在 30 秒内几乎没有影响训练主流程。这里面最关键的工程技巧是幂等。所有工具调用都带着 request id重放时如果发现同一个 request id 已经存在直接返回旧结果不重新执行。比如“搜索”这类只读操作重放无所谓但“发一条消息”“下单”“扣费”这类操作如果重放两次后果可能很严重。在 Agent 训练平台里外部工具的幂等网关不是加分项是必需品。4.4 什么情况不建议硬恢复状态恢复不是万能的。我们后来总结出三类“不硬恢复”的情形。第一类是环境损坏的崩溃。Agent 代码把关键文件改成不可恢复的形态或者把 Python 运行时搞坏了这时候硬恢复只会让新沙箱继续摔在同一处。我们的规则是同一个步骤连续恢复失败两次直接放弃该轨迹回到最近的安全检查点重新采样。第二类是明确的内存超限。沙箱被 OOM 杀掉时正在运行的进程堆栈数据全部丢失硬恢复很可能再次触发 OOM。我们做的是给目标步骤提高配额后重跑一次而不是直接恢复现场。如果提高配额还是 OOM就把这个样本标记为“异常执行样本”它本身反而是训练数据质量分析的重要素材。第三类是恢复风暴。如果网络抖动导致大量活着的节点被误判为宕机恢复任务会同时启动反而把集群压垮。所以我们引入了双节点仲裁一个节点失联时必须有另一个独立观察者确认才认为节点真的宕机。另外恢复任务要有并发上限避免瞬间大量重排重放。5. 踩过坑之后沉淀下来的几条铁律5.1 网络限制必须在沙箱层做不能指望代码检查我们踩过最狠的一脚是一个 Agent 生成了类似“端口扫描”的代码在五分钟内向网段内其他节点发了几十万个小包。那个节点上的出口带宽瞬间打满同节点的正常训练任务全部卡顿模型服务侧的请求超时率飙升。排查时最容易看到的指标是连接跟踪表溢出。但我们没有去埋怨模型而是反思平台为什么没有拦住。后来我们把网络限制彻底下沉到沙箱层每个沙箱创建时强制设置出网带宽上限和连接数上限。代码检查那种事后逻辑根本追不上 Agent 的随机发挥只有在执行层做硬限制才是可靠的。现在所有沙箱默认出网带宽 50Mb/s如果任务需要更大带宽显示申请并写明理由。5.2 镜像仓库被自己人打挂不是危言耸听有一次训练版本热更几位同学的代码改动把基础层也带上了新哈希相当于把全量缓存全部击穿。几百个节点几乎同时发现缓存不命中同时回源 registry 拉层仓库进程内存飙升最终整站不可用。那次事故后我们立了一个规矩基础层的变更必须过发布审批普通训练迭代只能动应用层和运行层所有镜像构建必须带不可变哈希 Tag同一个 Tag 不允许覆盖。另外registry 前端加了基于令牌桶的并发控制每个层同时最多允许 N 个节点拉取超出部分在节点侧排队。这是故意制造“受控的慢”而不是让仓库直接崩溃。事故是不优雅的排队只是慢但至少任务不会全挂。5.3 恢复流程本身可能成为新的事故源状态恢复是为了减少事故但设计不好它自己就是事故。我们在早期也遇到过一个网络分区导致三十多台节点同时被判离线恢复机制随即创建了几乎同样数量的替代任务这些替代任务同时去访问模型服务直接把模型服务的队列打满最后变成了全集群范围的连锁故障。从那以后我们坚持两条原则。第一判定节点离线必须有第二观察者单点判断永远不可信。第二恢复任务必须限流不能一把梭同时拉起所有替代沙箱。宁可让恢复慢一点也不能让恢复本身击溃依赖的下游服务。这条经验适用于所有做调度系统的人请务必刻在脑子里。5.4 日常运维检查清单最后晒一份我们内部要求的日常运维清单算是从一次次故障里压出来的最低标准镜像层不允许覆盖任何更新只允许新 Tag 上线。每个沙箱必须有运行时上限不允许存在“永久任务”。所有外部工具调用必须走网关网关负责幂等和重试上限。对所有运行中的沙箱做随机抽样审查记录输入输出序列用于复盘。任务提交时必须声明资源需求和镜像哈希缺一项直接拒绝排队。每季度做一次恢复演练故意杀一台节点验证整套流程不是纸上谈兵。清单看起来很基础但大部分平台的问题是“基本都没做到”。把这些做成硬性校验而不是自觉稳定性会提高一个量级。说回最初那条告警。后来我们查清了那个试图读取系统敏感文件的 Agent是因为上下文里的错误指令导致模型生成了探测本地配置的代码。它被沙箱里的权限规则拦住了没有造成任何实际影响反而成为训练数据里一条值得分析的异常轨迹。我觉得这就是 DSec 这类系统最大的价值它不是阻碍 Agent 探索而是给探索划定了一个不会伤到自己的安全边界。调度、镜像、恢复这三件事本质上都是为了让这种探索更稳、更快、更持久。如果你也在做 Agent 训练平台我最后想给一个建议先想清楚你的沙箱是给“可信的自己人”用的还是给“不可信的模型生成代码”用的。如果偏向后者的场景请务必把隔离边界、镜像不可变、恢复幂等这三件事当成一等公民来设计它们不值得你后续拿一个个事故来重新认识。
返回列表