做多租户 Agent 托管平台那阵子,我最大的痛点不是 Agent 业务逻辑怎么写,而是"别人提交的一段不可信代码,我要怎么保证它不会拖垮我这台宿主机"。Docker 容器级别的隔离我信不过,KVM 裸起 QEMU 又慢又重。后来 AWS 开源的 Firecracker 进入视野,我用它给每个 Agent 套了一层微虚拟机级别的硬件隔离,把镜像构建、启动流程和 I/O 性能整个摸了一遍。这篇文章不聊概念,只讲我在 Agent Sandbox 项目里实际跑通的镜像制作链路、启动过程踩过的坑,以及块设备和网络 I/O 的真实表现,希望能帮到同样在折腾 Firecracker 做沙箱的人。
1. 为什么把 Agent 塞进 Firecracker:容器隔离的边界与微虚拟机的取舍
1.1 容器的"隔离"到底缺了什么
先说清楚我这里说的 Agent 是什么:独立运行的自动化任务进程,包括爬虫脚本、代码执行沙箱、数据采集任务、以及给 LLM 工具调用的远程执行环境。这类任务的共性是"不可信、要联网、可能吃满 CPU 和磁盘、还需要频繁快速拉起一批新的"。
我最早用 Docker 容器做隔离,namespace + cgroup 在大多数场景下确实够用,但有两个问题始终绕不过去:一是容器共享宿主内核,一旦碰到内核级漏洞或恶意提权尝试,隔离就被击穿了;二是 Docker 的默认网络、挂载、权限配置很容易留出口子,普通 Web 服务无所谓,但运行的是"可能被恶意样本调戏"的 Agent 时,我不敢把宝押在容器上。
gVisor 我也试过,它把内核调用拦截到用户态,安全模型更干净,但兼容性是硬伤。部分 Agent 用的系统调用它不支持,直接报错;而且用户态内核的 CPU 开销不小,密集型计算任务性能打折明显。QEMU 最稳,可完整虚拟机的启动时间按秒算,内存开销轻松上几百 MB,一个 Agent 吃这么多资源,成本上撑不住。
1.2 Firecracker 在 Agent 场景下的定位
Firecracker 走的是"微虚拟机"这条路:它只实现运行 Linux 所需的最小子集,主动砍掉了 PCI 枚举、ACPI、BIOS 这些兼容性包袱,直接把 guest 塞进 KVM 的 VM 里。代价是只支持 Linux guest、不能热插拔外设、设备模型非常简单。但这些限制对 Agent 沙箱来说反而是优点——我们根本不需要一台完整 PC,只需要一个能跑 bash、python、curl、访问网络的最小 Linux。
实测数据比我预想的好太多。我搭建的原型环境里,从 firecracker 进程启动到 guest 内核开始执行,通常在 300ms 内完成;单实例额外内存开销压到 5MiB 以内。这是什么概念?有一次业务高峰我同时拉起 80 个隔离环境,如果用 QEMU 要等半分钟以上,Firecracker 这边基本在几秒内全部就绪。这个特性对"动态伸缩 Agent 池"的场景是决定性的。
1.3 和 QEMU、gVisor、runC 的横向对比
用一个表格总结我实测下来的选型结论:
| 方案 | 启动时间 | 单实例额外内存 | 隔离边界 | 兼容性 | 适合场景 |
|---|---|---|---|---|---|
| Docker/runC | 百毫秒级 | 几 MiB | 共享宿主内核,namespace+cgroup | 最好 | 可信代码、CI 常规任务 |
| gVisor | 百毫秒级 | 十几 MiB | 用户态内核拦截 syscall | 中等,部分 syscall 不支持 | 偏 Web 场景的不可信代码 |
| QEMU/KVM | 秒级 | 几百 MiB | 完整硬件虚拟化 | 最好 | 需要多外设、多架构的复杂 VM |
| Firecracker | 数百毫秒级 | 5MiB 左右 | KVM 硬件虚拟化(最小设备模型) | 仅 Linux,外设极简 | 不可信代码、Serverless、Agent 沙箱 |
选 Firecracker 的核心逻辑很简单:我要的是"硬件虚拟化级别的隔离 + 容器的启动速度 + 可以接受的资源开销",Firecracker 刚好卡在这三个条件的交叉点上。外设少不是问题,Agent 需要的外设本来就只有磁盘、网卡和串口。
2. 镜像制作全流程:从内核裁剪到 ext4 rootfs
2.1 内核选择与裁剪要点
Firecracker 官方维护了一套精简内核配置(叫 linux-unprivileged),核心思路是只保留 virtio-net、virtio-blk、virtio-vsock 这几类 VirtIO 设备驱动,关掉一切 Firecracker 用不上的硬件支持。我早期偷懒,直接用发行版自带的 generic 内核,结果 guest 启动慢不说,还因为内核自动探测的设备 Firecracker 根本不提供,日志里疯狂报错,偶尔直接 panic。
后来我自己编译内核,配置思路非常明确:
- 保留:ext4、virtio_pci/virtio_mmio、virtio_net、virtio_blk、virtio_vsock、e1000 可选
- 关闭:声卡、显卡、USB、SATA、无线网卡、蓝牙、ACPI 高级电源管理
- 模块策略:尽量编进内核而不是模块,否则 rootfs 里缺对应 modules 目录会导致启动时找不到驱动
编译产物建议用 vmlinux 格式,这样 Firecracker 加载时不需要额外的解压步骤。裁剪后的 vmlinux 大约 8-15MB,bzImage 4-7MB。别小看这几 MB 的差距,在"同时拉起几十个实例"的场景下,内核镜像体积直接影响内存页缓存的命中率和启动耗时。
2.2 用 debootstrap 构建最小 rootfs
Rootfs 我试过两种方案:从 Docker 镜像导出 tar,以及用 debootstrap 从零构建。结论是 debootstrap 干净得多。
Docker 镜像导出的 rootfs 看起来方便,但坑非常多:Docker 的分层结构没办法直接映射成 ext4 的一个分区,导出的目录里经常缺少 /dev 下的设备节点,fstab 内容也和虚拟机场景不匹配,最关键的是 Docker 镜像里默认没有配置串口终端,Firecracker 的 console 输出经常是黑的。我花了一整天排查,最后发现不是 Firecracker 的问题,是 rootfs 缺了 getty 配置。
最稳的做法是这样:
# 安装基础系统 sudo debootstrap --variant=minbase \ --include=systemd,openssh-server,curl,sudo \ bullseye /var/lib/fc-rootfs \ http://deb.debian.org/debian/ # chroot 进入并安装 Agent 运行环境 sudo chroot /var/lib/fc-rootfs /bin/bash # 在里面装 python3、node、或者你的 Agent runtime # 设置 root 密码、添加 agent 用户、配置 ssh key # 清理 apt 缓存,移除不需要的包 exit注意--variant=minbase这个参数,它只装最基础的包,不装文档、不装 man page、不装推荐包,能把 rootfs 体积控制在很小的范围内。chroot 进去之后建议把 systemd 的某些服务关掉,比如不需要的 getty、ntp、dbus 相关服务,减少 Agent 启动后不必要的进程开销。
2.3 镜像体积与启动速度的权衡
rootfs 做好之后,要把它封装成 Firecracker 能直接识别为块设备的镜像文件。我用 ext4 格式:
# 创建一个 2GB 的 ext4 镜像 dd if=/dev/zero of=/opt/fc/agent-rootfs.ext4 bs=1M count=2048 mkfs.ext4 /opt/fc/agent-rootfs.ext4 # 挂载并把 rootfs 内容拷进去 sudo mkdir /mnt/fc-rootfs sudo mount -o loop /opt/fc/agent-rootfs.ext4 /mnt/fc-rootfs sudo cp -a /var/lib/fc-rootfs/* /mnt/fc-rootfs/ sudo umount /mnt/fc-rootfs镜像体积我建议控制在 1-2GB 之间。太小会导致 Agent 日志一多就撑爆磁盘,太大则镜像分发和缓存都会变慢。实际使用大约 800MB-1.5GB,里面装的 python、node 和 Agent 依赖完全够用。
如果想进一步压缩体积,可以用 squashfs 做只读 rootfs,配合 overlayfs 挂一个可写层。Firecracker 对只读 rootfs 支持得很好,因为单实例只允许一个 root 设备和一个附加 drive,把 rootfs 设为只读,把可变数据放附加 drive 是更符合沙箱语义的做法——Agent 无论如何折腾,重启之后又回到干净状态。
2.4 预置 Agent 运行环境和网络配置
每个 Agent 启动后需要不同的 hostname、SSH key、/etc/resolv.conf 内容。我的做法不是在镜像里固化,而是在 rootfs 里放一个初始化脚本,由 systemd 在启动阶段执行:
# /etc/systemd/system/agent-init.service [Unit] Description=Agent Init Script After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/agent-init.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target这个脚本从 Firecracker 自带的 MMDS(MicroVM Metadata Service)读取配置。MMDS 的用法很简单:宿主机通过 API 往元数据服务写一段 JSON,guest 内部通过curl http://169.254.169.254/就能读到。例如把 Agent 的 ID、token、要执行的任务类型都放在 metadata 里,guest 启动后一次性拉取并写入本地配置。
这里有个经验:不要在启动脚本里做太重的逻辑,比如不要在 agent-init 里拉取大文件或等网络超时。初始化脚本越轻量,Agent 的冷启动越快。复杂的业务逻辑放 Agent runtime 启动后再异步处理。
3. 启动链路拆解:REST API、启动参数与典型失败现场
3.1 启动链路全景
Firecracker 的启动方式和 QEMU 完全不同。它本身是一个长时间运行的进程,对外暴露一个 Unix Socket,通过 REST API 来配置虚拟机和启动实例。整个链路分三步:
- 启动 firecracker 进程
- 通过 API 依次配置 boot-source、machine-config、drives、network-interface
- 发送 InstanceStart 动作,guest 开始运行
我见过很多新手踩的最大的坑是:想用一个命令直接拉起虚拟机。Firecracker 没有这样的命令,它把"配置"和"启动"拆成了明确的阶段,好处是每一步都能单独校验,坏处是脚本要写得仔细一点,顺序错了容易报一些看起来莫名其妙的错。
3.2 关键配置项逐个说明
一个最小可用的启动脚本大概是这样的:
#!/bin/bash set -e API_SOCK=/tmp/fc-agent-$RANDOM.sock KERNEL=/opt/fc/vmlinux ROOTFS=/opt/fc/agent-rootfs.ext4 # 1. 启动 firecracker 进程 /usr/bin/firecracker \ --api-sock $API_SOCK \ --id agent-001 & sleep 0.1 # 2. 配置引导内核 curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/boot-source' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d "{ \"kernel_image_path\": \"$KERNEL\", \"boot_args\": \"console=ttyS0 reboot=k panic=1 pci=off\" }" # 3. 配置 vCPU 和内存 curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/machine-config' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "vcpu_count": 2, "mem_size_mib": 512, "ht_enabled": false }' # 4. 挂载 root 设备 curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/drives/rootfs' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d "{ \"drive_id\": \"rootfs\", \"path_on_host\": \"$ROOTFS\", \"is_root_device\": true, \"is_read_only\": false }" # 5. 配置网络 curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/network-interfaces/eth0' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "iface_id": "eth0", "host_dev_name": "tap0", "guest_mac": "06:00:00:00:00:01" }' # 6. 启动实例 curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/actions' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"action_type": "InstanceStart"}' echo "Agent started: $API_SOCK"逐个解释关键点:
boot_args里的console=ttyS0必须加,否则 guest 内核日志和提示符不会输出到 Firecracker 的串口,你连"系统到底启动到什么程度"都看不到reboot=k panic=1让 guest 遇到致命错误时快速重启而不是挂死,对自动化运维很重要pci=off可以关掉 PCI 枚举,减少启动时间ht_enabled我默认关掉,避免 vCPU 超卖导致的性能抖动is_root_device必须为 true,表示这个盘是根文件系统所在设备
关于path_on_host,Firecracker 默认直接在宿主机文件系统里读文件。如果启用了 jailer,路径会变成 jailer chroot 之后的相对路径。这一点在生产环境特别容易踩坑,后面会详细说。
3.3 启动失败的排查顺序
Firecracker 启动失败通常分几个层次,按下面顺序排查效率最高:
第一,看 firecracker 进程日志。启动时加--log-path /var/log/fc/agent-001.log --level Info,错误信息会直接写进去。很多"No such file or directory"其实是 kernel 路径写错了,或者 jailer 的 root 权限问题。
第二,看 guest 的串口输出。如果 boot_args 里有console=ttyS0,guest 内核日志会直接打到 firecracker 进程的 stdout。启动脚本里我建议把 stdout 重定向到日志文件,这样就算 guest 崩了也能看到最后一条内核日志。
第三,检查 root 分区参数。这是新手最容易忽略的地方:boot_args里要不要写root=/dev/vda?要写,而且取决于你的 ext4 镜像布局。如果整个镜像文件就是一个文件系统、没有分区表,那么设备是/dev/vda;如果镜像里有分区表、rootfs 在第一个分区,那设备是/dev/vda1。mkfs.ext4直接写在块设备上就是无分区表的方式,用/dev/vda。
第四,确认 KVM 是否可用。Firecracker 必须在 KVM 之上运行,ls /dev/kvm存在且当前用户有权限才能跑。在容器里跑 Firecracker 或者某些云主机上没开嵌套虚拟化,启动会直接报 KVM 相关的错误。
第五,网络配置问题。Firecracker 本身只负责把 virtio-net 设备接到宿主机的一个 tap 设备上,guest_mac只是 guest 侧的 MAC 地址。tap 设备要提前建好并配好 IP:
sudo ip tuntap add dev tap0 mode tap user $USER sudo ip addr add 172.16.0.1/24 dev tap0 sudo ip link set tap0 up如果 guest 里配了静态 IP 而宿主机没有 NAT,那 guest 能 ping 通宿主机却上不了外网。需要额外加 iptables 规则做转发。
4. I/O 性能实测:块设备、网络与 vsock 的调优空间
4.1 virtio-blk 的表现与限制
做了几轮压力测试之后,我对 Firecracker 的 I/O 性能有个比较直观的结论:单实例块设备性能大约是宿主机原生性能的六到七成,但在 Agent 场景下完全够用。
以我自己服务器上的 NVMe 盘为例,宿主机dd if=/dev/zero of=/tmp/test bs=1M count=1024大概能跑到 2GB/s 以上;Firecracker guest 里同样的测试大概在 500-700MB/s。顺序读差不多,4K 随机写的差距更明显,宿主机 200K IOPS 的水平,到 guest 里只有 50K 左右,原因是虚拟队列的轮询和中断处理有额外开销。
但这个性能损失对 Agent 任务没有实际影响。Agent 主要是小文件读写、写日志、加载程序二进制,这类负载对 4K 随机写的延迟更敏感,而 Firecracker 的 virtio-blk 延迟在亚毫秒级别。真正要防的是"某个不守规矩的 Agent 疯狂写盘把宿主机磁盘池打爆",所以我对每个实例都开了 rate limiter:
{ "drive_id": "rootfs", "path_on_host": "/opt/fc/agent-rootfs.ext4", "is_root_device": true, "is_read_only": false, "rate_limiter": { "bandwidth": { "size": 104857600, "one_time_burst": 1048576, "refill_time": 100 }, "ops": { "size": 4000, "one_time_burst": 400, "refill_time": 100 } } }这个配置限制每个实例最大 100MB/s 带宽、4000 IOPS。我实际测试过,加了限流之后对正常 Agent 任务延迟几乎没有影响,但在"某个 Agent 失控"的场景下能保住宿主机其他实例不跟着遭殃。
4.2 网络路径:tap 设备与 vhost-net
Firecracker 的网络模型比较朴素:一个虚拟网卡对应宿主机一个 tap 设备。默认路径下,数据包从 guest 出发经过 virtio-net 到宿主内核的 tap 设备,再由宿主协议栈处理,性能一般。在 1Gbps 内网环境下,单实例实测吞吐大约在 700-800Mbps,延迟比物理网卡高 20-50 微秒。
如果要追求更高的吞吐,可以启用 vhost-net。Firecracker 对 vhost-net 的支持是有限的,但基本能用。开启方式是在 network-interface 配置里加"vhost_mode": true。用 vhost 模式后数据路径会绕开一部分宿主协议栈的开销,实测吞吐能到 900Mbps 以上。代价是配置复杂度和排查问题的难度上升,vhost 模式下 tap 设备的管理更麻烦。
我的建议是:如果 Agent 主要是 HTTP 请求、API 调用、数据上传这类常见网络负载,默认 tap 模式足够;只有当某个 Agent 确实有大流量持续传输的需求,再单独开 vhost-net。别把所有网络流量都卡在同一个宿主机上,会导致软中断占用某个 CPU 核过高。
4.3 通过 vsock 和宿主机交换数据
Firecracker 里我最喜欢的特性是 vsock。它是一条 guest 和宿主机之间专用的虚拟 socket 通道,不依赖网络命名空间、不依赖路由和防火墙规则,配置简单且性能稳定。我在三个场景里重度使用 vsock:
- 宿主机向 guest 下发任务文件
- guest 向宿主机上报心跳和日志
- 宿主机直接执行 guest 内的命令(通过 vsock 转发 SSH 或自定义协议)
配置 vsock 只需要在启动时加一步:
curl --unix-socket $API_SOCK -i \ -X PUT 'http://localhost/vsock' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"vsock_id": "vsock0", "guest_cid": 3}'guest_cid是 guest 在宿主机上唯一的标识。guest 内程序连接宿主机时,用connect(2)到 cid 为宿主机(通常 cid=2)的某个端口即可。宿主机端可以用socat VSOCK-CONNECT:3:8080 TCP-LISTEN:8080做转发,也能写原生程序直接监听 vsock 端口。
我对比过 vsock 和 TCP 本地回环,vsock 吞吐比 loopback TCP 略低,但延迟更稳定,而且不受防火墙和网络命名空间干扰。在 Agent 场景里,我强烈建议把"任务下发"和"日志回收"这类控制面流量全部走 vsock,让 guest 的 eth0 只跑 Agent 业务流量。
4.4 实测数据与配置建议
留一份我环境中不同配置下的实测数据,仅供参考(单 vCPU、512MB 内存的 Agent,宿主机为常见 x86 服务器):
| 测试项 | 默认 tap 模式 | 开启 vhost-net | 备注 |
|---|---|---|---|
| 顺序读(dd 1M) | 约 600MB/s | 约 650MB/s | 瓶颈在虚拟队列而非磁盘 |
| 4K 随机写 | 约 40K IOPS | 约 45K IOPS | 加 rate limiter 后仍有 35K+ |
| TCP 吞吐(单连接) | 约 750Mbps | 约 950Mbps | 内网实测 |
| TCP 延迟(P99) | 约 60 微秒附加 | 约 40 微秒附加 | 不含物理链路段 |
| vsock 吞吐 | 约 500Mbps | 不适用 | 控制面专用,够用 |
结论是:Firecracker 的 I/O 性能不是瓶颈,真正决定 Agent 任务快慢的是 vCPU 数量和代码本身。如果一个 Agent 任务很吃 I/O,优先检查是不是业务代码写了大量小文件同步写,而不是动不动就怀疑虚拟机层拖了后腿。
5. 生产环境落地:多 Agent 并发的资源管理实践
5.1 用 jailer 做一层资源隔离
Firecracker 裸跑有权限过大的问题:如果通过 API 传一个/etc/passwd或者宿主机的盘镜像路径给它,它能直接读。生产环境绝对不能这样裸奔,必须用 Firecracker 自带的 jailer 工具。
jailer 的用法很简单,它会自动完成三件事:
- 创建 chroot 环境,把 firecracker 进程和它的可访问文件系统锁在一个目录里
- 创建新的 cgroup,限制 CPU、内存、PID 数量
- 创建新的网络命名空间
典型用法:
/usr/bin/jailer \ --id agent-001 \ --exec-file /usr/bin/firecracker \ --uid 1000 \ --gid 1000 \ --cgroup-version 2 \ --chroot-base-dir /var/lib/firecracker-runtime/使用 jailer 之后,API socket 的路径、内核镜像和 rootfs 的路径都会变成相对于 chroot 目录的路径。比如 chroot base 是/var/lib/firecracker-runtime/agent-001/root/,那kernel_image_path要写/opt/fc/vmlinux对应的在这个 chroot 里的位置。如果忽略了这个路径变化,会报 "No such file or directory"。
我个人的经验是:jailer 是 Firecracker 生产环境使用的前提,不启用 jailer 等于把宿主机的根目录暴露给了一个能执行任意代码的 API。这一点怎么强调都不过分。
5.2 并发启动的控制策略
有个问题让我印象很深:有一次 50 个 Agent 同时启动,宿主机 CPU 直接被打满,guest 串口日志全乱掉,有的实例迟迟不报 ready。后来分析发现,每个 Agent 启动瞬间都要解压内核、分配内存、创建 tap,多个实例同时做这些操作会引起宿主机资源争抢。
我的解决方案是引入并发控制。启动脚本里用信号量或者xargs -P限制同时启动的实例数:
seq 1 50 | xargs -P 16 -I {} ./start_one_agent.sh {}-P 16表示最多同时启动 16 个实例。实测下来,并发 16 时宿主机负载可控,每个实例的启动时间也从"挤在一起时的 2-3 秒"回落到"稳定 400-800ms"。另外,我给每个启动流程加了 10 秒超时,超时没收到 ready 信号就杀掉 socket 并标记失败,这样也不会因为某个坏镜像卡住整个队列。
另一个细节是:firecracker --id参数必须全局唯一,否则会报 ID 冲突。在并发脚本里记得用时间戳或随机数生成 ID。
5.3 镜像缓存与共享只读 rootfs
Agent 变多了之后,镜像占用的磁盘空间会变得可观。一个紧凑的 rootfs 镜像 1.5GB,50 个实例如果各自拷贝一份就是 75GB。我的做法是共享只读镜像加独立可写盘。
Firecracker 允许配置两个 drive:一个 root 设备(只读)、一个附加 drive(可写)。把 rootfs 设为只读共享,所有实例都指向同一份文件;可写盘用不同的文件,挂载到 guest 的/var/lib/agent-data。效果是磁盘占用从"N 个实例 x 1.5GB"降到"1.5GB + N x 小数据盘"。
但这里有个坑:Firecracker 本身不会自动挂载附加 drive。需要在 guest 的初始化脚本里加一步:
mkdir -p /var/lib/agent-data mount /dev/vdb /var/lib/agent-data # 或者用 ext4 的标签来确定设备/dev/vda是 root 盘,/dev/vdb是附加 drive,这个顺序除非你改 drive 配置,否则是固定的。把可变数据全部放/var/lib/agent-data,rootfs 始终保持只读,这样 Agent 即使被挖矿脚本污染,重启一次就能恢复干净状态。
5.4 监控、日志回收与故障自愈
最后聊运维层面。Firecracker 每个实例的日志默认打到 stdout,我在启动脚本里都重定向到/var/log/fc/agent-{id}/boot.log。guest 内的 Agent 日志我通过 vsock 转发到宿主机统一收集,不依赖 guest 里的 SSH 服务。
监控方面,Firecracker 提供 metrics 接口,可以定期从 API socket 拉取:
curl --unix-socket $API_SOCK \ http://localhost/metrics返回的 JSON 包含 CPU 使用、内存用量、网络收发字节数、磁盘读写次数等关键指标。把这些数据推到 Prometheus,配合告警规则,能及时发现"某个 Agent 的磁盘 IOPS 异常"或"网络流量突增"。
故障自愈我做了三层:第一层是 Agent 自身的心跳,guest 通过 vsock 定期上报,超过 15 秒没收到心跳就判定 Agent 卡死,直接杀 firecracker 进程;第二层是启动超时检测,前面提过;第三层是回收逻辑,杀掉进程之后要记得删除 socket 文件、移除 tap 设备、清理 cgroup。不然跑上几天,残留的 socket 和 tap 会越来越多,最终导致资源泄漏。
我在实际运维中还发现一个很实用的技巧:把"启动一个 Agent"封装成一个纯函数,输入是 Agent ID,输出是 API socket 路径。不管是手动调试还是集群调度器触发,都走同一个入口,排查问题的时候思路会清晰很多。这个项目从原型到上线,Firecracker 部分大概花了两周,前三天都在折腾镜像和启动参数,后面基本就是稳定复用了。如果你也在做类似的多租户 Agent 沙箱,我建议先把这个最小闭环跑通,再去考虑 I/O 调优和自动化调度。