用Firecracker自建Agent沙箱集群:架构设计与生产实践

发布时间:2026/10/20 22:17:46
用Firecracker自建Agent沙箱集群:架构设计与生产实践
跑 Agent 跑得多了就会开始琢磨代码执行沙箱到底该怎么搭。我最近在做的这套方案是用 Firecracker 自建一个 Agent 沙箱集群核心由三块组成状态机负责管理沙箱从创建到销毁的全生命周期文件管理解决代码、数据和根文件系统的读写与持久化进程编排则处理任务如何被安全地跑起来并回收结果。之所以不直接用现成的容器方案是因为 Agent 场景里执行的代码完全由模型生成什么幺蛾子都可能写出来对隔离边界的要求比普通微服务高一个量级。这篇文章把我自己的设计思路、实现细节和生产化过程中的踩坑记录都摊开讲给同样在做 Agent 基础设施的同学一个参照尤其是想绕过闭源商业平台、自己掌控沙箱能力的情况。1. 为什么 Agent 沙箱要用微虚拟机从痛点到选型1.1 隔离边界的真实需求先说结论Agent 沙箱最大的痛点是“隔离要硬、启动要快、密度要高”这三件事在传统技术栈里很难同时满足。Docker 这类容器共享内核隔离边界靠命名空间和 cgroups。好处是启动飞快启动时间以毫秒计算资源开销几乎为零但一旦内核本身存在可利用漏洞容器内拿到的权限很容易被放大成宿主机权限。也许日常跑自动化测试没什么问题但在 Agent 场景里模型会按提示词写出各种读文件、连网络、执行系统调用的代码你没法预判它是良性还是恶意只能默认所有代码都不可信。传统虚拟机隔离得很好但一个完整 Guest OS 的启动是以秒为单位计算的内存开销动辄 1GB 起步。如果沙箱集群需要支撑上百个 Agent 并行资源占用立刻变成瓶颈。假设一个任务的平均执行时间是 30 秒单机支撑 50 个并发沙箱用传统 VM 方案光内存就得准备 50GB 朝上这个成本放在任何团队里都很难接受。Firecracker 这类 microVM 方案把开销和启动时间压缩到了接近容器的量级同时保持硬件虚拟化隔离边界。我实际测试的体感是分配 256MB 内存的 microVM冷启动大概在 300-500ms 区间去掉内核加载就是几百毫秒。这已经可以支撑“按需拉起、用完即销毁”的沙箱池。所以我的选型结论是传统容器适合弱信任场景传统 VM 适合重量级场景而 Agent 代码执行沙箱这种高频创建销毁的强隔离需求microVM 是当前最合理的折中。1.2 几个主流隔离方案的横向对比我在设计初期整理过一张对比表按自己实际测试的数据整理。要说明的是不同机器、不同内核版本、不同镜像大小下数字会有浮动但相对量级可以参考方案隔离边界冷启动时间单实例额外内存并发密度适合场景传统容器内核共享毫秒级极小非常高内部测试、构建缓存用户态内核拦截系统调用拦截层百毫秒级几十 MB高轻度不可信代码虚拟化容器硬件虚拟化百毫秒到秒级几十 MB中高较重隔离需求microVM硬件虚拟化百毫秒级数 MB 到几十 MB高高频不可信代码执行选择 microVM 而不是虚拟化容器还有一个原因是依赖可控。虚拟化容器方案为了兼容容器生态往往要引入额外的运行时组件而 Firecracker 本身就是一个封闭的管理面 精简 VM 模型没有复杂的容器编排耦合。如果你的目标就是做 Agent 代码执行沙箱而不关心通用容器调度那这套模型反而更干净。另外microVM 的强隔离还体现在设备模型上。Firecracker 默认暴露给 guest 的设备很少没有传统模拟设备那种庞大的攻击面。这意味着 guest 里就算拿到 root能做的破坏也被限制在单个 VM 内这对跑不可信模型代码来说非常关键。2. 沙箱集群的总体架构与状态机设计2.1 控制平面与数据平面分离沙箱集群的架构通常分成控制平面和数据平面。控制平面是一组无状态服务负责接收任务、调度沙箱、维护状态机数据平面是实际运行的 Firecracker microVM 集群。两个平面必须分开因为沙箱会被反复创建销毁任务队列和沙箱元数据可以放在持久化存储里控制平面只要保存当前状态机的各个实例如果某个调度节点挂了另一个节点可以接管未完成任务重新从状态机的终止状态出发做补偿而不至于丢状态。数据平面则不应该有太多逻辑。microVM 内部只需要一个最小化的 Agent 进程通过 vsock 与控制平面通信接收任务指令、执行进程、回传 stdout/stderr、上报退出码。这种设计把“在沙箱里跑东西”抽象成一个简单的 RPC 接口宿主机侧完全不知道任务具体内容需要拿结果时再向 Agent 查询。控制面和数据面通过一个稳定协议通信我直接用 JSON 作为序列化格式简单可靠排查问题时抓包也方便。在部署层面每台宿主机上可以同时跑几十个 microVM但每台机器要限制上限否则 CPU 超卖严重时反而影响整体吞吐。我的经验是一台 8 核 16GB 的机器保留 1 核 2GB 给宿主机自身运行剩下的资源跑 10-16 个 256MB 沙箱比较稳妥。这个数字不是拍脑袋定的而是压测后发现在更高密度下任务执行时长会出现明显的尾部延迟放大。2.2 状态机的状态定义与迁移规则沙箱生命周期我划分为六个状态Pending、Booting、Ready、Busy、Stopping、Terminated。每个状态都对应一组资源和外部依赖比如 Booting 意味着可能已经创建了套接字或磁盘文件Busy 意味着guest 里有一个正在运行的进程树。Pending任务已申请沙箱但还没有进入调度队列此时不分配任何宿主机资源。一个任务如果长时间停在 Pending可能是调度器资源不足需要打告警。Booting调度器选了宿主机开始创建 microVM、挂载镜像、启动 Agent。这是资源最紧张的状态因为一旦开始创建释放动作就必须跟上。ReadyAgent 已经和宿主机建立通信链路沙箱空闲可接任务。Ready 状态的沙箱是热备资源可以预创建一批。BusyAgent 正在执行某个任务这个状态下不再接受新任务。如果任务执行超时仍然要停留在 Busy直到回收流程介入避免出现边执行边销毁的竞争条件。Stopping执行完成或超时开始回收资源。这时先向 Agent 发送优雅退出指令等待其保存临时文件如果超时直接强制销毁。Terminated资源完全释放元数据保留一段时间用于审计。只有 Terminated 之后才能安全地删除磁盘镜像和日志文件。状态机的迁移规则必须严格单向。比如只有 Ready 可以进入 Busy只有 Busy 可以进入 StoppingPending 不能被跳过到 Ready。为什么这么严格因为 Agent 沙箱一旦启动就会有网络端口、磁盘分区、进程树等外部副作用如果状态跳变不收敛垃圾回收就会漏掉资源。我调试中遇到的大部分故障都来自状态跳变不合法比如直接从 Pending 去 Terminated导致底层 microVM 没被销毁宿主机资源被慢慢吃光。3. 文件管理从镜像引导到运行时文件读写3.1 根文件系统的构建与裁剪文件管理的第一层是根文件系统。好的 rootfs 应该越薄越好我一般用最小系统构建工具生成基础环境再不断裁剪掉文档、包管理器缓存、系统日志和语言包等没有运行价值的内容最后打包成 ext4 镜像。镜像大小直接影响启动速度。我通常控制在 150MB 左右太大的镜像会让磁盘读取时间变成启动瓶颈太小则容易在跑 Python 代码时缺少依赖。内核方面要选支持 KVM 的 vmlinux 镜像。内核配置不能默认全开要按 Agent 执行代码的真实需求裁剪比如去掉不必要的文件系统驱动和网络协议栈。裁剪内核还有一个额外好处就是减少了 kernel 本身可能被利用的攻击面。guest 里跑不可信代码内核越精简越不容易找到意想不到的系统调用路径。文件管理还有一层容易被忽略可写分区的问题。我建议根设备只挂载为只读guest 的所有临时写入走 tmpfs。这样即便任务崩溃或者被注入恶意行为也不会污染镜像文件。宿主机回收时直接删掉镜像副本即可不需要做写时复制或快照回滚。这种“只读根 临时写”的思路和很多容器镜像管理实践是一致的但在 VM 场景下更刚需因为块的粒度更大回滚成本更高。3.2 运行时文件注入与持久化策略运行时的文件读写我分成三类临时文件、沙箱私有数据、外部输入文件。临时文件放 tmpfs销毁不心疼沙箱私有数据指 Agent 任务写出来的中间产物通过宿主机的数据目录做持久化外部输入文件则通过 API 上传管理服务先存入宿主机某个目录再以只读方式挂载给 guest。为什么不对每个 microVM 做独立的块设备因为块设备创建和快照的开销大文件级别管理更灵活。把“沙箱的持久化”和“沙箱的临时执行”分离开是提升集群资源利用率的关键。比如一批任务需要同一个数据文件宿主机构建好文件后一次性注入多个沙箱远比每个沙箱各拉一个副本高效。另一个好处是备份和审计简单宿主机的数据目录可以直接纳入日志系统统一管理。这里要注意文件权限问题。guest 内 Agent 通常以非 root 身份运行但宿主机写入数据目录时用的是宿主机用户两者用户 ID 不一定一致。我踩过几次坑之后干脆把宿主机数据目录的属主改为固定 UID并在 guest 内创建相同 UID 的用户。这样两端文件权限完全对齐不会出现“文件写得进去读不出来”的怪问题。如果不改 UID靠 chmod 777 勉强也能跑但在生产环境里权限太粗一旦某个任务被攻破可能把其他任务的数据也读走。4. 进程编排如何安全地执行 Agent 任务4.1 Guest Agent 的设计思路有了 microVM有了文件还缺最后一块怎么安全地执行任务。Firecracker 本身不管“进程”它只提供硬件虚拟化边界所以必须在 guest 内部放一个 Agent。这个 Agent 是宿主机与沙箱内命令执行之间的唯一桥梁。我把它做成一个常驻服务启动时通过 vsock 主动向宿主机注册之后一直监听任务请求。为什么不直接从宿主机往 guest 里塞命令因为那样等于绕过了 Agent 的安全逻辑任何直接 shell 注入都可能发生。Agent 收到任务后用操作系统的进程创建接口启动子进程把 stdin、stdout、stderr 分别接到管道里用独立进程组管理任务控制平面再异步读取输出直到进程退出。这里有个容易忽略的点不能直接在 Agent 里用 system() 这类接口执行拼接命令因为模型生成的代码如果包含分号、管道、重定向会把一次命令变成多个进程进程组一旦失控回收就会出问题。我自己的做法是让 Agent 支持两种执行模型一种是执行整段 shell 脚本另一种是把脚本内容作为参数传给一个受控解释器两种都必须在独立 PID 命名空间和受限的 cgroup 里跑。这样即使脚本炸了也只能炸在它自己的进程组里。4.2 超时回收与资源限制的配置细节超时回收机制是进程编排的兜底。控制平面对任务设置默认超时比如 120 秒。超时后先发送 kill 信号再等 3 秒如果进程组还没有退出Agent 就向宿主机上报失败宿主机直接把整个 microVM 销毁。不要相信一个跑偏的进程会自己退出超时保护是沙箱回收的最后一道防线。资源限制也要细分。CPU 限制我不用固定配额而是用相对权重因为每个 Agent 任务的 CPU 需求波动大固定 0.25 vCPU 可能在复杂计算时跑不动固定 2 vCPU 又浪费。内存限制则严格很多guest 内设置 cgroup 内存上限超过限制直接触发 OOM。磁盘限制最容易漏如果任务往 tmpfs 或数据目录写大量文件磁盘空间会快速耗尽最终影响宿主机稳定性。我在数据目录所在文件系统上设置了项目级配额每个沙箱最多写固定容量超出即拒绝写入并上报任务失败。还有一个细节是进程退出后的清理。Agent 需要主动 reaping 僵尸进程避免子进程退出后被父进程遗留占用 PID。在沙箱这种短生命周期场景里僵尸进程带来的问题通常不太明显但当任务较长且不断 fork 子进程时PID 耗尽会让任务莫名失败。我在 Agent 里加了一个简单的子进程回收循环每处理完一个任务就统一次回收所有已结束的子进程。5. 实操一个最小可用的沙箱集群骨架5.1 环境准备与 Firecracker 启动实操部分我按自己搭的最小可用集群来讲。硬件上需要一台支持 KVM 的 Linux 机器软件需要内核镜像和根文件系统镜像以及 Firecracker 二进制。启动 microVM 的配置我用 JSON 描述下面是一个最小编制配置分配 256MB 内存和一个 vCPU{ boot-source: { kernel_image_path: /srv/sandbox/vmlinux, boot_args: consolettyS0 rebootk panic1 pcioff }, drives: [ { drive_id: rootfs, path_on_host: /srv/sandbox/rootfs.ext4, is_root_device: true, is_read_only: false } ], machine-config: { vcpu_count: 1, mem_size_mib: 256 } }启动时我把配置文件写到临时目录然后用命令拉起进程。启动后通过 socket 文件和控制面通信整个流程可以由宿主机上的守护进程管理。要特别留意的是每次启动 microVM 前必须先确认宿主机节点有足够的空闲内存和 CPU 配额否则多沙箱同时启动时会互相争抢资源导致启动时间剧烈抖动。5.2 状态机与任务执行的代码骨架状态机的代码骨架我会用一个很小的类来维护状态和迁移合法性。下面这段代码表达了核心逻辑实际项目中还需要加上持久化和并发锁。用 Python 写只是因为我开发效率高生产环境可以换成其他语言逻辑是一样的class SandboxState: def __init__(self): self.state Pending self.allowed { Pending: [Booting], Booting: [Ready, Stopping], Ready: [Busy, Stopping], Busy: [Stopping], Stopping: [Terminated], Terminated: [], } def transition(self, target): if target not in self.allowed[self.state]: raise ValueError(finvalid transition: {self.state} - {target}) self.state target任务执行链路则分成两步。宿主机控制平面通过 vsock 向 guest Agent 下发任务对象Agent 收到后创建子进程并回传输出。我用一个简化的命令执行函数示意def run_command_in_guest(connection, command, workdir, env, timeout): connection.send({ type: run, command: command, workdir: workdir, env: env, timeout: timeout }) output read_output_with_timeout(connection, timeout) return output这里的关键是 read_output_with_timeout。必须在宿主机侧同时监听 stdout、stderr 和退出码任何一个管道阻塞都会导致整个任务卡死。我倾向于把 stderr 合并到 stdout 一起传回减少宿主机侧的状态复杂度如果需要区分再在消息里加一个字段标记管道类型。6. 生产经验校准从 E2B 这类平台学到的教训与调优6.1 沙箱池化而不是每次冷启动为什么这个项目的标题里特意提到生产经验校准因为我在做生产化改造时对照了不少成熟沙箱平台的设计思路尤其是 E2B 这类 Agent 执行环境平台。有几个方向是绕不过去的。最核心的一点是不要让每个任务都走冷启动。E2B 这类平台用“按需沙箱池”的方式来满足低延迟也就是维护一个空闲沙箱队列任务来时直接取出一个 Ready 状态的 microVM。冷启动永远只发生在扩容阶段而不是请求阶段。我自己做了一批空闲沙箱热备实测 p95 延迟从 1200ms 降到了 300ms 以内。实现方式很简单控制平面监控 Ready 状态的沙箱数量低于阈值就主动创建新的 microVM 补充高于阈值就销毁多余的空闲沙箱。需要注意热备沙箱也不是越多越好因为每个空闲沙箱仍占内存。用“最低空闲数量 最大空闲数量”这个经典策略管理既能保持低延迟又不至于把宿主机资源耗死。6.2 文件、依赖与资源单位的调优建议第二个坑是依赖管理。E2B 这类平台通常会让镜像预装常用依赖沙箱不会无限制容纳运行时的各种安装操作。如果 Agent 任务需要 apt install 或者 pip install更可靠的做法是在创建镜像时预装或者在任务完成后直接销毁沙箱而不是基于长时间运行的沙箱去累积状态。因为每次安装都会产生文件系统写入累积起来不仅拖慢任务执行还会让镜像越来越大最终把启动时间拉长。资源单位也需要调整。现实中一个 Agent 任务大多几十秒执行完分配的 CPU 周期和内存应该按“任务”而不是按“沙箱常驻时长”评估。控制平面调度时应该尽量把处于 Busy 状态的沙箱集中到少数几台宿主机把 Ready 状态的沙箱打散到多台机器这样既能提高真实利用率又能在某台宿主机宕机时快速扩容其他节点的空闲池。第三是关于错误恢复的冗余设计。生产环境中microVM 本身也可能挂掉这时候不能依赖状态机里的“正常回收”路径。我额外加了一个外部看门狗定期检查每个沙箱的存活状态如果发现某台宿主机上的 Agent 心跳中断超过阈值就直接把沙箱移入 Stopping 再置为 Terminated。这类异常恢复路径虽然平时很少触发但一旦触发能避免宿主机上堆满半死不活的僵尸沙箱。6.3 常见问题排查与避坑清单最后整理一份排查记录都是我在实际操作中遇到的问题按现象、可能原因、排查手段和解决对策四个维度列出来方便生产环境里快速定位现象可能原因排查手段解决对策microVM 启动变慢rootfs 过大或内核镜像过大统计镜像文件大小压测冷启动裁剪镜像检查磁盘 IOvsock 频繁重连guest Agent 异常退出或未启动进入 guest 查看进程列表给 Agent 加守护增加退出上报任务超时但 microVM 不销毁状态机卡在 Busy看门狗检查迁移合法性超时后强制把状态置为 Stopping 再 Terminated文件注入成功但读不到内容UID 权限不一致对比宿主机和 guest 的文件属主统一固定 UID避免 777 粗放授权CPU 争抢导致任务变慢单机 microVM 数量过多压测观察 p95 延迟变化限制单宿主机并发 microVM 数配置绑核还有两个小技巧比较有实战价值。第一个是镜像克隆要快我用的是在宿主机上用快速复制的方式生成 rootfs 副本而不是启动时全量拷贝 150MB 文件这样每个沙箱的启动时间能再省几十毫秒。第二个是日志要分层宿主机层的日志记录哪个沙箱什么时间创建销毁、状态怎么迁移guest 层的日志只记录任务执行输出两套日志分开存储排查问题时不会互相干扰。现在这套方案已经稳定支撑了内部的多个 Agent 任务场景状态机迁移、文件管理和进程编排三条线都经过了多轮生产环境验证。如果读者往下扩展可以考虑加入更细粒度的网络策略以及基于快照的沙箱秒级恢复能力这些都是后续可以继续折腾的方向。