最近半年我一直在做一件事:用 DeepSeek 系列模型作为底座,跑大规模的智能体(Agent)训练任务。模型本身没问题,哪怕是本地用 vLLM 部署起来,推理吞吐也够用,真正把我逼疯的是训练过程中那几千个沙箱实例的管理问题。
一开始我们用的方案很朴素——一个容器一个任务,跑完就扔,资源池靠人工分配。但智能体训练和传统模型训练完全是两回事,它会有大量长尾的探索行为,动作空间大、交互频率高、失败路径多,每次打分、每次工具调用都可能需要独立的隔离环境。我经常早上醒来看到一屏的任务失败告警,不是模型不好,而是沙箱先崩了:磁盘写满、DNS 解析异常、环境变量污染、同时加载的模型上下文把内存打爆。后来我意识到,这不是某一个调度器能解决的问题,而是整个训练基础设施的设计思路有问题。
这也是我写下这套方案的原因。它叫 DSec(DeepSeek Elastic Computing),是一套面向大规模智能体训练设计的沙箱基础设施,核心解决三件事:第一,让训练任务有安全、独立的执行环境,互不干扰;第二,让资源可以真正弹性伸缩,而不是靠提前规划;第三,让轨迹数据、打分结果、异常现场可以完整回收,支持后续分析和回放。下文会从设计思路、架构分层、调度策略、数据闭环到真实踩坑,把整套方案讲透。
1. 为什么通用容器方案撑不住智能体训练
很多人以为智能体训练和微调差不多,都是“喂数据、算梯度、存权重”。实际上智能体训练的任务形态完全不同,它的核心是环境交互——智能体要感知环境状态、做决策、执行动作,再拿到反馈。这个过程对底层基础设施提出了几个传统模型训练完全不会遇到的要求。
1.1 交互密集导致的环境数量膨胀
模型训练是典型的批量计算,数据准备好了扔给 GPU,算出梯度更新权重,任务目标单一,环境基本固定。但智能体训练不一样,尤其在做强化学习和多智能体模拟时,每个智能体要有自己的独立环境,而且环境经常是动态生成的。
举个例子,一个写代码的智能体,每轮训练可能要生成几十个候选方案,每个方案都要在一个全新环境里编译运行;一个做网页操作的智能体,每个动作序列可能都要在隔离的浏览器环境里执行。这种需求叠加起来,训练过程中需要同时存在的环境实例数是千级甚至万级的。
我实测过一组数据:单机单卡跑 DeepSeek 模型的推理任务,算上并发请求,GPU 利用率能到 80% 以上,但如果我们只给每个智能体任务分配一个普通 Docker 容器,CPU 空闲率长期在 60% 以上。不是 CPU 资源不够,而是环境创建、销毁、调度分割了太多算力。原因也很简单,普通容器方案在设计时根本没考虑“创建和销毁频率极高”这种场景,每次docker run都是一整套镜像拉起、网络配置、存储挂载流程,这些开销在批量任务面前会被放大到一个很难看的数字。
1.2 长尾失败和不可控行为需要强隔离
训练智能体最讨厌的就是任务行为不可控。模型在探索阶段可能产生死循环,可能疯狂写盘,可能调外部 API 把自己卡住,可能是环境的变量被改乱导致后续任务全部错乱。如果这些环境共享了宿主机的资源,一次失控就可能影响全局。
我在跑一个多智能体协作任务时遇到过这个问题。某个场景里智能体要操作文件系统,有个策略让它在/tmp目录下疯狂创建临时文件,几百个沙箱同时这么干,直接把宿主机磁盘写满了,连控制面的数据库都因为磁盘不可写而报错。那次事故让我明白了一个道理:智能体训练的环境隔离,目标不是防止黑客攻击,而是防止训练过程中的意外行为互相污染。这个场景下,隔离强度要按处理恶意代码的标准来设计,哪怕是模型自己生成的代码,也要当作不受信任的代码来对待。
1.3 传统虚拟化方案的成本瓶颈
既然容器隔离不彻底,那直接用虚拟机呢?虚拟机隔离确实干净,但性能损耗和启动速度都是问题。一个常规 VM 启动要几十秒,而智能体训练环境中,新任务的平均等待时间最好控制在 5 秒以内,否则整个训练迭代周期会被拉得很长。传统虚拟化方案在这个场景下行不通,我们需要的是介于容器和 VM 之间的一种执行单元,具备轻量、快速、隔离性好的特点。
DSec 在这个问题上做了个折中:进程级隔离为主,通过 cgroup、namespace、Seccomp 组合实现资源隔离和权限隔离,特殊高危场景再走轻量级微虚拟机(Firecracker 这类)。这两层策略结合之后,绝大多数场景的隔离性和性能都兼顾了。
2. DSec 的整体架构:控制面与数据面分离
设计这套基础设施时,我参考了服务网格和 Kubernetes 的架构思想,把系统分成控制面和数据面两部分。控制面负责调度、配额、生命周期管理,数据面负责真正的执行、数据采集和缓存。控制面和数据面完全分离的好处是:即便某个数据面节点彻底挂掉,控制面依然可以重新调度任务,不会出现单点失控。
2.1 控制面模块划分
控制面的核心模块有四个:调度器、配额管理器、租户管理器和观测中心。
调度器负责给每个训练任务分配合适的执行节点。它需要综合考虑节点当前负载、镜像是否已缓存、数据依赖位置、GPU 资源余量等因素。最朴素的做法是轮询,但实际跑了一阵就会发现,必须引入“镜像亲和性”这个约束——如果某个节点已经缓存了目标镜像,冷启动成本会大幅降低,优先往这些节点调度能明显提升任务创建速度。
配额管理器处理的是资源超卖问题。训练任务有高峰有低谷,如果按峰值需求预留资源,成本完全没法看;但超卖太狠又会引发节点过载。我们的策略是两层配额:第一层是硬配额,控制每个任务最多能用的 CPU、内存和磁盘;第二层是弹性配额,允许任务在节点资源富余时临时借调资源,但借调的部分不保证可用期限,一旦节点压力上来会被优先回收。这套机制跑通以后,整个集群的资源利用率从 40% 出头提到了 70% 左右。
租户管理器解决的是多团队共用集群的问题。不同团队跑的任务可能依赖不同版本的 Python、系统库、模型文件,如果全部混杂在一批节点上,环境冲突是必然的。租户管理器会给每个团队分配独立命名空间,再把命名空间绑定到特定的镜像仓库和数据卷池。这个设计的代价是需要维护多套镜像,但换来的是环境问题排查工作量大幅下降。
观测中心负责把所有沙箱的状态汇总到可视化面板。它要回答几个核心问题:当前有多少任务在跑、有多少任务在排队、失败原因分布是什么、资源水位在什么区间。追踪指标分别是任务启动耗时、任务失败率、平均运行时长、资源实际使用量。我强烈建议在做沙箱基础设施的第一天就接好观测系统,否则后面排查问题会非常痛苦。
2.2 数据面执行节点的内部结构
数据面的执行节点是三段式结构:节点代理、执行沙箱、缓存服务。
节点代理是控制面和数据面的桥梁,负责接收调度指令、上报节点状态、拉取镜像。执行沙箱就是任务真正运行的隔离环境。缓存服务做的事比较细:把常用的模型文件、代码库依赖、数据集的切片提前放到节点本地,任务起来时不用每次从远端拉取。
节点代理还要负责做健康探活。这个不是简单的 ping 操作,而是需要检查沙箱内关键服务是否可响应、磁盘是否可写、网络连通性是否正常。智能体训练过程中经常出现一种状况:任务进程还活着,但已经死锁或者卡在外部调用上,这类任务在探活结果里应该被标记为“假死”,触发自动重启策略。
数据面还有一个容易被忽略的组件:沙箱的染色标记。每个沙箱都会被赋予一组元数据标签,描述它是哪个任务创建的、依赖哪个镜像版本、运行在哪个节点上。一旦任务失败,这些标签会跟着错误日志一起进入归因分析系统,方便快速定位是环境问题、模型问题还是任务本身逻辑问题。
2.3 镜像分层和“预热”机制
智能体训练有一个规律:同一批任务用的镜像往往是相同的,比如“Python 3.10 + 特定版本的训练框架 + 预置工具链”。如果每创建一个沙箱都重新构建一套完整环境,镜像拉取的时间会成为最大的瓶颈。
DSec 的做法是把镜像拆成三层:基础层、依赖层、业务层。基础层是操作系统和运行时,几乎不变;依赖层是训练框架和常用库,只在升级时变化;业务层是每次针对具体任务生成的脚本、配置和数据。
这三个层会在集群内做分布式缓存。任务创建时,执行节点只需要检查本地有没有这三层对应的层数据,缺失的话只拉取缺失的层。实测下来,镜像拉取耗时从原始的几十秒降到了平均 3 秒以内。另外,我们还做了一层“预热”优化:调度器在预测到某个任务即将创建前,提前把镜像层推送到目标节点。这个机制依赖一个简单的预测模型,根据历史任务启动模式预测未来的镜像需求。
3. 弹性计算引擎:调度策略与资源超卖
弹性计算的关键不是“能扩能缩”,而是对资源的精细控制和调度策略。本部分我们把调度器的设计细节拆开来讲。
3.1 调度约束与优先级划分
DSec 的调度器设计借鉴了 Kubernetes 的调度框架,但对其做了大幅精简,只保留几个核心插件:资源过滤器、拓扑约束、优先级打分。
资源过滤器在调度前先过滤掉不满足条件的节点。这些条件包括:剩余 CPU/内存是否够、磁盘空间是否满足、GPU 是否可用。拓扑约束负责把同一批任务尽量调度到同一节点或同一机架,因为智能体训练经常有任务间通信需求,拓扑聚集能让通信延迟明显下降。优先级打分则是根据任务特征对候选节点排序,镜像缓存命中的节点加分,最近失败率高的节点减分。
优先级方面,我主要区分了三类任务:在线试运行(权重最高)、正式批训练(中等)、回放分析(低权)。在线试运行是研发人员手工触发、需要即时反馈的任务,必须优先保证;正式批训练是夜间批量跑的大任务,调度窗口拉长一些也没关系;回放分析是在离线环境下看历史轨迹的任务,对实时性不敏感,可以排在最后执行。
3.2 超卖阈值与回收策略
资源超卖在智能体训练场景里比在普通微服务场景里更危险,因为训练任务往往会持续占用资源,不会主动释放。我们最开始抱着“稍微超卖一点没事”的心态,把 CPU 超卖比例设为 1.5 倍,结果在高峰期直接触发了大批任务因 CPU 饥饿而超时。
后来采取的策略是分资源类型设置不同的超卖系数,这个可以通过表格来说明:
| 资源类型 | 超卖系数 | 回收优先级 | 回收策略 |
|---|---|---|---|
| CPU | 1.2 | 低 | 限制 CPU 配额 cfs_period 让出 |
| 内存 | 1.0(不超卖) | 高 | 触发 OOM 后自动终止最外层子进程 |
| 磁盘 | 0.8(预留余量) | 高 | 触发写限速,禁止大文件创建 |
| GPU | 1.0(按卡分配) | 中 | 不做显存复用,只按整卡分配 |
| 网络带宽 | 1.5 | 低 | 优先限速,不影响任务存活 |
内存不超卖是踩坑之后定的死规矩。智能体任务的内存使用跟上下文长度强相关,一个长上下文的推理调用就可能把系统内存打掉大半,预测不准,不如不超卖。磁盘超卖系数设为 0.8 则是为了保证日志、缓存、临时文件有充分的落盘空间。
回收策略的触发条件也要设计好。当节点内存使用率超过 90%,并且有更高优先级的任务需要调度到这个节点时,才触发低优先级任务的回收。具体操作是给任务发 SIGTERM,等待 30 秒,超时后强制 SIGKILL。这个 30 秒是给任务留出机会保存检查点,不至于所有进度作废。
3.3 分批调度与任务亲和性
大规模智能体训练任务的特点是“数量多,单个任务耗时短”。一个训练回合可能要跑 5000 个子任务,每个子任务耗时 1 到 3 分钟不等。如果 5000 个任务一次性全部调度,调度器会瞬间成为瓶颈,节点排队也会过于集中。
DSec 采用的方法是把任务按照依赖关系分组,每组一个批次,每批不超过 200 个实例。批与批之间串行推进,批次内部并发执行。为了进一步控制节点压力,同一批任务还会按 5 个一组做微批次启动,每个微批次启动间隔 2 秒。这个看起来像“人为限流”的设计,实际是为了避免任务同时启动导致镜像拉取风暴和节点负载尖刺。
任务亲和性约束也很关键。当一组任务需要访问同一个数据集副本时,调度器会优先把它们调度到数据副本所在的节点;如果数据副本分布式存储,则优先调度到离副本网络跳数最近的节点。这样能把数据集读取的时间从十几秒降到一两秒,对训练总耗时的影响非常显著。
4. 沙箱技术选型:容器、微虚拟机与隔离策略的取舍
沙箱是整个 DSec 的核心执行单元,也是最容易出问题的部分。我们试过三种路线:普通 Docker 容器、gVisor、Firecracker 微虚拟机,三种路线的区别和适用场景值得展开说一下。
4.1 三条技术路线的实测对比
| 方案 | 启动耗时 | 隔离强度 | 性能损耗 | 资源密度 | 适用场景 |
|---|---|---|---|---|---|
| Docker runc | 约 200ms | 中(共享内核) | 接近零 | 高 | 同批同源的常规训练任务 |
| gVisor(runsc) | 约 500ms | 较高(用户态内核) | 约 5%-10% | 中偏高 | 需要隔离系统调用但不想用 VM 的场景 |
| Firecracker 微VM | 约 1-2s | 最高(硬件虚拟化) | 约 3%-5% | 中 | 代码执行、高危操作、不可信代码验证 |
Docker 的启动速度确实快,但共享内核的问题也很明显:如果宿主机内核被攻击或者有 Bug,所有容器都会受影响;更常见的是容器内的进程能感知宿主机的一些资源信号,这会让训练结果产生微妙的噪音。
gVisor 作为用户态内核,把大部分系统调用拦截在用户态处理,隔离效果比 runc 好很多,性能损耗在可接受范围。但它在高并发 I/O 场景下的表现一般,特别是涉及大量网络请求和文件读写时,延迟会明显上升。我们用 gVisor 跑过一段时间的浏览器自动化任务,结论是勉强能用,但执行效率比 inux 原生差了约三成。
Firecracker 微虚拟机是 AWS Lambda 底层的隔离方案,每个实例占用内存只有 5MB 左右,启动速度在 1 秒级别,隔离强度是完整的硬件虚拟化级别。它最大的优势是每个沙箱有独立的内核,宿主机内核安全漏洞影响不到沙箱,沙箱内爆出再大的问题也不会波及其他沙箱。代价是镜像格式不一样,需要把容器镜像转换成 Firecracker 的根文件系统快照,存储占用略大。
4.2 DSec 默认策略:组合而不是单选
前面提到,我们的默认策略是组合使用,不是把所有任务都塞进同一套隔离方案。常规任务默认走 Docker + Seccomp + cgroup 的组合,不需要太强的隔离;涉及执行模型生成代码、解析不可信文件、外部 API 回调的任务,走 Firecracker 微虚拟机;对系统调用敏感但不能容忍 VM 开销的中间态场景,走 gVisor。
这里有一些比较细节的配置经验。Docker 场景下必须把 Seccomp 默认规则打开,并额外禁掉一些危险系统调用,比如mount、ptrace、reboot等。cgroup 的内存限制必须配合memory.swappiness=0设置,否则沙箱内进程在内存紧张时会大量使用 swap,导致训练任务变得极慢。Firecracker 场景下要注意调整/dev/vda的块设备限速参数,避免沙箱内突发写盘影响磁盘整体性能。
4.3 网络隔离与出网控制
智能体训练经常需要沙箱访问外部服务,比如调 API、搜索资料、访问数据库。如果让所有沙箱直接出网,既存在安全隐患,也容易造成带宽争抢。DSec 的网络模型做了两类控制:外网访问网关化和内网流量微分段。
外网访问统一走代理网关,每个沙箱通过环境变量拿到专属的代理地址。代理层会做协议过滤、流量审计和速率限制,防止某些任务因为模型误动作产生异常流量。
内网流量按租户和任务组做微分段。默认情况下,不同租户的沙箱之间禁止互通;同租户不同任务组的沙箱之间也要经过安全组规则才能通信。这套规则用 Linux 的iptables和ipset实现,编排层把安全组规则翻译成节点上的实际规则。有个容易踩的坑是:iptables 规则过多会拖慢节点上的新建连接速度,建议把常用的放行规则合并成 ipset 集合,尽量减少每条流量的规则匹配次数。
5. 数据闭环:轨迹采集、缓存管理和故障现场保留
智能体训练的基础设施如果只做到“能跑”,那离好用还有一大段距离。DSec 之所以能稳定支撑大规模训练,是因为我们专门设计了数据闭环:每个沙箱的完整运行数据都会被采集和保存,既支撑训练调参,也支撑故障排查。
5.1 轨迹数据的全量录制
智能体训练的数据有很强的时序特征:智能体的每步观察、思考、动作、反馈都要成对记录。这些数据被我们称为“轨迹数据”。轨迹数据是训练的核心燃料,但传统的日志系统很难承接它。
DSec 在每个执行沙箱内部署了一个轨迹录制代理。这个代理拦截几个关键的信号源:标准输入输出、文件系统变更、网络请求、进程启动与退出事件。拦截到的事件统一序列化为 JSON Lines 格式,定时刷到本地缓存,再由收集服务做批量上传。
轨迹数据量是很大的。一个运行 5 分钟的沙箱,平均要产生 30-80MB 的轨迹数据。如果做全量保留,存储成本会非常夸张。我们采取的策略是两级存储:热数据保留 7 天,存放在集群内的分布式文件系统上,供实时分析和短周期调参;冷数据压缩后归档到对象存储,保留 90 天。压缩这一步很关键,轨迹数据是高度结构化的,用对压缩算法能压到原始体积的十分之一。我们用的是zstd压缩,配合字典模式,压缩效果比 gzip 好不少,解压速度还更快。
5.2 分析回放的关键:动作级对齐
采集轨迹数据只是第一步,真正难的是“回放”。回放是指按照时间轴重现沙箱当时的运行状态——智能体看到什么、做了什么、环境如何响应。只有动作级对齐的回放,才能定位到失败发生的具体节点。
DSec 在回放系统上的设计做了两个重要决策。第一,每个轨迹帧必须包含全局单调递增的时间戳,这个时间戳由录制代理统一生成,不能依赖宿主机的墙钟时间,因为墙钟可能在 NTP 校准过程中发生跳变。第二,沙箱内所有非确定性输入(如随机数、系统时间、网络响应)都需要在轨迹帧中额外记录原始值,回放时用记录值替换真实值。
这两个决策让回放系统的可用性大幅提升。我们曾经靠回放定位到一个特别隐蔽的问题:某个智能体任务在某个节点的行为跟其他节点不同,排查很久找不到原因,最终发现是宿主机/etc/hosts文件配置不一致导致的 DNS 解析差异。没有回放系统的话,这种问题几乎不可能根因定位。
5.3 沙箱回收前的故障快照
任务失败时最怕的是“现场已经被清理了”。DSec 在任务结束前会做一个故障快照,把沙箱的文件系统变更、进程列表、网络连接表、资源使用曲线一次性打包,存储到独立的故障归档目录。
这里有一个细节:故障快照的动作必须发生在沙箱正式销毁之前,而且要有超时控制。有些失败的沙箱可能是因为磁盘满或者进程卡死导致响应很慢,如果没有超时控制,回收流程会被卡住,影响整个节点的资源释放。我们的超时设置为 10 秒,10 秒内快照没做完,强制销毁,保护节点整体可用性。
故障快照还承担着“训练过程可审计”的功能。如果最终训练效果异常,我们回放快照能还原某个任务当时到底发生了什么,避免因为数据缺失导致无法归因。
6. 大规模并发下的稳定性治理:限流、幂等与僵尸任务
当沙箱数量从百级涨到千级甚至万级时,稳定性问题的性质会发生根本变化。很多单机场景下不存在的问题,在大规模并发下会集中爆发。这一部分我们聊聊三种必须处理的稳定性问题。
6.1 控制面限流:防止任务风暴压垮调度器
最典型的故障场景是任务风暴:某个批训练任务因为模型 bug,导致子任务在几秒钟内全部重试,调度器瞬间收到数千个创建请求,数据库连接数被打满,整个控制面进入假死状态。
DSec 的限流分两层做。第一层是入口限流,每个租户每秒最多能提交 50 个任务创建请求,超过的请求直接返回 429,由租户侧做重试退避;第二层是调度限流,调度器自身批处理队列的消费速率限制在每秒 200 个,队列深度超过阈值后新任务会排队等待,而不是同步挤压控制面。
幂等设计也很关键。智能体训练任务的重试是常态,任务创建接口必须具备幂等性:同一个任务 ID 重复提交多次,只会创建一个沙箱,后续提交直接返回同一个沙箱的信息。这个幂等键不仅包括任务 ID,还包括任务内容和镜像版本的哈希值,避免参数变化后误用了旧任务的执行结果。
6.2 僵尸任务的安全回收机制
训练过程中经常出现一种现象:某个任务已经执行完毕(比如训练回合结束),但沙箱进程因为某些原因没有退出,一直占用资源。这类任务我们称为僵尸任务。如果回收机制不完善,僵尸任务会越积累越多,最终把资源池耗尽。
DSec 的僵尸任务扫描器每 30 秒执行一轮,扫描策略是:任务状态为“已完成”或“已取消”但沙箱进程仍存活超过 60 秒的,判定为僵尸任务;任务运行时长超出最长时限(默认 30 分钟)且没有心跳上报的,判定为假死任务;资源使用率触发 cgroup OOM 且没有重启价值的,直接标记回收。
回收动作必须分级。普通僵尸任务可以等待优雅退出;如果是无法响应 SIGTERM 的假死任务,直接 SIGKILL。还有一类比较难处理的是“任务进程已退出,但子进程还活着”的情况。Docker 容器如果父进程退出而子进程不会自动清理,需要额外做进程组隔离和孤儿进程收养。我们统一用su-exec启动沙箱主进程,并把主进程放在独立的进程组和会话中,这样清理时可以做到整体收割,不留孤儿。
6.3 慢依赖与外部服务雪崩
智能体训练经常要调用外部服务,包括模型 API、数据库、搜索引擎。如果外部服务变慢,沙箱内任务的等待时间会拉长,资源被白白占住,后续任务排队越来越严重,最终形成雪崩。
我们的应对思路不是简单加超时,而是做分层的超时控制和熔断。第一层是网络连接超时,默认 5 秒;第二层是读超时,默认 30 秒;第三层是任务整体超时,默认 5 分钟。外部服务的健康状态会被动态记录到本地缓存中,连续失败超过 3 次的服务被熔断 30 秒,熔断期间请求直接失败并触发降级逻辑,而不是继续等待。
还有一个容易被忽略的问题:DNS 缓存。沙箱内任务如果频繁做 DNS 解析,汇聚到节点级 DNS 缓存服务上,会形成高并发查询压力。我们在每个节点上跑了一个本地 DNS 缓存,缓存 TTL 根据记录类型调整,A 记录缓存 60 秒、AAAA 记录缓存 30 秒,显著减少 DNS 查询次数。这个细节在排查任务响应慢的问题时帮了大忙。
7. 真实案例复盘:一次磁盘写满导致的集群级故障
理论讲了这么多,说一个我们真实踩过的大坑,完整还原从现象到根因再到修复的排查过程。这个案例对每个做沙箱基础设施的人都很有参考价值。
7.1 故障现象
那天晚上在跑一批 8000 个智能体子任务的批量训练,任务平均耗时 2 分钟。跑到第 40 分钟左右,告警面板开始刷屏:大量任务报错No space left on device。起初我以为是某个节点磁盘满了,手动排查发现几乎所有节点都同时报磁盘写满。
更诡异的是,不少报错的任务本身并没有大量写文件的需求。有些任务只是读了一个配置文件,在内存里做完计算,尝试写日志时就报磁盘错误。这意味着问题可能出在共享存储或者全局挂载点上。
7.2 排查链路
我先把控制面数据库的自动备份关掉,然后登录三个有代表性的节点看磁盘状态。df -h显示/分区剩余空间正常,但/data挂载点显示用量 98%。继续看/data下哪个目录占用最大,定位到/data/sandbox-overlay。
这个目录是沙箱镜像层的联合文件系统挂载点。理论上,每个沙箱销毁后应该自动清理对应的 overlay 层。我用du --max-depth=1扫了一遍,发现最老的一批 overlay 目录是三天前的,说明清理任务从三天前就已经开始大面积失联。
进一步排查发现,清理任务本身是用一个独立的定时 CronJob 跑的,它工作时需要连接控制面的 API 获取“可清理沙箱列表”。而控制面 API 三天前因为一次批量任务提交(当时有 2000 个任务排队)触发了数据库连接池耗尽,导致 API 出现了长达几分钟的不可用窗口。CronJob 在 API 不可用时会直接跳过本轮清理,但代码里有一个 bug:跳过时没有更新自己的心跳状态,导致集群监控系统误以为清理任务已经失联,把它标记为“故障”,监控系统反复触发重启,却始终没有真正清理 overylay 数据。
7.3 根因与修复
根因就是“清理链路中有一个环节悄悄失效,但没有任何降级策略”。CronJob 在 API 不可用时的处理逻辑是跳过本轮,而不是重试,而且它没有独立的存活探针,一旦跳过就不算存活,监控系统误判并不断重启它,但重启也不会弥补跳过的那几轮清理。
修复方案分三层。第一层,给清理任务加独立的心跳上报和数据目录扫描逻辑,即使控制面 API 不可用,也能根据节点本地的沙箱状态直接清理。第二层,控制面 API 加自动降级机制,数据库连接池不足时,优先保证清理任务的请求被处理。第三层,监控系统增加“跳过轮次”的告警,超过 3 轮未清理就发高优告警。
此外还加了一道保险:每个节点上的磁盘用量达到 85% 时,节点代理自动触发一次本地 overlay 清理和镜像缓存清理;达到 92% 时,停止接受新的沙箱调度。这道保险后来还救过我们一次,因为某个训练任务在沙箱内意外生成了海量缓存文件,磁盘直接冲到 89%,保险机制把影响控制在了单节点,而不是整个集群。
8. 从“能跑”到“好用”:可观测性、安全边界与后续演进
沙箱基础设施做到一定程度,稳定性不再是唯一目标,效率和安全性会变得同样重要。这里分享一些我们在把 DSec 从“勉强能用”推向“真正好用”的过程中沉淀下来的经验。
8.1 多层级可观测性设计
可观测性不是堆监控面板,而是要能在排查问题时快速定位到对应层级。DSec 的观测体系分四层:任务层、沙箱层、节点层、集群层。
任务层的指标包括任务排队时间、执行时长、失败原因分类、重试次数。沙箱层的指标包括启动耗时、内存峰值、CPU 使用曲线、I/O 次数、网络连接数。节点层的指标包括资源利用率、镜像缓存命中率、僵尸任务数量、Docker/微VM 创建速率。集群层则关注整体资源水位、调度成功率、控制面延迟。
真正好用的可观测系统应该在异常发生时自动联动分析。我们的做法是给关键指标设置了多级阈值,当指标越线时,观测系统会自动收集当前节点上所有沙箱的上下文到一张快照表,供事后分析。这样做能避免“指标报警了,但现场已经没了”的窘境。
8.2 安全边界的进一步收紧
沙箱安全不是一次配置就能一劳永逸的,需要动态更新。DSec 在安全方面的演进方向主要是这三个方面:
第一,镜像签名验证。所有拉取到节点上的镜像层都要校验签名,防止镜像在拉取过程中被篡改。这个在内部集群可能觉得多余,但一旦集群要接入外部镜像仓库或者多租户共享,它的价值就立刻体现了。
第二,最小权限执行。沙箱内任务的进程默认以非 root 用户运行,即使任务被攻破,影响面也有限。这里容易踩坑的是,很多智能体训练脚本习惯在 root 下运行,迁移到非 root 需要额外处理文件权限和依赖安装,但这一步无论如何都要做。
第三,敏感数据脱敏。智能体任务在跑的过程中可能接触到 API Key、数据库密码等敏感信息,这些信息一旦进入轨迹数据被回放,就是安全隐患。我们在轨迹录制代理中加入了脱敏正则库,自动识别常见密钥格式并替换成掩码;另外还在网络代理层加了一层管控,只允许沙箱访问白名单内的外部域名,默认拒绝未知域名。
8.3 DSec 未来的扩展方向
DSec 这套沙箱基础设施目前已经稳定支撑了我们多个智能体训练项目的日常运行。就我自己的观察,下一步有几个方向很值得探索。
第一个方向是面向推理侧的伸缩联动。智能体训练的前端推理(也就是 DeepSeek 模型服务部分)和后端沙箱执行目前是分开调度的,如果能把两部分资源联动起来,推理队列压力大时提前收缩沙箱池,沙箱池排队过多时动态扩缩推理承载,整体资源利用率还能再上一个台阶。
第二个方向是沙箱内训练数据的自动管理。现在轨迹数据上传是定时批量执行,未来可以改为按事件触发,比如某个智能体回合结束立即上传,这样能让训练反馈的延迟降得更低,对实时调参场景很重要。
第三个方向是异常沙箱的自动归因。目前我们的归因分析还是半手动模式,观察系统收集数据之后,需要人工去查。如果能把历史故障案例沉淀成模式库,让新故障自动匹配最相似的历史案例并推荐排查方向,运维负担会大大降低。
回到开头说的那句话:智能体训练对基础设施的真正要求,不是“能跑通一个任务”,而是“几万个任务同时跑还能保持稳定、可控、可复盘”。DSec 在沙箱隔离、资源弹性和数据闭环上的这套实践,已经让我们在这条路上走了很远。
最后分享一个我很建议的做法:如果你也在搭建类似的沙箱基础设施,一定从第一天就把故障快照和轨迹录制做好。这两套数据看起来不是训练的核心产出,但一旦遇到不影响模型、直接影响训练的基建问题,它们就是你能拿出来的唯一救命稻草。别等到集群崩了才想起来要补课。