资讯动态

基于Firecracker的AI Agent沙箱集群:架构设计与实践

发布时间:2026/10/15 10:25:29 来源:尧图企业网站定制
AI Agent 跑起来之后第一个让人头疼的问题就是执行环境怎么隔离。代码要能跑文件要能写进程要能复现但是宿主机的安全不能被一波 prompt 注入打穿。我们最初直接复用公司的 Kubernetes 集群给每个会话发一个 Pod跑了几周发现性价比和稳定性都撑不住GC 延迟一上来就是几秒、镜像拉取挤占带宽、回收不干净还经常残留僵尸容器。后来我花了三周时间基于 Firecracker 自建了一套 Agent 沙箱集群用状态机管生命周期、用分层文件系统管数据、用内置代理进程做编排最终稳定支撑了每天几十万次的任务执行。这中间还参考了 E2B 在生产环境暴露出来的一些设计取舍能避的坑都提前避了。这篇把整个方案拆开讲清楚从选型逻辑到具体参数再到排障经验给准备自己做 Agent 运行时的团队一个可落地的参考。1. 集群架构与选型思路为什么是 Firecracker1.1 Agent 沙箱到底需要什么给 Agent 做沙箱和传统微服务隔离是两回事。传统场景追求的是尽量小的开销、尽量多的实例密度但 Agent 沙箱有几个特殊需求第一每次任务的代码可能都是动态生成的需要秒级启动一个新的、干净的隔离环境第二Agent 会操作系统文件、安装软件包、生成图片音频这些状态必须能在任务生命周期内被持久化任务结束后又能一键回收第三Agent 的执行不是一次性函数调用而是有工具调用、有中断、有继续执行沙箱能和宿主机保持一条可靠的控制通道。这三个需求放在一起会直接把 Docker 这类面向单机容器部署的方案排除掉它确实能隔离但是做不到毫秒级启动大量全新实例而且镜像层、存储层的管理模型是给长跑服务设计的不适合高频创建和彻底销毁。真正合适的方向是微虚拟机——强调启动速度、极小的设备模型、但保留硬件虚拟化的隔离边界。1.2 Firecracker 与 Docker、gVisor、Wasm 的权衡我当时做了个小对比表把主流方案按启动速度、隔离强度、生态完善度打了一遍分方案启动速度隔离强度生态完善度资源开销备注Docker 容器百毫秒级内核共享、依赖系统调用过滤极高低适合常驻服务不适合高频销毁gVisor百毫秒级用户态内核、系统调用截获较高中高兼容性有坑重 IO 场景性能差WASM 运行时毫秒级基于能力模型中等很低但与真实操作系统环境差距大Firecracker 微VM125ms 左右硬件虚拟化、独立内核中等低需要自己拼控制面和存储方案Firecracker 选型纯粹是冲着两个硬指标启动速度和隔离强度。它不为生产虚拟化提供 BIOS、视频设备、USB 控制器这些用不到的设备全虚拟化设备只剩 virtio-net、virtio-block、virtio-vsock 三件套。设备模型越小启动越快攻击面也越小。每个 sandbox 都是一颗独立内核宿主机的内核崩溃不会传染到 Agent 执行环境对于执行不可信代码来说这是 Docker 给不了的保证。1.3 控制面与数据面分离的集群结构自建集群没有现成的控制面可用我按“控制面-数据面”的思路拆了三层这也是后来好维护的关键控制面负责接收 Agent 调度请求、维护沙箱状态机、分配 IP 和资源、下发启动配置。这层是一个无状态 API 服务可以横向扩展。计算面每台物理宿主机运行多个 Firecracker 微VM宿主机只暴露两类接口一是给微VM下发命令的 vsock 通道二是从控制面接收启动/销毁指令的 Agent API。存储面文件分三层管理镜像仓库只存只读 base 镜像沙箱运行时的写入落在宿主机上的临时可写层需要持久化的文件通过显式同步推到对象存储。三层之间完全通过 API 通信不搞共享内存、不搞分布式锁。状态全部记在控制面的数据库里宿主机只是无状态执行节点这样挂了任意一台机器控制面重新调度实例就能恢复。2. 沙箱生命周期状态机从冷启动到销毁2.1 为什么必须引入状态机一开始我图省事直接用数据库里的字符串字段记录沙箱状态比如running、stopped结果三天后就被现实教育了宿主机网络抖动导致创建超时、Agent 进程崩了需要重启、Jailer 启动报错各种异常路径交叉在一起running状态完全没法表达创建中但底层已经在清退这种半中间状况。状态之间迁移权限也口说无凭运维同学手动改数据库然后调度系统状态和实际运行状态就分叉了。状态机不复杂但是强制把合法的迁移路径固定下来后混乱局面上来就消失。我设计的状态集合如下沙箱状态集合状态含义provisioning控制面已收到请求正在分配资源、组装配置launching已调用 Firecracker API 启动微VM等待 guest 就绪ready微VM 启动完成vsock 通道可用等待 Agent 会话接入attachedAgent 会话已连接可以执行命令suspended会话超时被临时挂起微VM 保持运行但 CPU 受限terminating收到删除/过期请求正在保存收尾数据并清理资源dead微VM 已退出资源已释放等待记录归档这里我特意区分了launching和ready因为 Firecracker 的 REST API 返回 200 只代表 VMM 进程启动成功guest 内核还没跑完此时立刻去跑命令会撞上连接拒绝。很多早期集成的问题都是没做这个区分。2.2 合法迁移路径与超时设计定义好状态后迁移路径显式写出来只允许下面这些跳转provisioning - launching资源分配成功launching - ready内核启动、vsock 监听就绪ready - attachedAgent 会话完成鉴权和握手attached - suspended会话空闲或外部主动挂起suspended - attached会话续期恢复到可用状态ready/suspended/attached - terminating会话过期或收到终止命令terminating - dead资源清理完成整个状态机最核心的经验是每个迁移都必须有对应的超时。launching超时设成包含内核启动余量的值具体按镜像复杂度调节常见是 5~15 秒attached - ready/suspended的心跳检测用 30 秒间隔两次心跳丢失就进terminatingterminating最多等 10 秒10 秒后强制 kill 宿主机上的 VMM 进程。超时不是拍脑袋定的我先把 90 分位的冷启动耗时统计出来再把超时设置为它的 3~4 倍。这样既不会拿正常启动当异常也不会让故障恢复迟钝到用户可感知。2.3 异常路径失联、OOM、磁盘打满状态机定义了正常迁移路径生产环境更要紧的是异常路径怎么处理。我整理出三类高频故障分别给了兜底策略Guest 失联vsock 心跳连续两次超时先把状态置为suspended同时后台尝试一次 guest agent 探活如果探活失败则进入terminating。这套机制避免了一闪而过的网络抖动把整个沙箱删除。OOM微VM 进程 OOM 会导致整个 sandbox 直接退出。控制面收到 exit code 后把用户 session 内未同步的文件先同步掉然后直接进清理流程不回ready。OOM 场景不值得救因为内存状态已经不可信。磁盘打满运行时可写层一旦打满guest 里的文件读写全部报错。虽然状态机不感知磁盘我安排了每台宿主机上的一个小守护进程周期性对每个微VM 发df指令超过阈值就触发terminating避免盘被写爆影响同机其他沙箱。3. 文件管理镜像分层与可写层同步3.1 只读根 临时可写层的组合Firecracker 的块设备给了 virtio-block但它本质上就是一个 raw 文件没有任何文件系统层帮你做 CoW。所以文件管理必须自己设计。我采用的是经典分层模型Base 镜像制作成一个 ext4 格式的 raw 镜像文件内容是 Ubuntu 最小系统 内置 Agent Runtime 相关依赖打包后交给镜像仓库管理。这个镜像启动时以只读方式挂载不 mount rw也不会被改动。可写层给每个沙箱分配一块独立的 qcow2 或者直接使用 overlay 文件。我这里为了性能和实现简单直接给每个沙箱单独建了一个 raw 文件初始大小固定使用稀疏文件的方式避免提前占满磁盘。挂载组合启动时用 virtio-block 把 base 镜像和一个空的可写盘同时挂进去guest 内的 systemd 启动脚本负责把可写盘 mount 到/workspace同时把只读 base 的overlay目录和其他需要可写但不需要持久化的区域重定向到 tmpfs。这一层的关键是不要试图让 base 镜像可写。每次任务开始都应该从同一份干净镜像开始否则镜像漂移排查起来会让人疯掉。Mirror 漂移是 E2B 在生产里强调过的大坑之一。3.2 用户数据持久化与同步时机Agent 任务执行过程中用户代码、生成文件、包管理器缓存都会写进/workspace。这些数据默认只在沙箱生命周期内有效任务结束后沙箱销毁数据也消失。但很多场景要求用户能拉走结果文件所以数据同步必须做到显式化。我给出的同步模型是两级任务级同步Agent 会话结束时控制面通过 vsock 下发sync_files命令guest 内的同步代理把/workspace下的所有文件打包压缩通过预建立的传输通道推到对象存储并生成清单。快照级同步如果任务中途要挂起需要记忆状态同样走sync_files但只同步特定目录通过 config 里的sync_paths列表声明粒度更小传输更快。同步必须走到“对象存储写入完成”才允许状态机进入terminating否则任务完成了但结果丢了那是生产事故。3.3 快照恢复的限制与改进快照功能是 Firecracker 很诱人的特性可以把整个微VM 状态 dumped 出来之后秒级恢复。E2B 就靠这个做 session 复用。我初期也做过一版全量快照踩了三个坑实践后调整方案全量快照太慢如果/workspace里已经有几百 MB 文件snapshot 会把内存加磁盘全部序列化拉低恢复速度。改成分层之后base 镜像本身不需要进快照只要序列化内存状态和未同步的文件 delta。快照跨机器不通用Firecracker 快照包含 vCPU 状态、内存布局跨 CPU 型号迁移之前必须做 CPU 特性对齐。宿主机最好统一 CPU 型号才能把“恢复”当成一个低成本调度操作。我最后干脆限制了快照只在本机 resume不从快照跨机恢复。快照恢复后的网络连接要重建恢复一个微VM 快照不等同于恢复 Agent 会话TCP 连接、vsock 通道里的既有链接全部失效。恢复完成之后需要让 guest 内的 Agent Runtime 重新主动向控制面发起 handshake状态机里把握手完成才算attached。4. 进程编排微VM 内的代理进程模型4.1 为什么要内置代理进程沙箱只是提供了一个隔离环境能不能被编排还得看 guest 里有什么程序在配合。直接在 guest 里跑 systemd 管理业务进程当然可以但控制面想精确控制子进程生命周期的时候就没有抓手。我的做法是在基础镜像里内置一个 Rust 写的轻量代理agent它开机自启控制面通过 vsock 与它通信。代理进程负责四件事接收并执行控制面下发的 shell script / 二进制指令管理子进程创建、运行、等待退出、收集 stdout/stderr 和 exit code通过 vsock 维护心跳和文件传输通道执行文件同步、快照、环境清理等沙箱管理命令。这个代理本质上就是微VM 里的 systemd 替代品只做 Agent 编排相关的功能不做无关的系统管理工作。自定义代理的好处是协议由自己控制可以随时扩展新命令不会被外部的 agent 协议绑定。4.2 命令执行与超时控制Agent 业务经常要执行一段 Python 脚本或者调用一个 package manager这些指令通过execute命令下发。我把命令格式做成 JSON通过 vsock 发给代理代理内部用forkexec的方式跑子进程这样能让命令在独立的进程组里运行方便后续整体 kill。{ cmd: execute, id: task-abc-123, command: python3 /workspace/task.py, timeout_ms: 60000, env_vars: { PYTHONUNBUFFERED: 1, WORKSPACE: /workspace } }这里最关键的三个参数timeout_ms不能默认永久。我们的默认策略是min(timeout, 60s)超过则向代理发 SIGKILL。子进程如果有孙进程SIGKILL 只杀父进程是不够的必须用kill(-pgid)或 cgroup 杀确保把整个进程树带走。workdir默认设为/workspace避免 Agent 后续操作路径产生歧义。env_vars每次执行都要显式声明不要在代理里缓存环境变量因为任务之间是隔离的环境缓存反而会产生脏状态。命令执行期间控制面通过 vsock 持续接收代理推送的 stdout/stderr最多缓存 1MB超出后丢弃并置一个output_truncated标志。曾经有任务把无限循环输出打到缓存结果内存被打爆所以缓存上限和丢弃标志都必须有。4.3 会话状态与进程关联Agent 沙箱不是只跑一条命令就结束会话可能横跨多次工具调用。每次工具调用在微VM 里产生一批进程这些进程之间如果互相残留会影响下一次调用。所以安排在每次执行任务前代理先做一次环境复位清理临时目录、杀掉残留进程、删除/workspace/.tmp_*文件。复位不是系统重装而是逻辑上保证上一次执行的影响不污染下一次执行。干净状态对 debug 特别重要。有一次排查问题发现前一个任务留下了三个后台进程一直向某个 socket 端口回数据导致后一个任务每次连接都失败。加了环境复位之后这种问题从根本上消失排查时间从小时级降到分钟级。5. 实操部署关键参数与性能调优5.1 VCPU、内存与磁盘的配置Firecracker 相比完整 KVM 虚拟机省掉了很多设备但其资源隔离还是得靠 cgroup 和 pid namespace 配合。我的默认配置经过真实流量压测后定为下面这套配置项默认值说明vCPU 数2Agent 任务普遍是 IO 密集型不需要 4 vCPU内存512MB低于这个大小跑 Python 3 常见依赖有点紧/workspace 可写盘2GB sparse超阈值按策略 kill内核 panic 停退出开启防止 guest 异常无限重启网络模型veth 对 私有 IP配置简单配合 iptables 限制出网控制通道virtio-vsock不依赖 IP隔离性更强内存是最容易爆的参数。512M 够跑大多数轻量任务但装大型依赖比如跑模型推理会 OOM。后来我支持了按任务声明内存比如sandbox_memory_mb字段调度系统按声明申请做一个弹性开局。E2B 生产里也做类似工作内存按需分配是提升资源效率的关键。5.2 启动流程的具体实现整个启动代码不复杂但顺序很重要。我简化后的核心流程控制面收到create_sandbox请求校验配额和参数控制面为沙箱分配唯一 ID、私有 IP、宿主机端口映射宿主机 API 服务组装 jailer 参数调用jailer --id ... --exec-file /path/firecracker -- --api-sock /srv/sandbox-xyz/api.sock --kernel /path/kernel --rootfs /path/rootfs --vsock ...宿主机 API 服务通过 unix socket 与 Firecracker 的 REST API 通信等待InstanceStart成功轮询 vsock 通道等待代理进程注册上线代理上线后控制面与代理完成握手进入ready状态并返回公网连接信息。实际中多出来的坑是jailer默认要求指定--uid和--gid如果不对齐文件权限guest 会没有权限访问挂载文件。我统一用一个专用低权限系统账户跑所有 jailer 进程避免 root 运行 VMM 带来的风险。5.3 网络配置与出网控制Agent 沙箱不是完全隔离在孤岛的很多任务需要访问包仓库、API 服务。我采用的网络方案是每个沙箱一对 veth宿主机上挂一个 Linux bridge网关指向宿主机。宿主机制定 iptables 规则只允许目标端口为 80/443 的 TCP 出网其余全部丢弃。guest 内部网络配置通过 kernel cmdline 传入 IP 和网关省掉了 DHCP 带来的延迟。我在 kernel 上直接配置ip172.16.10.2:::255.255.255.0::eth0:off指定静态 IP 之后微VM 启动时不会再浪费时间做 DHCP 请求。实测启动时间因此减少了约 30ms对比整体冷启动耗时是不可忽视的一部分。5.4 冷启动优化的方向最终优化后的冷启动时间做到了 600ms 左右这里有 250ms 是 vCPUs 初始化150ms 是内核启动200ms 是代理进程等待 vsock ack。又做了几个细节优化保留 base 镜像页缓存宿主机的 page cache 中常驻基础镜像启动时直接复用缓存页避免从磁盘重新读整块镜像使用预建的 jailer 命令模板减少每次拼接字符串和解析参数的 CPU 开销虽然很少但整体 latency 目标下不可忽略把代理进程的启动放到 systemd default target 前的 early boot代理越早起来控制面越早能开始握手整个流程就短。6. 常见问题与 E2B 生产经验校准6.1 冷启动与持久会话的选择E2B 在生产中惯用的模式是Sandbox 创建以后并不立刻绑定一个会话而是预创建一批热沙箱等用户请求到达时从中取一个把会话状态 join 上去。这套预加载 绑定思路我直接借鉴到了自建集群里。实现方式是控制面维护一个warm_pool空闲时按最小水位线提前启动 5~10 个ready状态的沙箱用户请求来时只要从池里取一个把用户的 code 注入/workspace然后下发执行命令即可。这种方式下用户感知到的启动时间从 600ms 进一步降到 200ms 以内。但注意不要把预加载做成预执行沙箱虽然热但里面没有任何用户数据注入数据以后才能执行。如果用宿主机的 overlayfs 做了 session 分离那可以做到热沙箱 目录切换不过复杂度高不少适合用户量大的时候再引入。6.2 我踩过的一些文件系统大坑说出来都是泪ext4 的 lazy allocation 导致磁盘占用异常启动后的空盘看起来只占了几 MB一旦开始写文件就瞬间暴涨。监控里磁盘使用率曲线很难看后来直接把/workspace的 mount option 加了nodelalloc并设置固定容量告警阈值。sparse 文件被 qemu-img 转换后变实盘如果中途想用 qemu-img 做检查它会默认把 sparse 区域写满成 0直接把 2GB 的裸盘整成 2GB 真实占用的文件。不要随便对运行中沙箱的磁盘做 qemu-img 操作。overlayfs 在 Firecracker 环境里的 kernel 限制旧内核上 overlayfs 问题多直接放弃 overlay改用 raw 磁盘文件做独立挂载逻辑最清晰。文件同步与快照的竞争条件如果 Agent 任务还没结束控制面就开始 sync_files会读到进程正在写入的半成品文件。我强制 sync 前先给微VM 发一个fsfreeze指令冻结文件系统sync 完成后再解冻。6.3 与 E2B 生产经验的校准结果E2B 的核心逻辑中和自建集群一致的部分我总结为四点可以直接复用的经验沙箱是一个以微VM 为单位的集合而不是进程集合控制面只需要管理“沙箱生命周期”不需要管沙箱内部具体跑了几个进程。冷启动优化永远在最大头所有能把可达 launch 时间降低的动作都值得投入因为 Agent 交互过程中用户感知延迟很大部分来自沙箱环境准备。状态机是调度系统不可妥协的骨架任何状态迁移如果没有超时、没有重入保护故障恢复就会不可控。文件管理是生产路径中最容易出 bug 的部分对象存储上传超时、磁盘打满、快照损坏优先级高于功能开发。同时也校准出几个我和 E2B 有差异的点E2B 将大部分沙箱逻辑封装在 managed runtime 层而自建集群里我更倾向于把控制面逻辑与具体沙箱技术解耦这样未来如果 Firecracker 被替换成其他 VMM控制面只需要适配新 adapter 即可。6.4 运维偶发问题速查表现象可能原因处理办法沙箱启动超时base 镜像页缓存未预热 / 磁盘 IO 瓶颈预热镜像页缓存检查宿主机磁盘 IO 负载沙箱失联网络抖动 / 代理进程崩溃心跳超时后重连尝试一次失败则销毁重建同步文件超时/workspace 文件过多或过大检查同步路径中是否有 cache 目录必要时排除沙箱内网络访问失败出网规则只允许 80/443确认目标端口必要时扩展策略内存持续增长guest 内残余进程检查代理进程的 zombie 回收逻辑强制 kill 整个进程组快照恢复后命令无响应恢复后 vsock 断开控制面等待代理重新握手不要直接恢复旧会话这套集群从设计到上线前后改了不少地方但最核心的还是状态机与文件管理这两层。把沙箱生命周期想清楚、把文件读写路径理顺调度层就稳定了一大半。最后再分享一个小技巧每台宿主机上我都会留一个debug-shell微VM 沙箱带了一个特权代理用来手动进入微VM 排障。这个 shell 沙箱放在独立网络 namespace 里权限也限制在宿主机的 debug 网段不干扰生产流量。排查问题时不用再去捞宿主机的日志直接连进去跑几条命令比什么日志分析都快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑