资讯详情

资讯详情

每天300万沙盒背后:Agent隔离基础设施的工程挑战与实践

如果一天要拉起 300 万个隔离执行环境这个系统到底会踩多少个坑我先说结论它不会死在“能启动容器”这件事上而是死在那些你以为不是问题的细节上——比如文件系统孤儿、连接跟踪表溢出、一次镜像损坏导致全集群冷启动。我这么说不是凭空吓唬人。Agent 基础设施和普通后端服务最大的区别在于沙盒不是“偶尔用一下”的附属品而是整个系统的执行底座。模型负责思考沙盒负责动手。动手的机会一多底座的问题就会成倍放大。这篇文章我想从工程角度拆一拆一个稳定承载每天 300 万次沙盒创建的基础设施真正难在哪些地方哪些坑是我反复踩过以后才想明白的。适合看这篇文章的朋友包括正在做 AI Agent 平台的人、准备接 Agent 执行能力的后端团队以及所有对“大规模隔离执行环境”有好奇的开发者。我不会去复述概念只聊工程现场。1. 每天 300 万沙盒先搞清楚数字背后的形状1.1 沙盒是 Agent 的“手”不是可有可无的附件Agent 不能只靠模型输出文本它要写代码、跑命令、访问网页、处理文件、调用外部工具甚至要把一整条链路的结果返回给用户。问题在于模型输出天生不可预测你无法保证它不会执行恶意命令也无法保证它不会因为读到一个网页内容就突然“被诱导”去做危险操作。沙盒在这里扮演的角色不只是一个隔离环境更是 Agent 的“手”和“边界”。所有不可信的代码执行全部被关在沙盒里外部系统通过 API 或者消息队列与沙盒交互宿主机和内部服务不直接暴露给沙盒进程。这是 Agent 平台最基础也是最关键的一条安全边界。但沙盒又不能设计成完全封闭的黑盒。Agent 很多时候需要联网搜索、访问后端工具、读写临时数据所以网络策略、数据卷、生命周期管理都必须被纳入同一套基础设施来设计。这就是为什么 Agent 沙盒比传统 CI 里的构建容器要复杂得多。1.2 三百万日活沙盒从调度视角看是什么概念先用一个粗算理解规模300 万除以 86400 秒大约是每秒 35 个沙盒需要被创建或启动。峰值往往是平均值的 5 到 10 倍也就是说系统要能扛住每秒一两百个新沙盒的创建压力。如果单个沙盒的平均存活时间是 10 秒集群里任意时刻都有数千个沙盒在同时运行如果你把存活时间放到分钟级同时在线量会到上万。这个量级对计算资源来说并不算惊人真正棘手的是“创建频次”。每创建一个沙盒背后至少涉及调度决策、网络隔离、文件系统准备、进程启动、健康检查、日志接入这几步。每一步多花 10 毫秒在 300 万次规模下就是额外 30000 秒的资源占用。別看单次延迟小放大到日维度后你就能理解为什么传统“先拉镜像再启动容器”的流程完全撑不住。另一个容易被忽略的点是Agent 沙盒不是均匀分布地跑完就结束。它会因为用户对话、定时任务、批量数据处理出现明显的洪峰。基础设施必须做好削峰填谷而不是期望流量天然均衡。2. 快、稳、隔离沙盒运行时的反复拉扯2.1 镜像和文件系统慢的问题比你想的更底层沙盒要执行代码首先得有一个完整的根文件系统。最笨的做法是每个沙盒从镜像仓库拉一个完整镜像但拉镜像的耗时和带宽成本在 300 万日活下直接会变成灾难。所以必须使用分层镜像、本地缓存和写时复制机制来减少重复读取。在实际落地时我会把“基础镜像层”长期缓存到节点本地每个沙盒只在数据层上挂一个很小的可写层。可写层的创建方式很有讲究如果直接用文件系统 reflink 复制耗费时间和目录大小有关如果用块快照创建速度会更快但需要提前处理快照在存储上的生命周期。这里最容易踩坑的是沙盒写入大量临时文件后删除时并不能让底层块设备空间立刻恢复残留的孤儿数据会逐渐吞掉磁盘。我见过磁盘空间明明没满但 inode 被占满导致整个节点无法创建新沙盒的情况最后只能靠异步巡检和过期数据回收来兜底。还有一个关键点沙盒镜像尽量保持“小而不缺”。镜像太大冷启动时节点从本地缓存加载到内存的时间会拉长镜像太小Agent 缺少常用库又会导致运行时频繁报错。我在项目里通常会把 Python、Node、Shell 这类基础运行环境打进同一个底座镜像再按任务类型挂载不同的工具层避免每个任务都从零起一套环境。2.2 池化与预热冷启动不能作为默认路径如果每个请求都走完整初始化链路哪怕优化到 500 毫秒也会让用户明显感觉到“卡顿”。更现实的做法是维持一个预热沙盒池提前创建好网络命名空间、挂载好文件系统、完成资源配额设置但进程先不启动。用户请求到达后只需要把任务进程放进池子里就可以在几十到几百毫秒内完成交付。池化管理需要特别注意三个问题池子水位、空闲回收和状态污染。水位太高浪费资源水位太低遇到突刺峰又来不及补充所以要有基于历史流量预测的动态补池机制。空闲回收是最容易忽略的一个沙盒如果长时间空转它占用的内存在大集群里会被放大得很严重回收时必须区分“还没分配任务的空沙盒”和“正在执行的活跃沙盒”不能误杀。状态污染是安全层面的隐患。沙盒每次执行结束后文件系统、进程列表、内存中都可能残留上一次任务的数据。我的做法是每次任务结束直接销毁这个沙盒而不是复用它除非沙盒本身完全不可变且没有数据写入。听起来浪费但在安全性要求下这是最可靠的策略。2.3 隔离强度不是每个沙盒都该用同一个安全等级Linux 容器的隔离依赖于内核机制包括命名空间、cgroups、capabilities、seccomp。这套组合能挡住绝大多数普通恶意行为但它和虚拟机有本质区别如果内核存在漏洞容器内进程是有可能逃逸到宿主机的。Agent 场景里我们面对的可能是模型生成的不可信代码这比传统后端服务面临的用户输入更危险因为你无法预测它会执行什么系统调用。我在实际的架构里会把隔离等级分成几档。低风险任务例如简单的文本处理、数学计算可以用常规内核隔离中风险任务例如依赖外部网络、需要安装软件包的代码执行要开启 seccomp 过滤和更严格的资源配额高风险任务例如涉及敏感数据或者要访问外部服务器我宁愿用轻量虚拟化技术或者把它调度到专用的隔离节点上而不是赌内核不会出问题。这里想强调一个常被忽视的原则隔离设计一开始就要假设“沙盒一定会被攻破”。在这个前提下你才会认真做宿主机的网络收敛、进程监控、特权账号保护。如果一上来就相信内核隔离是万能的那等逃逸事件真的发生时损失会远超预期。3. 调度与容量管理百万级短任务的宿命之战3.1 资源账单和装箱策略CPU 能超卖内存不能玩火沙盒调度的第一步是给每个沙盒定义资源占用模型。不能只说“给 512M 内存和 1 核 CPU”因为 Agent 任务可能持续几秒也可能持续几分钟一些任务是 CPU 密集另一些是 IO 密集。资源配额设置得太宽单节点能放的沙盒数量就少设置得太窄任务容易被打死。以一台 64 核 128GB 内存的节点为例如果每个沙盒分配 0.1 核、256MB 内存理论上可以放 640 个沙盒但实际调度时还必须考虑内存页缓存、内核线程、conntrack 表、临时文件系统缓冲这些都占资源。我给节点容量定的经验值是留 20% 到 30% 的余量尤其是在内存上绝不做刚性超卖。CPU 可以适当超卖因为大多数沙盒不会持续吃满 CPU但内存一旦超卖OOM Killer 会随机杀进程那对在线业务来说就是事故级别的影响。对超卖我需要补充一句内存超卖并不是完全不能做但必须有非常精确的回收机制和任务优先级。如果你能定义清楚“哪些任务可以被杀掉哪些绝对不能”才有可能在不触发雪崩的前提下提升密度。否则宁可多留一点资源也不要拿稳定性和安全性换那点容量。3.2 队列、限流与幂等不能把压力全部打在运行时上在线业务里如果每秒有上百个创建请求直接打到集群调度器它会先被一堆琐碎操作拖垮。正确的做法是在前面加一层任务队列做流量整形和优先级排序。队列可以适当容忍等待但必须给客户端一个明确的排队状态不能让人干等却不知道发生了什么。队列之后才是调度器。调度器负责决定任务进哪个节点池、哪个节点、要不要等待资源释放。这里很容易出问题的点有两个一个是同一用户或同一会话产生大量请求必须做租户级限流另一个是客户端超时重试如果服务端没有幂等处理同一个任务可能会被创建出多个沙盒造成重复计费和资源泄漏。我们当时用了一个很简单的组合键来保证幂等会话 ID 任务序号 尝试次数。服务端遇到重复创建请求时要么返回已有沙盒的连接信息要么直接拒绝并把原因写进响应。这个设计虽然不起眼但在故障演练时救回了整条链路否则只要网络抖动一次重试风暴就能把集群打爆。3.3 分池与混部不同类型任务不要互相伤害Agent 任务类型完全不一样有的是毫秒级返回的简单工具调用有的要执行一个十分钟的数据处理脚本还有的要启动浏览器做自动化测试。把这三类任务放在同一个节点池里大概率会互相干扰。短任务怕长任务占满 CPU长任务则容易因为短任务频繁调度导致缓存抖动。我建议按任务语义拆节点池快速任务池、长任务池、高 IO 池、不可信任务池。快速任务池用池化沙盒追求低延迟长任务池可以允许创建更多沙盒但单个存活时间长高 IO 任务单独部署避免大量磁盘读写影响其他沙盒。不同池之间按照服务等级设置权重必要时允许短任务临时借用长任务池的空闲资源但借用必须加上抢占退避不能把正在执行的长期任务直接杀掉。缩容和故障恢复也是调度的一部分。节点要下线时不能粗暴地杀掉全部沙盒。正确做法是先标记拒绝新任务再进行优雅排空等待已执行任务完成或超时后再销毁。这个过程要有进度显示否则运维人员很难判断节点什么时候可以真正下线。4. 网络与数据边界沙盒能上网但不能“什么都访问”4.1 命名空间、IP 与连接跟踪网络是隐形瓶颈每个沙盒都应该有自己的网络命名空间和独立的 IP 地址不能让所有沙盒共享宿主机的网络栈。出站访问通过 SNAT 共享集群出口 IP入站访问默认全部关闭。这一步决定了沙盒之间彼此不可见也决定了沙盒无法直接攻击宿主机上的服务。但大规模沙盒场景里网络层最常见的瓶颈不是带宽而是连接跟踪表。每个出站连接都会在节点上留下一条 conntrack 记录如果沙盒里的 Agent 频繁请求外部 API一个节点上几千个沙盒同时建连很快就能把连接跟踪表塞满。表现就是新连接突然大量失败服务看起来像“网络故障”实际是内核在悄悄丢弃连接。要解决这个问题需要在几个层面同时下手一是通过出口代理做连接复用避免每个沙盒都直接建连外部二是延长空闲连接超时时间减少重建频率三是给沙盒设置出站连接数上限和速率限制防止某个异常任务引发全局网络风暴。光靠加节点不解决根因因为负载一上来同样的表项问题还会再出现。4.2 出口策略内网资源不能成为沙盒的后花园Agent 任务有时确实需要访问外部网络但这不意味着它需要访问你公司的内网。更危险的是沙盒里的进程可能被人诱导去请求云厂商的元数据服务拿到临时凭证再进一步访问存储、数据库等敏感服务。这个风险在普通后端服务里都要防在 AI Agent 里因为模型可能主动发起网络请求风险会被放大很多。我的默认策略是沙盒网络出口全部经过正向代理默认只允许白名单域名。对必须访问的公网资源开放白名单对内部网段一律阻断。如果任务需要访问内部 API那也不能直接把内网地址暴露给沙盒而是通过代理层插入短期令牌由代理负责任的转发响应。这样即使 Agent 生成了恶意命令它也没有机会直接“看到”内网。DNS 过滤同样重要。很多攻击链的第一步是外带数据最常见的办法就是对攻击者控制的域名发起 DNS 请求。我在出口代理层做了域名级审计和可疑域名拦截同时要求沙盒进程禁止直接解析内部 DNS 记录。4.3 数据残留与临时凭证沙盒不保存秘密沙盒执行环境里难免会写入文件、下载数据、产生缓存。如果销毁沙盒只是删掉磁盘文件数据其实还有恢复的可能。安全要求高的场景我会用加密磁盘或者内存文件系统沙盒销毁时直接丢弃密钥让所有写入数据不可逆地消失。这个做法会牺牲一部分性能和存储成本但换来的保证是“会话结束数据归还”。临时凭证也是一个高频翻车点。不要把数据库密码、云厂商凭证、API Key 之类的东西写进沙盒的配置文件。需要给沙盒授权时就用一次性令牌注入到进程环境变量里并且令牌的有效期只能覆盖任务的生命周期任务一结束就立即失效。这样即使沙盒被攻破攻击者拿到的也是一个几乎马上过期的凭证无法横向移动。5. Agent 场景特有的安全难题不是普通容器安全5.1 提示注入会改变威胁模型普通后端服务的安全威胁大多来自外部输入而 Agent 它还多了一个脑洞大开的“内部输入”模型的输出本身就是不可信输入。模型可能会因为读了某个网页或者被一段精心构造的文本所诱导生成一个看起来正常但实际在下载恶意文件的 shell 命令。我在做基础设施时不只是把沙盒当一个执行工具而是把它当成提示注入攻击的最后一道防线。什么意思呢模型可能会被骗但沙盒可以用最小权限原则限制命令的影响面。比如不需要读写宿主文件的任务就只给临时目录的写权限不需要安装软件的任务就禁止包管理器执行不需要访问网络的任务就彻底断网。这样即使模型被骗它实际能做的破坏也被限制在安全边界以内。这里要提醒一句不要把安全完全押在内容审核或模型行为检测上。内容审核可以漏检模型行为可以异常但一个配置正确的沙盒是确定性系统它在默认情况下就会拒绝越权行为。你必须把沙盒的安全属性当作产品功能来维护而不是善意的附加项。5.2 最小权限的内核配置建议内核隔离在 Agent 场景会用得非常重但配置项必须逐条验证不能守着默认设置不放。我比较常用的安全加固包括用用户命名空间把容器内的 root 映射成低权限用户移除所有特权能力通过 seccomp 只保留任务真正需要的系统调用设置 no_new_privs隐藏宿主机上的敏感挂载点将根文件系统挂载为只读仅给临时目录可写权限。系统调用裁剪要说清楚不是直接把 syscall 禁到“能用就行”的程度。Agent 要执行 shell、创建子进程、访问网络它需要的系统调用往往比较多。我的做法是分场景做模板例如“纯计算模板”“网络工具模板”“代码沙箱模板”每个模板对应一份不同的 seccomp 配置再在运行时结合任务等级做动态选择。Capabilities 的裁剪也容易被忽略。默认容器如果不显式处理会保留很多当前任务根本用不到的能力。我会把它们全部 drop再按需添加比如需要绑定低端口才加 net_bind_service。这个动作本身很简单但能极大地降低内核提权攻击的风险面。5.3 镜像供应链与运行时风险镜像也不是天然可信的。如果沙盒要执行任务的代码那它依赖的基础环境、工具包、Python 包全都可能成为供应链攻击的入口。我的处理方式包括镜像仓库开启签名校验拉取时锁定摘要依赖锁定到精确版本不允许模糊匹配定期扫描已知漏洞高危镜像直接阻断使用。运行时同样需要监控。我建议在沙盒内安装轻量行为探针记录进程树、出站连接、文件变更和资源消耗。出现异常行为时不一定要立即杀死沙盒可以先标记任务状态让调度器决定是否隔离或重试。这个功能在排查用户报障时会提供极大的帮助否则你很难解释为什么一个看似正常的任务会突然飙高 CPU 或连接敏感服务。6. 可观测性几百个指标里找到那根断掉的针6.1 事件流比日志更重要沙盒基础设施的可观测性不能只依赖 stdout 日志。因为沙盒生命周期短、数量大如果每个沙盒打一大堆调试日志采集系统会被淹没。我更建议把沙盒生命周期关键节点设计成结构化事件流创建请求到达、排队等待、分配节点、挂载文件系统、进程启动、退出码、销毁原因、回收耗时。每条事件流都必须带上会话 ID、任务 ID、沙盒 ID方便追踪一次 Agent 会话的完整执行链路。日志可以有但日志的作用是排障时的补充证据而不是主要观测数据。我在项目里发现很多“用户任务失败”的问题在事件流里几秒钟就能定位但要靠日志找的话得翻好几个文件。采集环节还要考虑削峰和降采样。300 万次创建的现场日志不可能全部实时入库我的做法是把事件写到本地文件再由独立的采集进程批量上报。如果真的遇到采集服务故障宁可丢弃一部分非关键日志也不能因为日志堆积反过来压垮沙盒节点。6.2 故障批量出现时别让用户跟着一起“车祸”基础设施最常见的事故是同时大批量失败某个节点内核问题导致几十个沙盒被杀或者某个镜像仓库抖动导致所有冷启动失败。这里需要设计两类故障处理机制一类是快速失败另一类是可控重试。快速失败指的是当任务已经排队超过配置的等待时间就不要让用户继续等直接返回超时错误让上层决定是降级还是重新调度。可控重试则是说对于失败的批量任务不能让它们在一瞬间全部重新打到集群里要按指数退避的方式分批恢复。没有这两层设计任何一次节点故障都可能引发连锁反应最后变成“故障后重试风暴再次打垮集群”。同时我还强调租户级隔离。不能让一个用户的批量任务故障把所有用户都拖累。每个租户有自己的并发上限和排队队列单个租户抖动时其他租户的请求仍然能正常调度。这有点像给整个系统做了一套“电路保险丝”它会牺牲掉一部分坏了的分支但保住主干。6.3 成本不是“记账”问题而是观测维度到了这个规模沙盒成本已经不是一个财务话题而是一个容量规划话题。如果你不能回答“每个任务平均消耗多少沙盒秒”“哪些模型版本的任务运行时间异常长”“哪个租户的沙盒密度最低”你就没办法做有效的容量预测。我会要求全链路记录资源用量CPU 时间、内存峰值、磁盘读写、网络流量、沙盒销毁时的实际存活时长。这些数据按任务类型、模型版本、租户维度聚合每周复盘一次。你会发现很多平时注意不到的现象比如某类任务的模型会把同一个操作重复执行三次看似没增加用户等待时间实际上沙盒成本翻了三倍再比如某些任务从不使用网络却默认开通了公网出口白白占用带宽和连接跟踪容量。有了成本观测优化才不是拍脑袋。你给用户降成本的时候能精确指出是哪里贵、贵了多少、改哪里能省下来。这对长期运营一个高密度沙盒集群非常关键。7. 如果让我重来一次我会抓的重心7.1 先把一次执行做到可观测、可恢复再谈扩大规模很多团队做 Agent 基础设施一开始就追求“每天百万级”的大目标却忽略了单次执行是否足够可靠。我会先把沙盒创建、任务执行、结果返回、销毁回收打成一个最小闭环先让几百个用户用顺手再逐步放大到上千、上万、百万。扩量级的前提不是把调度算法做得更复杂而是把每次失败的原因和恢复路径搞得足够清晰。一次执行如果能做到失败后自动重试、数据不残留、用户能明确看到卡在哪一步这个系统才配得上“基础设施”四个字。否则调度再高效也只是把故障搬得更快而已。7.2 永远不要假设沙盒不会被攻破我会在团队内部反复强调一个原则沙盒逃逸不是低概率黑天鹅而是系统设计时必须面对的常态风险。这意味着你要默认宿主机的隔离边界可以被突破要提前把宿主机的最小权限、网络收敛、特权管理做好同时定期做逃逸演练。我见过一些团队把大量精力放在优化镜像启动速度上却忘了检查沙盒默认权限配置最后被一个简单的命名空间逃逸攻击试穿。优化速度只是效率问题隔离失效是安全事件。效率可以慢慢优化安全底线必须一步到位。7.3 如果继续往下做我会先动隔离等级自适应现在让我接手下一个版本的 Agent 基础设施我不会急着出新调度算法也不会先换存储引擎而是优先做风险分级与隔离等级自适应。简单说就是让调度器根据任务的敏感程度、代码来源、网络需求自动选择轻量内核隔离还是重量级虚拟化隔离。这个方向的价值很直接九成普通任务根本不需要最顶级的隔离用太重的手段只是浪费成本但所有任务都只用一层轻量隔离出了事又兜不住。按风险分级后既能保住安全底线又能释放出大量资源给真正需要高性能的场景。这个思路还没有一个统一标准但我会愿意在里面投入最多时间因为它同时解决成本、安全和规模三个问题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →