Agent 沙盒的资源限制:CPU、内存、磁盘、网络带宽——防止 Agent 吃掉所有资源

凌晨三点,你的某个 Agent 还在循环。

AI technology illustration

它在重试同一个失败的 API 调用:超时 30 秒,重试,把完整日志 append 到 /tmp 下一个临时文件,再重试。第二天早上你看到两样东西:一张不想看的云账单,和一条 "磁盘 inode 耗尽" 的告警。

这不是 AI 变坏了——你压根没给它写退出条件。

我最早做 Agent 沙盒时,想法很朴素:给 Agent 一个 Docker,跑完销毁,不就成了吗。直到一个失控的循环把宿主机磁盘吃了一半——注意,不是容器磁盘,是宿主机——我才开始认真琢磨:"防止 Agent 吃掉所有资源" 这件事,到底应该怎么做。

Agent 跟普通程序差在哪:它有自动重试权

普通程序崩溃,就停了。Agent 不一样——它看到 exit code 137(OOM 被杀)会怎么做?它会想,"任务还没完成,我重试一下"。然后再次被杀,再重试。你以为内存限制杀了进程,实际只是开启了 Agent 的重试风暴。

所以 Agent 沙盒的资源管理,核心不是一套死的配额,而是配额与失败策略的联动。你限制它的同时,还得让它知道自己被限了。

CPU:你以为限的是速度,其实限的是时间片

我最早以为,限制 CPU 就是让进程跑慢一点。后来发现完全不是。Linux 用 cgroup 的 CPU 配额机制,做法粗暴得多——直接冻结。

mkdir /sys/fs/cgroup/agent
echo 50000 100000 > /sys/fs/cgroup/agent/cpu.max   # 0.5 核
echo $PID > /sys/fs/cgroup/agent/cgroup.procs

cpu.max 的格式是 配额 周期,单位微秒。上面这行意味着:在每 100ms 的周期里,这个 cgroup 里的进程最多跑 50ms。跑满之后,内核把它冻住,睡完剩下的时间再继续。

所以被限了 CPU 的 Agent 表现不是均匀变慢,而是一顿一顿的——跑 50ms、停 50ms。对批处理任务没大影响,但如果 Agent 在维护一个 WebSocket 或跟外部 API 保持心跳,这种卡顿会带来诡异的超时。

还有一件事容易漏:--cpus=0.5 管的是用户态 CPU,不含 GPU。Agent 要是有一块显卡,还得单独给 GPU 设配额。

内存:被杀的不一定是负责逃跑的

Docker 用户普遍只设一个 --memory,也就是 cgroup v2 里的 memory.max——硬限制,超了直接杀。但这里藏着一个 Agent 场景的坑。

echo 2G > /sys/fs/cgroup/agent/memory.high   # 软限制:超了开始限流
echo 3G > /sys/fs/cgroup/agent/memory.max    # 硬限制:超了直接杀

两次真实教训:第一次,一个数据处理任务跑了 40 分钟,内存涨到 3G 被 OOM,中间计算结果全在内存里,磁盘上什么都没留。后来我加了 memory.high=2G——超过 2G 时内核先回收页面缓存、强制限流,让进程慢下来而不是干脆被杀掉。但进程要是真越了 max,该杀还得杀。

第二个坑更隐蔽:被杀的那个进程,不一定是主循环。OOM killer 挑的是 oom_score 最高的进程——往往是某个子进程。于是 Agent 的主循环活着,子进程死了,它一脸无辜地重试,再死,再重试,直到重试次数配额也耗尽。

这也是为什么很多 Agent 平台会在容器或虚拟机边界直接杀整个环境,而不是放任里头的进程各自生死——对 Agent 来说,"整个环境死了" 是一个清晰的状态,"有个子进程死了但我还活着" 是歧义。歧义会让 Agent 做出最坏的决定:重试。

磁盘:空间够用,inode 先死

我踩过最冤的一次:Agent 在循环里写了 20 万个 4KB 的临时 JSON 文件。磁盘空间还剩 60%,但所有新文件创建直接报 No space left on device。查 df 显示一切正常,一度以为云盘坏了。后来才明白,是 inode 被耗尽了。

给 Agent 做资源限制,这几件事容易漏:

  • 磁盘空间上限:文件系统配额,或给沙盒挂一块固定大小的数据盘。
  • inode 数量:海量小文件是 Agent 循环最容易造出来的东西,而 inode 耗尽时 df 看起来一切正常。
  • 文件句柄数RLIMIT_NOFILE):循环里 open 不 close,句柄泄漏能拖垮宿主机。
  • 进程数量pids.max):防止 Agent 疯狂 fork,这也是 Docker --pids-limit 存在的意义。

还有一个跟容器相关的隐藏坑:overlay 文件系统。Agent 在容器里写入数据,即使后来删除,写时复制层的历史数据块并不一定立即回收;如果某个进程还开着已删除文件的句柄,空间更是直接消失。对 Agent 这种反复写、反复删的负载,磁盘增长失控是常态。

网络:给 Agent 加一个流量阀

Agent 是带着内部 API 权限的。如果它在死循环里反复拉取数据,或者更坏——被 prompt injection 骗着持续外传数据——带宽限制就是最后一道止损闸。tc 一行命令就能实现:

tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms

tbf 是令牌桶:允许短时突发,但长期速率被钉死在 1Mbps。Agent 疯起来,数据也运不出去。

除了带宽,还要考虑并发连接数。Linux 的 conntrack 表是宿主机级共享资源,Agent 并发连接一多,整台机器的连接表就会被挤爆,殃及所有邻居容器。限制并发连接数(比如 iptables connlimit)比限带宽更紧迫。

四套方案怎么选

方案 隔离边界 启动耗时 额外内存开销 Agent 场景
Docker 容器 内核 namespace + cgroup 几十毫秒 接近零 内部工具、可信代码、快速量产
gVisor 用户态内核拦截 syscall 百毫秒级 几十 MB 需强隔离的通用 Linux 执行环境
Firecracker 硬件虚拟化 <125ms 约 5 MiB 多租户平台(如 E2B 沙盒服务)
WASI 线性内存 + capability 毫秒级 几 MB 轻量计算、插件生态

选型的第一变量是信任边界:跑你自己的 Agent,Docker 足够;跑陌生人提交的代码,Firecracker 或 gVisor 才让人睡得着。第二变量是 syscall 密集度——gVisor 对每个系统调用都有拦截开销,CPU 密集任务会明显慢一截;Firecracker 的硬件虚拟化没有这个损耗,代价是每个沙盒的固定内存开销。

话说回来:配额是保险丝,预算才是方向盘

到这里你应该理解了:cgroup 解决的问题,是保证沙盒不会拖垮宿主机。但"沙盒没炸" 不等于 "Agent 没浪费钱"。

一个真正重要的认知变化:把配额告诉 Agent,让它自己规划。只给 2GB 内存,Agent 会考虑分批处理数据而不是一把梭加载全量;告诉它 "这个任务最多跑 20 分钟",它会更早选简单算法。资源限制不只是管理者的棍子,也是 Agent 规划时的信息。

反过来,如果 Agent 不知道上限,它会写出最天真的代码:全量读入内存、无限重试、把整个数据集下载到本地——然后用光所有配额,崩掉,重试,再崩。这就是为什么只有配额而没有一个清晰的失败策略时,限制反而会变成资源浪费的放大器。

最后给一份我自己的检查清单,给 Agent 配沙盒时逐项过:

  • CPU 配额(含 GPU——最容易漏)
  • 内存软上限 + 硬上限
  • 进程数限制 pids.max
  • 磁盘空间 + inode + 文件句柄
  • 网络带宽 + 并发连接数
  • OOM 和超时后的行为:重试有限次吗?失败态清晰吗?

真话是:资源配额你做得多完美,都拦不住一个设计糟糕的 Agent 烧钱。限制是防守,预算意识才是让 Agent 学会节省成本的开始。给 Agent 写好退出条件——这句话,比任何 cgroup 参数都值钱。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/503.html

(0)
上一篇 3天前
下一篇 3天前

相关推荐