沙箱池与冷启动

11 分钟读完

O25
运维 · 智能体运维:部署与运营

沙箱池与冷启动。

别人报给你的每一个「百毫秒以内」的沙箱数字,都是一次快照恢复、且是一个一个量出来的;而这句话的两半,碰上一支智能体机队都活不下来。智能体是成批抵达的,而在成批的情形下,同样这些厂商会慢上一到两个数量级,彼此的排名还会倒过来;至于那个产出漂亮数字的恢复技巧,正是 Firecracker 自家维护者称之为「重复使用即不安全」的那套机制。「热」和「干净」是同一个旋钮,不是两个,而你总得在某处把它拧到某个位置。

STEP 1

那个头条数字是串行的,而你的负载不是。

厂商宣传报的是最好情形下、一次一个的启动:API 调用发出去,一个沙箱回来,秒表停住。那是一次真实的测量,量的却是一种你几乎永远不会身处其中的情形。一个扇出到八个子智能体的智能体、一次事故之后排空的队列、一轮批量评测——这些都会一次性要来许多沙箱,而并发正是分配器、镜像缓存与调度器开始起作用的地方。

有一个会发布带日期、带版本的 JSON 结果的公开基准,把这道落差摆得很实。它量的是到可交互的时间——从 API 请求到在一个全新沙箱里第一条命令成功执行——某家厂商在请求一个接一个到来时录得 83 毫秒中位数,而在并发 100 时录得 14.8 秒中位数。同一个产品上约 180 倍的跨度,而且这不是孤例:在被测的各家之间,串行与突发两张表的排序几乎完全倒置。串行表里最快的那个,正是突发表里最慢的那个。

  • 读尾部,不要读中位数。有一家厂商的串行中位数在一秒以内,而 p95 与 p99 在二十秒以上。沙箱获取上二十秒的 p99 是一次用户看得见的卡顿,而再怎么追着中位数跑,也不会让它浮现出来。
  • 诚实的厂商声明是那个给了区间的。凡是厂商公布了区间的——「视镜像大小,通常落在 1 到 3 秒」——实测就落在区间内。而每一个精确的「亚秒」声明,实测都远在自己声明之外。
  • 基准也是有作者的。上面这个由一家售卖「盖在这些厂商之上的抽象层」的公司运行;这是去核对方法的理由,而不是把数据丢掉的理由:它开源、带日期、可复现,这比任何厂商页面都多。而真正能定论的,是你自己在自己的并发与区域下跑出来的数。

团队最常量错的那一项:在一个循环里一次获取一个沙箱,然后把 p50 叫作自己的冷启动。按你真实的抵达形状去获取——你的扇出实际产生的那个突发规模——并分别记录 p50、p95 与 p99。要预期这个数比当初说服你选平台的那个差上数倍,而且它属于另一家厂商,不是你以为的那家。

STEP 2

「热」和「干净」是同一个旋钮,而上游把话说得很明白。

快启动来自「不启动」。一份 microVM 快照把客户机内存与被模拟的硬件状态存进文件;恢复时把文件内存映射进来、载入 CPU 状态、继续跑——没有内核引导、没有 init、没有包导入。那些漂亮数字就是从这儿来的,而这也是对的技术。

它还有一条很少进到架构图里的、被写下来的约束。Firecracker 自家的快照文档写着:若缺少一套强机制来保证「独一无二之物在多次恢复之间仍然独一无二」,则从同一份状态恢复执行超过一次,被认为是不安全的。它点名的风险,你只要想一秒就会觉得理所当然:

  • 随机数会重复。客户机的熵池与 PRNG 状态是快照的一部分。从同一份快照恢复出来的两个沙箱,可能生成同样的「随机」值——会话 ID、nonce、临时文件名、密钥材料。
  • 标识符会重复。客户机曾经推导一次并缓存下来的任何东西——一个 UUID、一个机器 ID、一个注册令牌——如今被每一次恢复共享。
  • 凭证会重复。在快照拍下之前取到的凭据,被烤进了由它派生的每一个实例。

缓解手段是存在的,而那正是该向厂商问的:一个代际计数器设备,让客户机能察觉自己是被恢复出来的、从而给 PRNG 重新播种;外加在用户态把任何「独一无二之物」去重。运维上的要点是:「热」是一条关于共享状态的断言,所以对任何报出快速恢复的厂商,该问的不是多快,而是恢复时有哪些东西被重新随机化了。如果电话那头没人答得上来,那你池化的就不只是内存,还有身份。

STEP 3

复用通常是默认行为,而那个默认的作用域是错的。

快照恢复是这件事精妙的那个版本。粗笨的那个版本是:许多沙箱 SDK 用你提供的一个名字来寻址沙箱,而把同一个名字递回去,递回给你的就是同一个活着的沙箱——文件系统、进程,一样不少。那是一项功能,而它只在一个地方是陷阱:当你挑的那个名字,比你的信任边界更窄的时候。

有一家厂商把这件事记录得异常直白。同一个 ID 永远返回同一个沙箱实例;在它内部,所有进程看到同一批文件、所有会话都能看到所有进程;文档还配了一对「安全/不安全」示例,附带一句警告说用户能读到彼此的文件,并给出结论:要完全隔离,就得每个用户一个独立沙箱。这个建议是对的,而违反它又特别容易出于无意——因为那个顺手的键,无论是工作流名、工具名还是智能体名,几乎从来都不是用户。

  • 按信任边界给沙箱定键。最低限度按最终用户;当同一用户内部的任务之间也不该互见时,就按任务。把租户标识放进沙箱 ID 是一份廉价的保险,而且在 diff 里可评审——这与智能体的多租户是同一套论证。
  • 显式销毁,不要指望超时。厂商都提供一个拆除原语,会终止容器并删掉它的状态。空闲超时最终确实会回收沙箱,但「最终」既是一笔账单,也是一扇窗口。
  • 别假定「休眠」等于「重置」。磁盘能否熬过一轮休眠—唤醒,各家不同,而且至少有一家的文档自相矛盾。去测,别去读:写个文件、让沙箱休眠、再唤醒、看一眼。
  • 把文件系统当作智能体的记忆。一个编码智能体会留下一个仓库、一个虚拟环境、一个打了一半的补丁,以及它自己的草稿笔记。这恰恰就是复用之所以快的原因,也恰恰是复用之所以泄漏的原因——参见沙箱与隔离模式。
STEP 4

池子大小是 Little 定律,而持有时间以分钟计。

Serverless 的直觉假定请求在毫秒级结束,于是几个实例的热池就能吸收掉大量流量。而智能体会在整个任务期间一直占着它的沙箱。就这一处变化——W 以分钟而非毫秒计——让池子从一个延迟小把戏变成一个容量问题;而这套算术,与给呼叫中心排班用的是同一套。

# Little's law: concurrent sandboxes in flight = arrival rate x hold time
# tasks/minute x minutes held = sandboxes you must have

  12 tasks/min  x   6 min held  =  72 concurrent
  12 tasks/min  x  20 min held  = 240 concurrent   # same traffic, one slow tool

# Then size the warm pool against the BURST, not the mean:
#   warm = p95 simultaneous acquisitions, not p50 concurrency
  • 持有时间是最敏感的那一项,而没人公布它。关于真实智能体任务到底占着沙箱多久,不存在任何第一方的分布数据。可观测到的是一道落差:各家 harness 的超时聚在一小时上下,而报出来的逐任务墙上时间聚在个位数分钟。那道落差不是余量——它是空转的账单,而量出它是你的事。
  • 量持有,别量运行。沙箱从获取一直被持有到释放,其中包含智能体挂着一个空闲容器、等模型回话的每一秒。对一个多轮智能体来说,这一段常常占了持有时间的大头。
  • 一个慢工具就能重新决定机队规模。一个给中位任务加上十四分钟的依赖,会在流量不变的情况下把你的并发沙箱数变成三倍。这与并发与扩缩容是同一种耦合,只不过外接了一块按秒走的表。
  • 给持有时间封顶,然后守住这个顶。一个从「你愿意为多久买单」推导出来的最长沙箱寿命,和别的运营限额一样是一条限额;没有它,一个卡住的智能体会一直占着容量,直到厂商的默认超时——那可能是一小时。
STEP 5

空转免不免费,是厂商的属性,不是池子的属性。

热池是一场押注:预付的容量比用户看得见的延迟更便宜。这注押得好不好,完全取决于一处计费细节;而这处细节各家不同,又很少摆在定价表的显眼处。

  • 按预置量计 vs 按活跃量计。至少有一家主要厂商,在实例醒着时按你预置的资源收内存与磁盘、只按活跃使用收 CPU——于是一个空闲但醒着的沙箱不是免费,只是更便宜。实例休眠后计费才停止,这就让「不活动超时」成了一个定价旋钮,而不是一项卫生设置。
  • 「暂停」并不总是免费。有一家厂商对已停止和已挂起的机器都按其根文件系统收费,于是根本不存在免费的空闲态;另一家则对暂停中的沙箱只收存储费,而暂停这个动作本身要花掉与内存大小成正比的墙上时间。对同一个问题,这是两个相反的答案。
  • 挂起可能悄悄变成一次冷启动。在挂起被明确声明为不保证持久的地方——宿主迁移、维护与容量压力都可能回收一台挂起的机器——你的智能体代码就不能假定恢复后内存里的状态还在。把恢复路径设计成能识别冷启动并重建,并且刻意去测这条路径。
  • 有热池旋钮的地方就用它。有些平台显式暴露了「常驻热容器下限」「活跃时的空闲缓冲」与「缩容窗口」。这三项设置就是这场权衡的诚实接口,而它们的文档把话说得很直白:池子越大越贵,等待的请求越少。

在动手调任何一边之前,先把池子和 token 开销放上同一块看板。在短任务上,一个热池的花费可以与模型账单相当;而它正是最容易被归到「基础设施」名下、又永远追溯不回智能体那一次持有时间回退的那条账目。逐任务的视角见单位经济,而一个预留池究竟把什么换成了什么,见固定成本 vs 可变成本。

STEP 6

把决定以上一切的那四个数埋点埋出来。

这一页上几乎每个决定,最后都收敛成四项测量;它们没有一项来自厂商的看板,而全部都只是获取点与释放点上的几行代码。

  • 到可交互的时间,按你真实的突发规模。从获取调用到第一条命令成功,同时记下当时在途的同时获取数。没有第二个字段,这份分布就无从解读,因为它把两种相差两个数量级的负载混在了一起。
  • 持有时间,拆成「在干活」与「在等」。从获取到释放,并把阻塞在模型回话上的那一份单列出来。第二个数往往藏着最便宜的那份收益:在任务允许的地方,于一次长等待期间把沙箱释放掉,就是并发容量的直接下降。
  • 复用率与复用作用域。有多大比例的获取返回的是既有沙箱,以及它们是按什么键被寻址的。复用率讲的是你的延迟故事;复用作用域讲的是你的隔离故事——而复用率上升,只有在作用域是对的时候才是好消息。
  • 获取失败与队列深度。厂商的并发上限是真实存在的,而且恰好在你突发时撞上。一次获取失败必须是一等的、会重试的、可告警的事件——而不是一个最终以「一次莫名其妙失败的智能体运行」浮出水面的异常。这与限流与厂商容量是同一套纪律。

只做一件事的话:按你的扇出实际产生的那个突发规模,对两家厂商、在你的区域里,跑一遍你自己的获取基准,并且把它做成一个定时任务而不是一张一次性表格。公布出来的数字是串行的,而排序在并发下会变,所以从定价页做出的选择,是照着另一种负载做出的选择。然后把沙箱 ID 设成你的信任边界,把最长持有时间设成一个你愿意为之买单的时长——这两项设置,正好合上了那个「快启动」数字悄悄留下的隔离与成本两个口子。相关:沙箱与代码执行看那些原语,智能体压测看如何诚实地造出那个突发,以及持久状态与可恢复性看怎样在不丢掉这次运行的前提下释放沙箱。