☰
智能体训练专用沙箱基础设施:DSec弹性调度架构全解析
2026/10/3 18:51:23 网站建设 项目流程

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是按算力份额切分的,不是按卡切分的。我们用MPS(Multi-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(key=lambda n: (n.available_cpu - request.cpu_cores) + (n.available_gpu_share - request.gpu_share)) return candidates

TTL(Time 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.done

2.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)、网络策略下发延迟(2)

34秒看起来不慢,但如果你上过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在落地时花了不少功夫去培训上层框架的设计师:何时该用“固定沙箱池”、何时该用“按回合创建、用完即弃”的沙箱、以及何时该使用“共享场景模板”而不是每次都新建。这些上层的配合才是弹性的真正来源。现在我们的业务方已经能很熟练地控制自己的资源申请节奏,调度器的压力也小了很多——一个好的基础设施,应该是它的使用方越懂它,整个系统的效率就越高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询