Sprites 的设计与实现
Sprites 是不用 Docker 的 Linux 虚拟机:创建只要一两秒,带 100GB 持久根文件系统,闲置时自动休眠、几乎不花钱。文章讲支撑它的三个关键决定。
中文
复制

我们是 Fly.io,按惯例,文章写到这个地方,我们该告诉你我们的工作是把你的容器拿到我们自己的硬件上跑起来,遍布全球。但上周我们发布了 Sprites,它完全不是这么回事。Sprites 是个新东西:没有 Docker 的 Docker,没有 Docker 的 Docker。这篇文章讲的是它的工作原理。
水平一般的房主会买一整盒笔,然后塞进“放笔的抽屉”。高手明白的道理是:你得用对抗性的眼光看待笔。“一个系统的目的就是它实际做的事”;一个家庭系统的目的,就是让笔均匀分布。几个月后,抽屉一定是空的,不管你囤了多少笔。正确的做法是把笔撒遍每一个你可能想到去找笔的地方——抽屉、窗台、书桌。任何时候有人需要笔,手边就有好几支,而且恰好就在他第一个去找的地方。
这是我能找到的、用来解释 Sprites 的最佳说法。Sprites 是我们刚在 Fly.io 上发布的平台。Sprites 就是圆珠笔式的可抛弃计算机。无论你想留下什么痕迹,我们都把它安排好了:你离一支能用的 Sprite 永远只有一两秒。
Sprites 是 Linux 虚拟机。你有 root 权限。它们create只要一两秒:快到什么程度呢,创建并 shell 进去的体验,跟 SSH 登录一台已经存在的机器一模一样。每个 Sprite 都带一个 100GB 的持久根文件系统。闲置时自动休眠,休眠期间几乎不花钱。
结果就是,我几乎感觉不到有给 Sprite 起名字的必要。有时候我就敲个sprite create dkjsdjk,然后开始干活。Fly.io 里用 Sprites 的人,手头都挂着几十个。
云计算里还没有多少东西和 Sprites 的形状完全一样:
- 瞬间创建
- 没有时间限制
- 持久磁盘
- 自动休眠到廉价的非活跃状态
这是一篇讲我们如何把它做出来的文章。我们搭了一套新的编排栈,推翻了当初为 Fly Machines(我们的旗舰产品)做出的一些核心决策。事实证明,这些新决策让 Sprites 的扩展和管理变得容易得多。我们挺兴奋的。
对我来说很幸运的是,从 Fly Machines 到 Sprites,中间 90% 的路靠的正好是我们做过的三个big decisions,所以这篇文章写起来很轻松。那么,废话不多说:
决策 #1:不再有容器镜像
这是最容易解释的一个决策。
Fly Machines 大致上就是把 OCI 容器重新打包成 KVM 微型虚拟机。它们有 Docker 的使用体验,又有 EC2 实例的隔离性和安全性。我们非常喜欢它们,但它们显然不适合作为一次性圆珠笔式云计算机的基础。
Fly Machines 的“一个奇怪技巧”是它们能瞬间start和stop,快到可以及时唤醒以处理一个进来的 HTTP 请求。但前提是你已经created了它们。你必须预先分配。Creating一个 Fly Machine 可能要花一分多钟。正确的做法是提前创建一大堆,然后stop它们,这样需要时它们就准备好了。但对 Sprites 来说,我们需要create快到感觉它们早就等在那里了。
我们杀掉用户容器,只是因为我们想让它们死。
creating一个 Fly Machine 之所以慢,大部分原因在容器。我带着感情说这句话:你们的容器比一锅乱炖还离谱。又大又难伺候,拉取和解包都要花很久。区域局部性也很糟;在gru-3838上create圣保罗的一个 Fly Machine,和在gru-d795上放一个create,速度没有区别。为了让我们的 OCI registry 跟上这套系统,已经投入了多到令人心碎的工程工作。
我只能说,这活儿很难干。Sprites 去掉了面向用户的容器。字面意义上:问题解决了。Sprites 是在简单模式下做这件事。
现在,在底层,Sprites 仍然是 Fly Machines。但它们都从一个标准容器运行。每个物理 worker 都确切知道下一个 Sprite 会从哪个容器启动,所以我们很容易维持一批“空”的 Sprite 待命。结果是:create一个 Sprite 不需要做任何重活;它基本上只是做我们在start一个 Fly Machine 时做的那些事。
这一切现在就能用。
你现在就可以创建几十个 Sprite,只要你想。花不了几秒钟。 创建一个 Sprite。→

决策 #2:用对象存储做磁盘
每个 Sprite 自带 100GB 持久存储。我们之所以能做到这一点,是因为存储的根是兼容 S3 的对象存储。
你可以给一台 Fly Machine 配 100GB 存储,200GB 也行,500GB 也行。问题在于:
- 你得主动申请(用
flyctl);我们没法把它做成默认配置。 - 这份存储是 NVMe,直接挂在你的 Fly Machine 所在的那台物理服务器上。
[†] 如果你试图创建单节点集群,我们会打出一条醒目的红色警告
我们为 Fly Machines 设计存储栈,是为了 Postgres 集群。多副本 Postgres 集群用 Fly Volumes 效果很好。挂载存储快,但可能丢数据†——物理机炸了,没有任何魔法能救回上面存的比特。你只能退回到我们上一次的快照备份。对做了复制的 Postgres 来说这没问题,复制本来就是干这个的。但对任何没有显式复制的东西来说,这是一条非常锋利的刀刃。
从我们的角度看,更糟的是挂载存储会把工作负载钉死在特定的物理机上。我们有很多理由需要搬动 Fly Machine。在 Fly Volumes 之前,搬动只需要在服务器上按一下“drain”按钮。想象一下失去这种能力。用挂载存储把工作负载迁移做对花了 3 年,而且到现在也谈不上“轻松”。 对象存储是互联网的胡佛大坝,是我们最接近基础设施超级工程的东西。
Sprite 抛弃了这套模型。我们仍然利用 NVMe,但不把它当作存储的根,而是当作对象存储中某个 blob 的读穿缓存。兼容 S3 的对象存储是我们手上最值得信赖的存储技术。光是敲下“Sprite 由对象存储支撑”这几个字,我就能感觉到血压在下降。
这对编排的意义是深远的。从实际意义上说,一个 Sprite 的持久状态就是一个 URL。帽子搁哪儿,哪儿就是家!它们迁移(或从故障物理机恢复)轻而易举。我们的内部工具还处在早期,但可用的自由度一下子多了太多。
关于 Kurt 为实际存储栈设计的 Cronenberg 式方案,我还能再写 1500–2000 字,但它仍在变动中,这里就简单说说。
Sprite 的存储栈围绕 JuiceFS 模型构建(事实上,我们现在用的就是一个被改得面目全非的 JuiceFS,SQLite 元数据后端是重写的)。它的思路是把存储拆成数据(“chunk”)和元数据(记录“chunk”位置的一张映射表)。数据 chunk 放在对象存储上,元数据放在本地快速存储上。在我们的方案里,这份元数据存储靠 Litestream 保持持久化。没有任何东西依赖本地存储。 (我们预装的 Claude Code 会主动为你做 checkpoint,不会先问你)
这也让 Sprite 的 checkpoint 和 restore 变得很快。checkpoint 快到我们希望你把它当作系统的基本功能来用,而不是出问题时的逃生通道;它更像 git restore,而不是系统还原。之所以成立,是因为 checkpoint 和 restore 都只是在搬元数据。
我们的存储栈有一个类似 dm-cache 的特性,利用挂载的存储。每个 Sprite 挂一块稀疏的 100GB NVMe 卷,存储栈用它缓存 chunk,消除读放大。重要的是(我能感觉到自己的静息心率在下降),那块 NVMe 卷里的东西都无关紧要;存进去的 chunk 是不可变的,它们的真实状态在对象存储上。 我们对对象存储的偏好不止于 Sprite 存储栈。Sprite 的全局编排器是一个 Elixir/Phoenix 应用,它把对象存储当作账户元数据的主要来源。然后我们给每个账户一个独立的 SQLite 数据库,同样靠 Litestream 在对象存储上保持持久化。
决策 #3:由内而外的编排
在云托管行业里,用户应用由两个各自独立但同等重要的组件管理:负责编排工作负载的 host,和负责运行它们的 guest。Sprite 把它颠倒过来:最重要的编排和管理工作发生在 VM 内部。
Here's the thing: the user code running on a Sprite isn't in the root namespace. We've wedged a container between you and the kernel. What you see is an inner environment, managed by a set of services running in the VM's root namespace.
I wish we'd built Fly Machines this way from the start. I don't see a downside. The inner container lets us restart a Sprite without rebooting the whole VM, even from a checkpoint restore. I think Fly Machines users would get a lot out of this too.
On Sprites, we pushed this idea to its limit. The root environment hosts most of our orchestration code. When you talk to the global API, you're probably talking directly to your own VM. On top of that:
- Our storage stack lives there, handling checkpoint/restore and persistence to object storage;
- So does the service manager we expose to Sprites, which registers the user code that needs to restart alongside the Sprite;
- Logging, too;
- If you bind a socket to
*:8080, we make it reachable from outside the Sprite — yep, that's in the root namespace as well.
Anyone building on Fly.io knows that changing init (inside the container) is far easier than changing something like flyd — the Fly Machines orchestrator running on the host. Changes to Sprites don't restart host components or disturb global state. The blast radius is limited to the new VMs that pull the change. We lose sleep not because the code is hard to write, but because there's so much platform work left undone — making sure seemingly harmless changes don't send the whole cluster into a metastable failure takes forever. We kept that front of mind while building Sprites.
We kept what worked
Sprites on Fly.io build on infrastructure we already had. For example: if you want Claude or Gemini to build a full-stack app on the internet, Sprites might be the fastest way that exists today.
That's because Sprites plug directly into Corrosion, our gossip-based service discovery system. When you ask the Sprite API to generate a public URL for your Sprite, we generate a Corrosion update that propagates across our entire cluster in an instant. Your app then gets served over an HTTPS URL through our proxy edge nodes.
Sprites 与 Fly Machines 在我们的架构中并存。它们包含一些纯粹是改进的改动,但大多是权衡取舍:
- 我们一直希望让 Fly Machine 磁盘跑在对象存储上(我们有一个冷门的 LSVD 功能可以做到这一点),但性能不足以支撑生产环境中的热 Postgres 节点。
- 正因如此,专业生产应用是以 OCI 容器形式从 CI/CD 系统发布的;这也是编排 Fly Machines 如此困难的一大原因。
- 大多数(尽管不是全部)Sprite 使用是交互式的,Sprite 用户受益于其虚拟机主动休眠以保持低成本;电商应用以毫秒衡量响应速度,希望工作负载保持热态。
Sprites 针对的是一种不同于 Fly Machines 的计算类型进行优化,虽然 Kurt 相信未来属于可塑的、个性化的应用,我并不那么确定。在我看来,在 Sprites 上做原型开发和验收测试是合理的。然后,当你满意后,把它容器化并作为 Fly Machine 发布以扩展。实现这一点的自动化工作流将会出现。
最后,Sprites 是与用户代码的一份契约:一个 API 以及一组关于执行环境如何工作的预期。今天,它们运行在 Fly Machines 之上。但它们不必如此。Jerome 正在开发一个开源的本地 Sprite 运行时。我们也会找到其他运行它们的地方。
不用就不会懂
我没办法不听起来像个推销员。Sprites 是我们发布的唯一一个我个人觉得上瘾的东西。我还没完全想清楚为什么现在启动项目感觉容易多了——我可以打个响指就得到一台全新的电脑。关键在于,没有理由把它们分割开来,或者决定哪些代码该在哪里运行。你只要新建一个就行。
所以,为了让你完全理解,我觉得你应该直接安装 sprite 命令,创建一个 Sprite,然后在里面运行一个 agent。我们预装了 Claude、Gemini 和 Codex,并教会它们做诸如 checkpoint/restore、注册服务和获取日志之类的事情。Claude 会在 --dangerously-skip-permissions 模式下运行(因为为什么不呢)。让它构建点什么;我为某个 Slack 频道构建了一个“芝加哥最佳三明治”对阵图应用。
精灵只按你实际用到的部分计费(具体来说:只算你真正写入的存储块,而不是 100GB 的完整容量)。多开几个完全合理。它们就是一次性圆珠笔式的计算机。用顺手之后,身边没有它们反而会觉得别扭。