智能体训练专用沙箱基础设施:DSec弹性调度架构全解析

发布时间:2026/10/3 18:51:29
智能体训练专用沙箱基础设施:DSec弹性调度架构全解析 1. 为什么“训模型”和“训智能体”不能共用一套基础设施先讲一个我自己的经历。2024年年初我们团队开始从“单模型训练”转向“智能体训练”——当时还没有DSec这套东西第一反应是“直接复用现有的模型训练平台”。结果两周之内就被现实狠狠教育了一顿。模型训练平台擅长的是“把数据集喂给一个模型跑完一轮评估一轮”但智能体训练完全不同它是让一个或一组Agent反复与环境交互拿反馈、改策略、再交互。这个循环里最耗费资源的不是算力本身而是并发环境的模拟、状态快照的恢复、和轨迹数据的回放。举个具体例子。我们用一套大概80台GPU的训练集群去跑一个简单的ReAct Agent每个回合Agent要调一次检索API、改一次工具、再看一次环境返回。当时最头疼的问题是一个训练批次里可能有几百个环境的实例在同时运行环境之间又共享同一套代码库和配置。只要有一个环境实例把共享配置改了整个批次全部崩掉。排查了半天发现是Python的os.environ被Agent子进程给改了。这种问题在模型训练里基本不会遇到但在智能体训练里几乎天天都有。另一个问题是资源利用率。模型训练的消耗模式是“前向反向参数更新”资源曲线稳定可预测。智能体训练不一样Agent可能在某个环境里卡住等超时、可能在某一步爆出一个超大状态、也可能一个回合只跑了100ms就开始等下一个Action。这种高度不规律、不可预测的负载模式如果用传统静态分配的容器集群去扛要么CPU内存冗余浪费要么高峰时期互相抢资源。我们当时80台GPU机器上峰值时GPU利用率能到80%但谷底只有3%——一半时间都在空转。当时我们其实也调研过市面上的方案包括Ray、Kubernetes Argo Workflows、甚至直接用Ray Serve做环境托管。Ray确实解决了一部分“并发调度”问题但它对“环境隔离”和“生命周期模板化”的支持几乎没有。我们需要的是每个训练单元比如一个Agent的1000个并行环境拥有独立的网络命名空间、独立的文件系统快照、独立的状态回收机制同时还能在分钟级内完成批量扩缩容。Kubernetes能做到一部分但Pod级别的调度粒度太粗一个Pod里塞多环境实例又回到了隔离问题。于是我们决定自己写一套调度层代号DSec——DeepSeek Elastic Compute的缩写。定位非常明确它不是模型训练框架而是智能体训练专用的沙箱基础设施。它管的是“环境怎么起、怎么隔离、怎么调度、怎么回收”至于Agent策略怎么更新、奖励怎么算那是上层框架的事情DSec不碰。这套东西后来被团队内部戏称为“Agent的宿舍管理员”——你进去住多少天生命周期、住什么样的单间隔离级别、几点熄灯资源回收全由它管但你今天学什么功课它不管。从这篇文章开始我会把这套系统的设计思路、核心组件、调度策略、踩坑记录全部拆开讲。适合谁看两种人一种是正在搞多智能体训练/强化学习环境仿真的工程师正在为“环境实例太多管不过来”头疼另一种是准备搭建内部训练基础设施的平台团队想看看除了Ray和K8s之外还有什么思路。如果你只是调API跑了个LLM这篇文章的前半部分也能帮你理解“Agent训练到底比模型训练多吞掉多少资源”。2. DSec的整体架构四个组件怎么各司其职DSec整体分成四层弹性调度器Scheduler、沙箱运行时Sandbox Runtime、数据总线Data Bus、观测服务Observability。这四层我不会画架构图用文字把它的数据流讲清楚。2.1 弹性调度器负责“要多少给多少不要就立刻收走”调度器是整个系统的核心决策模块。它的职责只有三件事拿到上层训练框架的资源申请、决定在哪些物理节点上创建沙箱、监控沙箱空闲状态并回收。它不对Agent的逻辑做任何假设——不管你是用ReAct还是CoT还是自研的规划算法调度器看到的只是“环境实例”和“资源需求”。调度器内部维护了一张全局资源热力图。每个物理节点上报自己的CPU、GPU、内存、网络带宽的实时余量调度器每500ms刷新一次。当一个训练任务申请“100个沙箱每个4核8GB约等于0.2卡GPU”的时候调度器不会真的为每个沙箱独占一张卡而是按“弹性份额”来分配。这里的关键点是GPU是按算力份额切分的不是按卡切分的。我们用MPSMulti-Process Service把一张A100切给多个沙箱用每个沙箱拿到例如20%的算力时间片。这对智能体训练特别合适因为Agent大多数时候在做“思考、查工具、操作环境”真正的模型推理负载并不像纯模型训练那么连续。# 调度器核心决策逻辑的简化示意 class ResourceRequest: def __init__(self, cpu_cores, mem_gb, gpu_share, sandbox_type, ttl_minutes): self.cpu_cores cpu_cores self.mem_gb mem_gb self.gpu_share gpu_share # 0.0 ~ 1.0占一张卡的百分比 self.sandbox_type sandbox_type # python-repo-exec, browser-automation, multi-agent-net self.ttl_minutes ttl_minutes # 硬性TTL防止僵尸沙箱 def best_fit_nodes(scheduler, request): candidates [] for node in scheduler.nodes: if node.available_cpu request.cpu_cores and node.available_mem request.mem_gb: if node.available_gpu_share request.gpu_share: candidates.append(node) # 按“调度后剩余碎片最小化”排序 candidates.sort(keylambda n: (n.available_cpu - request.cpu_cores) (n.available_gpu_share - request.gpu_share)) return candidatesTTLTime To Live这个东西我必须强调它不是在后面补的兜底方案而是第一版就刻进设计里的。智能体训练最容易出现的情况是一个Agent环境陷入死循环比如在终端里跑了个不返回的shell命令或者训练框架本身崩了没人回收环境。没有TTL的话几百个空转环境会把集群拖垮。我们默认TTL是10分钟训练框架必须每5分钟做一次心跳续租。心跳断了两次调度器强制回收。2.2 沙箱运行时隔离不是虚拟机的“隔离”是进程级的“圈养”沙箱运行时这个组件我花的时间最多。它的目标很明确给Agent一个“它以为自己在操作真实系统”的假象但所有动作都被关在笼子里。我们试过几种方案。第一种是完整虚拟机QEMU/KVM隔离性确实好但启动时间动辄几十秒而且资源开销太大。一个Agent训练的回合可能只有几百ms你不可能为每个回合去冷启动一台虚拟机。第二种是Docker容器启动快、资源可控但它默认的PID/网络隔离对Agent来说“太彻底”了——Agent有时候需要访问宿主机的某些服务比如模型推理的HTTP端点跨容器网络至少要加一层端口映射配置很啰嗦。第三种是Firecracker这种微虚拟机兼顾隔离和启动速度但我们对内核版本依赖太强调试成本高。最终我们采用的是**“进程组沙箱 可选网络命名空间”的混合方案**。具体来说用Linux的cgroup做资源限制用namespaces做文件系统和网络隔离但保留一个“受控共享共享区”Shared Oversight Region让Agent可以通读宿主上托管的模型推理服务端点。这不是传统意义上的“容器”我们内部管它叫“进程围栏”。Agent在里面跑ps能看到自己的进程和宿主上的进程这样它真的以为自己在真实机器上但它对文件系统的写操作全部指向一个临时目录对网络的出站请求全部经过一层透明代理过滤。这个透明代理非常关键。Agent有时候会试图下载一个包、连接一个外部API、或者直接往外丢数据。DSec的沙箱网络代理会拦截这些请求按策略决定放行、改写还是直接拒绝。策略分三档第一档允许所有出站连接适合开放环境的探索任务第二档只允许白名单域名的HTTPS适合电商下单、网页操作这类任务第三档完全禁网适合纯代码执行任务。训练框架创建沙箱的时候选择哪一档由任务的“信任等级”决定。2.3 数据总线轨迹数据的“快递专线”不走共享盘智能体训练有一个和模型训练很不一样的地方训练数据的产生和消费是异步的而且量级极其不均匀。一个Agent跑完一个回合产生的轨迹数据可能只有几个KB短对话也可能有几百MB比如操作了NetLogo界面、浏览器全是截图。如果我们把所有轨迹数据先写到共享存储再从共享存储读给训练框架共享盘会毫无疑问成为瓶颈。DSec的数据总线做了一件事让数据和沙箱“同生共死”。每个沙箱在自己的生命周期内把轨迹数据先写在本地的一块内存文件系统/dev/shm/dsec-trace/里。沙箱结束之前数据总线组件会异步地把增量数据以“事件流”的形式推送给订阅者训练框架的回放模块。推送是走网络直连的不落盘。这样做的效果是1000个并行沙箱产生的轨迹数据几乎实时地到达训练框架的内存队列而共享存储只在最后“归档”阶段承担一次写入。# 沙箱内部查看轨迹文件流 $ ls -la /dev/shm/dsec-trace/ total 1284 drwxrwxrwx 2 agent dsec-trace 120 Jan 5 03:22 . -rw-rw-r-- 1 agent dsec-trace 882912 Jan 5 03:22 episode_00123.trace -rw-rw-r-- 1 agent dsec-trace 1024 Jan 5 03:22 episode_00123.done2.4 观测服务不是“看监控”是“看Agent在环境里干了什么”最后说观测服务。这一层一开始是团队里定位最模糊的但后来成了大家最依赖的模块。观测服务不只是采集CPU、内存这样系统指标它要做的是记录Agent与环境交互的语义日志。比如Agent调用了什么工具参数是什么环境返回了什么Agent的决策链是长还是短它在哪一步卡住了我们给每个沙箱注入了一个轻量级的sidercar伴行进程它不是Agent本身也不是环境本身而是在旁边“看”的观察者。它监听沙箱内通过标准库发起的工具调用Python的subprocess、requests、exec调用把调用链和结果摘要发给观测服务。这里面最有价值的是“步骤耗时瀑布图”——能看到Agent在哪个环节浪费了最多时间。我们优化过一个大表格操作Agent结果发现它光是在下载HTML页面并解析表格上就花了70%的时间策略改进点根本不在模型Prompt上而在环境怎么渲染表格给了它一个乱麻一样的DOM。观测服务的数据同时服务于两个场景一个是人工Debug一个是自动化的质量看门狗。质量看门狗会检查Agent是不是进入了死循环比如连续20次调用同一个工具且参数没变一旦检测到就自动终止该沙箱并在集群里把它标记为“可疑”。这个机制的误伤率一开始很高——有的Agent故意在图探索里反复点击同一个按钮直到触发页面变化就被看门狗误杀。后来我们加了一个“过度重复”的判定阈值必须连续2分钟重复且0个不同API返回值才算死循环。3. 沙箱模板化与生命周期管理从“起环境”到“按需定制环境”3.1 为什么模板化是硬需求而不是用来炫技的早期我们没做模板化每个训练任务都是临时写一段Python脚本来配置环境。两个问题马上暴露出来第一环境配置不可复现。一个Agent训练中断后想接着跑环境却已经变了结果对比完全不公平。第二环境配置时间和算力消耗成线性关系。一个跑大模型微调的习惯性假设是“环境初始化是one-time cost”但智能体训练里每次滚动回放都要重新起环境初始化时间被反复循环消耗。模板化最终落地成了一套YAML DSL。每个训练任务声明一个“场景模板”Scenario Template里面定义基础镜像、可被Agent调用的工具清单、网络策略档位、共享数据挂载路径、环境变量的安全白名单、以及最关键的存活状态定义Liveness。存活状态不是“进程活着没”而是“环境是否处于可交互状态”。比如一个浏览器自动化场景即使Chrome进程健在如果没了可用的Tab这个环境也算“半死”。Liveness的定义直接决定调度器什么时候回收它。# template-browser-search.yaml apiVersion: dsec.io/v1 kind: ScenarioTemplate metadata: name: browser-search-role spec: baseImage: dsec/python-3.11-browser:2.1 tools: - name: browser.click permissions: allow - name: browser.type permissions: allow - name: terminal.run # 禁止Agent执行可能破坏环境的多行命令 argumentValidation: [ {pattern: ^[a-zA-Z0-9\\-_\\.]$, hint: 仅允许简单命令} ] networkPolicy: tier: 2 allowDomains: [example.com, api.example.com] sharedData: - hostPath: /data/common-vocab.db mountAs: readonly envWhitelist: [ OPENAI_API_KEY, AGENT_ID ] liveness: checkType: tcp-connect target: 127.0.0.1:9222 timeoutSeconds: 15模板的写法决定了一个平台能支持多少种智能体场景是从“能跑一个Demo环境”到“能跑产业级任务”的分水岭。我们后来在沙箱里接入了浏览器自动化的场景基于Playwright、代码执行场景基于CPython裸进程、以及多Agent互动的场景沙箱里额外起一个轻量级的Agent网关。3.2 生命周期的四个阶段哪些回收决策是踩坑之后才加上的生命周期管理我们用四个阶段来表示Provisioning分配中、Active运行中、Draining排空中、Recycled已回收。前两个阶段很好理解排空和回收是重点设计。Draining阶段是整个系统里最能体现“弹性”二字的机制。调度器判断一个沙箱应该被回收不是因为任务结束了而是因为出现了以下三种情况之一一、TTL到期且心跳续约失败二、Liveness检查连续三次失败比如浏览器崩溃后无法恢复三、上层训练框架主动标记“这个环境不会再用”例如训练回放已经越过了它产生的那段数据。Draining阶段会给沙箱一个“优雅停机窗口”默认30秒。窗口期内数据总线会把沙箱内还没冲刷的轨迹数据强制排空到共享归档目录防止Agent最后几步的关键决策丢了。窗口期内也允许一个“救援操作”如果沙箱里有正在进行的工具调用会等它返回或者超时。一个沙箱进入Draining状态后调度器不会急着把它的资源标记为可用而是等真正的Recycled信号——这么做是为了防止资源被二次分配后旧环境的数据还没有完全归档新环境就写进来了导致两个任务的数据交错。你可能会问为什么不能直接kill然后强制重开因为在智能体训练里一个环境的“状态”不只是进程状态还包含临时文件、浏览器缓存、Agent工作目录里的中间产出物。这些即使对最终决策没用也往往是Debug时“为什么Agent这次和上次行为不一样”的关键证据。我们有一次把一个沙箱killed之后发现Agent之前生成的中间文件全部丢失导致复现实验怎么都对不上。从那以后Draining阶段强制保留掉落的文件快照至少2小时再交给归档任务按日清理。资源回收方面有一个细节不要只回收CPU和内存GPU份额的回收尤其要“泄压”得当。我们这踩过一个大坑Agent批量被回收时MPS的算力份额瞬间全部释放回池下一波调度请求会在几毫秒内抢到全部刚释放的GPU份额导致节点短暂过载模型推理服务出现延迟抖动。解决办法是给调度器加了一个“份额冷却器”释放的GPU份额不是立刻全局可见而是浮在一定时间内渐进可见类似限流算法的“令牌桶”。4. 高并发沙箱的调度策略亲和性、装箱和热点转移智能体训练的大规模性和普通微服务的大规模性不是一回事。微服务的并发特征是“请求无状态随时可以水平扩”。智能体训练的并发特征是“长连接、有状态、且每个连接都比较重”。这让调度策略必须同时考虑三类约束资源约束能不能放下、数据约束Agent要访问的共享数据在哪台机器上、网络约束Agent要连接的对端服务在哪台机器上。4.1 亲和性调度把缓存和共享数据的“距离”也纳入打分DSec的调度打分函数里除了经典的CPU利用率、内存余量、GPU份额碎片量之外加了一项“数据亲和分”。这项分的逻辑是你一个沙箱要挂载的共享数据比如一个几十GB的搜索引擎索引库如果在目标节点上已经存在缓存副本加50分如果只能走网络读远端扣30分如果目标节点根本没有副本且远端带宽闸口拥挤扣更多。大部分调度器不愿意做数据亲和性因为在Kubernetes里“数据在哪个节点”本身是个很难追踪的问题。但我们把共享数据挂载和沙箱调度放在同一个控制平面里所以每次沙箱调度的时候都能拿到一张“共享数据分布表”。这张表是数据总线模块维护的每30秒广播一次。亲和性带来的效果很直观沙箱在“数据就绪”状态下启动的比例从原来的43%提升到了91%。数据就绪意味着Agent启动之后不需要花大量时间去拉取数据这与沙箱启动时间的关联非常强。刚开始做亲和性调度时我以为最大的收益是节省网络流量后来发现更大的收益是降低了Agent首次交互的延迟——Agent如果起在一个数据缓存远的节点上一次普通的文件读取可能要等2秒这个等待在Agent的上下文里会表现为“环境非常迟钝”有时候会连锁诱发它的错误决策。4.2 装箱策略反着用“按卡分配”的思绪传统模型训练平台的装箱思路是“把一份大申请完整塞进一张卡”。DSec的抖动负载决定我们不能这么干如果一个Agent的算力需求是0.2卡一张A100既然可以放5个这样的沙箱偶尔高峰期5个沙箱同时请求算力MPS的时间片竞争会让每个Agent的有效响应速度变成原来的1/5。这在智能体训练里几乎等同掉线。我们采用的装箱策略是“按时间片错相”。调度器会统计每个节点上已分配沙箱的时间片模式这个Agent是在每个回合开头耗算力多、还是结尾耗算力多、还是均匀分布。如果一个新请求的负载模式和节点上现有沙箱的负载高峰重叠度太高即使节点还有大量空闲算力份额调度器也会优先把它放到另一个空闲但已有互补负载的节点上。这个策略一开始被队友吐槽“你管得太细了”但实际运行一个月后节点的GPU有效利用率中位数从31%提到了54%而P95的响应延迟反而是下降的。4.3 热点转移Agent训练场景特有的“集群漂移”还有一种只有智能体训练才会遇到的奇葩现象我起名“集群漂移”。当训练任务进入“评估阶段”时所有环境实例会同时进入高频交互模式而当任务进入“策略更新阶段”Agent在等梯度更新所有环境实例同时进入空闲等待模式。这种“整体节奏同步”的负载波纹在集群视角看就像一群候鸟整齐地从一个区域飞到另一个区域。热点转移机制就是用来应对这种节奏的。它的做法是允许沙箱在生命周期内迁移一次宿主机。注意“一次”这个限制很关键——迁移成本和沙箱内的状态量成正比频繁迁移会让系统浪费在状态序列化上的时间超过省下来的调度收益。当一个节点的持续负载超过85%时调度器会启动转移流程选一个负载温和的节点作为目标将部分沙箱做一次“冻结-拷贝-恢复”的迁移。Agent在转移过程中会感知到一次极短的暂停大约2到5秒但因为训练框架有回合级重试机制这点暂停对整体收敛影响极小。迁移做到无损是不可能的。实测表明迁移一个带有浏览器会话的沙箱大概有15%的概率会把Cookie或者Tab状态丢一部分。我们为此增加了“迁移前健康检查”和“迁移后回放验证”一旦验证失败自动回滚到迁移前状态并重新分配。这部分的代码量占DSec总代码的近四分之一——也印证了一个真理在分布式系统里“看起来最简单的状态迁移”往往是最难做的东西。5. 性能实测1000个并行环境训练一个购物比价Agent的真实数字理论说了很多上点实际数据。我们在内部做了一次大规模压测场景是一个“购物比价Agent”它需要在模拟电商环境里浏览商品页、提取信息、对比价格、最后完成购买决策。环境是浏览器自动化每个沙箱有4核8GB内存和0.3卡GPU份额浏览器里的OCR和页面渲染需要GPU加速。5.1 调度吞吐和启动延迟我们一次性创建1000个沙箱记录从发出申请到全部沙箱进入Active状态的时间。这个指标衡量的是调度器和运行时协作的极限能力。指标数值总申请沙箱数1000调度决策完成时间2.8秒全部进入可交互状态34秒单个沙箱的最长启动延迟41秒启动失败数需要重调度7个失败原因镜像拉取超时3、内存碎片不足2、网络策略下发延迟234秒看起来不慢但如果你上过Kubernetes就会知道一个Pod从调度到Running往往也要10秒左右1000个Pod稳定启动通常要1分钟以上。DSec用了一个比较取巧的技巧沙箱运行时不是每次新建整个环境而是用“预启动池”——调度器在闲时预热100个“空壳沙箱”只有基础进程和网络代理没有加载场景数据请求进来时直接把场景模板在空壳里做热加载加载一个场景模板平均只要8秒。空壳池不够时才走冷启动。这个设计思路本质上就是数据库连接池的翻版但对智能体训练非常管用。5.2 稳定运行时的资源抖动训练任务平稳运行后我们对集群做了24小时的监测。记录节点级的CPU利用率、内存余量、GPU有效算力使用率、以及Agent回合平均响应时间。比较有意思的发现是Agent回合平均响应时间和CPU利用率之间并没有强相关性。原因是大多数Agent的“等待时间”花在了环境加载页面、DOM解析和网络请求上而不是真正的CPU计算。这让我们的优化方向从“压榨CPU”转向了“压降环境延迟”。后来我们给浏览器自动化沙箱单独做了一个“页面预渲染缓存”把高频访问的URL渲染结果缓存下来Agent 回合平均耗时直接降了23%。还有个P95的尾延迟问题。在没有DSec之前我们常观察到某个Agent的某个回合卡了30秒但整个集群看起来一切正常。DSec的观测服务上线后我们追踪到这类尾延迟的根因大多数是“沙箱所在节点正在跑离线模型批推理任务”节点CPU被抢占导致浏览器渲染慢。为此我们专门把“批推理任务”和“交互式沙箱任务”做了物理隔离——批推理只允许占用较低的CPU优先级且不可抢占交互式沙箱的算力份额。5.3 数据总线的吞吐表现这次压测里1000个沙箱在1小时内产生了约800GB的原始轨迹数据含截图。数据总线把它们以事件流形式推送回了训练框架训练框架的实际消费率为1.2GB/分钟。这个量级的吞吐其实不算高但我们卡过的不是带宽而是事件读取顺序的错乱。因为沙箱是并发推送的训练框架拿到的轨迹文件顺序很乱Agent的回合展示完全没法看。后来我们在数据总线里加了一个“回合序列号”每个事件带一个单调递增的ID消费端按照ID做重排才解决了乱序问题。在观察这个压测过程的时候我还发现一个测试之外的隐藏收益数据总线的事件流结构天然适配了“分段回放”的训练模式——训练框架不需要等一个Agent跑完完整回合就能在它跑第二回合的同时回放第一回合的数据。这让“环境运行”和“策略更新”重叠了起来端到端训练吞吐大约有30%的提升。这部分收益没有写进任何设计文档纯粹是压测时无意间摸出来的。6. 踩坑实录那些在文档里绝对不会出现的三个深坑任何基础设施都有一堆“上线前根本想不到、上线后必须立刻补”的坑。DSec从开发到稳定运行我们踩过的大坑少说十几个挑三个最典型的讲环境变量的连锁污染、沙箱DNS缓存的幽灵响应、以及进程回收的竞态条件。每一个都导致过严重的生产事故也促成了一次代码级的重构。6.1 环境变量的连锁污染一个沙箱“炸”了整个训练批次有一次线上训练任务大面积失败失败的Agent都有一个诡异的共同点它们调用的外部API都收到了错误的认证凭据。查了半天发现AI根因是我们某个沙箱里设置了一个GLOBAL_API_TOKEN环境变量——本来是给该沙箱内的API调用做认证的但该变量通过共享的进程环境被继承到了同一节点上的其他沙箱。其他沙箱发起调用时透明代理误把这个变量认成自己的认证标识全部携带着一个不属于自己的token往外发。后来我们做了一个“环境变量按沙箱注入”的硬隔离沙箱运行时内的进程只能看到DSec显式注入的环境变量列表模板的envWhitelist字段除此之外一律不可见。系统的其他模块如果想访问宿主机环境变量必须通过一个独立的“环境代理Socket”来请求不能直接os.environ。这么做虽然写代码时不方便但从根本上切断了变量污染的传递链路。6.2 DNS缓存的幽灵响应Agent“看到”了已经删掉的实例第二个坑和网络代理有关。沙箱内的Agent请求外网域名时会经过DSec DNS代理。这个代理默认给每个域名设置了一个300秒的TTL缓存。问题是某些训练环境的域名解析结果会动态变化比如环境A的API服务被销毁后重建在另一台机器上而沙箱内的Agent在缓存TTL内依然拿到旧的IP然后连接失败。这个坑的表现非常隐蔽Agent会执着地反复重试同一个失败的连接看起来像是Agent自己陷入了死循环但实际上是缓存了过期IP。我们排查时从观测服务看到Agent一直在请求某个IP但那个IP上的服务根本不存在而直接测试域名解析又是正常的。最终我们把DNS代理改成“短TTL 每次解析都校验IP是否仍存活”的组合模式。如果解析出来的IP在近30秒内没有被任何沙箱访问过就直接报“域名已失效”让Agent立刻走重试而不是死等。6.3 进程回收的竞态条件环境都回收了Agent还在里面跑这个坑是彻头彻尾的“分布式系统经典教学内容”。沙箱回收时我们的Draining流程会依次执行停止网络代理 → 排空轨迹数据 → 向Agent发一个终止信号 → 等待进程退出 → 归档文件 → 释放资源。但实际执行时如果Agent进程对一个系统调用的阻塞时间超过了等待窗口终止信号就变得无效而网络代理先停了意味着Agent还能在本地计算但出站路径已经断了。结果是Agent进程变成了一个“外部完全不可见的幽灵进程”在沙箱内继续空转消耗CPU。修复方案是“先断资源再终止进程最后排空数据”。改成调度器先把沙箱的CPU和内存额度减到接近零让Agent无论做什么系统调用都会立刻陷入等待然后发送终止信号这时Agent无法再消耗新资源也只能顺从地退出最后才排空数据并归档。这个顺序调整让“僵尸进程”的残留时间从小时级降到了秒级。7. 上线后的一些运营心得DSec从上线到现在系统的稳定性已经不需要我操太多心了但运营层面上有一些经验觉得比代码还值钱写下来给你们参考。第一点是“预留资源”和“弹性分配”的边界要设好。弹性计算很容易给人一个错觉反正能随时扩缩容调度器只要看着办就行。但Agent训练的突发性比云上微服务的突发性猛得多——前一秒还是10个环境下一秒可能直接冲到800个。如果调度器没有一层“突发保护”大量新建沙箱会把系统关键组件比如镜像仓库、数据总线瞬间打满。我们的做法是给每个租户或每个训练任务设置一个“弹性信用额度”额度由历史消耗模式和当前全局负载共同决定。额度内的请求实时分配超出额度的请求进队列排队。这个排队机制上线后镜像仓库的P99延迟从2.1秒降到了120毫秒。第二点是沙箱的观测数据要定期归档并且按“可回放”标准来存。“可回放”意味着不仅要有轨迹日志还要有环境状态快照和决策上下文。我们的归档任务每6小时做一次按任务名称、沙箱ID、回合编号三级索引。这个索引模式最早是为了满足Debug需要结果后来帮了数据团队大忙——他们直接用归档数据做了一版行为分析发现某个Agent在高价格区间上容易做出“试探性出价”的行为这是纯看最终结果看不出来的。第三点也是我最想强调的一点基础设施的弹性能力必须“显式暴露给上层”让训练框架真正用起来。很多团队做完调度系统就完事了觉得底层能自动伸缩就万事大吉。但实际上如果训练框架不了解底层的弹性语义它会做出很“浪费”的请求模式——比如一次要满1000个沙箱之后再也不释放。DSec在落地时花了不少功夫去培训上层框架的设计师何时该用“固定沙箱池”、何时该用“按回合创建、用完即弃”的沙箱、以及何时该使用“共享场景模板”而不是每次都新建。这些上层的配合才是弹性的真正来源。现在我们的业务方已经能很熟练地控制自己的资源申请节奏调度器的压力也小了很多——一个好的基础设施应该是它的使用方越懂它整个系统的效率就越高。