Geoffrey Huntley 对谈 unikernel 重提,AI 加持下过去难产的 unikernel 一周可搭出
Unikernels were hard. key word: were
作者 Geoffrey Huntley 与 Justin Cormack 长谈 unikernel 重提,论证 AI 已能补齐过去稀缺的库与工具链。
作者演示用 AI 一周补齐 unikernel 缺失的库与工具链,给出 Spaceleans 与 Cursed 等可复现路径。
早些年曾参与 MirageOS 和 Unikernel Systems 工作的 Justin Cormack,也注意到了我所注意到的现象:人们正在(重新)发现 unikernel。他正在为他的 newsletter 推出一系列相关对话,而我是第一位。他给我发了邮件,五分钟后我就端着一大杯啤酒在酒馆里跟他聊了起来。我们深入探讨了 Mirage、Orleans、Haskell、Nix、Cursed 等诸多内容。
以下是其要点,整理成文,底部附有章节链接,方便你跳转到对话的特定部分。Justin 编辑过的对话实录见 Ignore Previous Directions。
Unikernel 曾经很难。关键词是「曾经」。现在我们有了 AI。
什么是 unikernel,以及它们为何曾经很难
我大约在 2015 年第一次接触到 unikernel。当时我组建了一支 Haskell 程序员团队,并在函数式编程上钻研得很深。有 Haskell 程序员的地方,就有 OCaml 程序员,而从那里你就会发现 MirageOS。这个想法很棒,我那时也玩过。
Unikernel 的核心理念是,你的应用就是操作系统。没有用户空间。如果你想运行 Web 服务器、DNS,或者发送电子邮件,没有任何东西可以 fork 或 spawn。你必须把这些功能作为应用中的库来编写。
这就是当时的阻力所在。Justin 对此记忆犹新:他们当初在构建 Mirage 时,已经有了 TCP 协议栈和 HTTPS 协议栈,但几乎没有存储相关的组件。他们不得不从 NetBSD 中抽取驱动,因为可以在用户空间里运行它们。那时候确实很难。
我们这个行业里有很多教条。Nix 难。Bazel 难。Unikernel 难。是的,它们曾经很难。这些困难的概念如今都已存在于模型权重里了。你只需要用提示词把它们调出来,并在认知上摆脱它们很难的成见。
操作系统是设计债
每一个非 unikernel 的应用,其构建都基于这样一个假设:先有一个应用,然后下面有一个操作系统。我们为什么要有操作系统?因为四十年前存在人类操作员。我用过 IBM 5250、AIX、Solaris 以及大型机。多用户操作系统的存在,是因为曾有人坐在它面前操作,之后我们才把应用放到它上面。
我认为那是设计债,这也是它当下很重要的原因。应用会被攻破。在 AI 出现之前应用就已经在被攻破了。有人攻破了用户态应用并拿到一个 shell。那个 shell 对数据外泄来说,简直就像 VIP 管家服务一样方便。
采用 unikernel 后,攻击面要小得多。如果某个功能不在应用(即操作系统)中,那么攻击者就无计可施——没有下一跳可利用。
Justin 在这里提出了合理的反驳。对于攻击面缩减,人们的理解其实很模糊。你可以从 Linux 容器中移除 shell,但几乎每个 Linux 环境里仍然有某种实际上等同于解释器的东西。即使没有可写文件系统,你也能执行一个新程序。你还得担心内存安全和 gadget 的问题。
都是事实。但看看我们这二十六年来一直在做的事情。我最早从 SunOS 和 cgi-bin 时代记住的那句格言是「别把编译器放到生产环境里。」后来出现了构建容器和生产容器。再后来是 Chainguard。我们一直在不断地削减攻击面,而不是反其道而行,确保根本没有攻击面。
而这正是大家忽略的地方:如果没有 shell 也没有解释器,模型权重里就没有任何东西知道下一步该做什么。这把一次顺手的攻击(随便挑一个本周框架的 RCE,你就能拿到 shell,而模型权重知道拿到 shell 访问权限后该怎么操作)变成了一次需要你源代码的定向攻击。
现在你可以直接移植缺失的库
经典的反对意见是:你的 unikernel 需要和 Stripe 通信,而 OCaml 没有 Stripe 库。在 AI 出现之前你只能叹气自己写。现在?跑一个循环把 Go 库移植到 OCaml。给你:unikernel 里的 Stripe。
Justin 有一个很好的例子。他一直在为设备构建极简的 Linux 操作系统镜像,这本来就已经是半个 unikernel 了,因为你只运行一个作为 PID 1 的应用程序。他需要做一个 XFS 文件系统。他没有去引入 xfsprogs 以及它带来的一切,而是坐下来和一个智能体一起,让它用 Rust mkfs.xfs 来实现,输出逐字节完全一致,每个参数都被正确解释。它一个一个地逆向工程了磁盘上的格式,并针对不同的块大小进行了测试。这花了几个小时。
这之所以有效,是因为原始工具是一个黄金标准。用两种实现生成不同大小的文件系统,然后比较它们的差异。把测试也移植过去。让它自动化。
移植软件已经变得轻而易举有一段时间了。下面是做法。
如果你有一个黄金标准,移植就是一个循环。
Geoffrey HuntleyGeoffrey Huntley
存储是另一个大缺口。如今大多数工作负载都是云形态的,即使是在本地部署也是如此,所以采用 turbopuffer 的方案:S3 作为主存储,可以无限扩容,再加一个本地 NVMe 块缓存,并用 LRU(或你喜欢的任何缓存算法)来处理热点数据。Justin 也是个狂热的"一切皆 S3"爱好者。只要延迟不是瓶颈,你就能获得可多用户访问的无限存储,而且可以在上面构建一切。
nix 机器测试和覆盖层
Justin 也在尝试 Nix,他惊讶地发现,第一次让智能体原型化一个操作系统时,它就把所有测试都构建进了 flakes。
Nix 这门语言很糟糕。Nixpkgs 很棒。NixOS 机器测试才是最厉害的地方:你写一个测试,启动一堆机器,测试你的网络规则和应用程序之间的交互。这才是大家不知道的部分。
当上游的东西坏了,或者你的依赖里有供应链问题时,那就是一个覆盖层的事。
世界还没有意识到,你实际上可以用一个 Nix 覆盖层修复一切
给世界打补丁。
Geoffrey HuntleyGeoffrey Huntley
如果你必须用工具调用一个真人,也就是所谓的"亲爱的维护者"——他可能正在度假,也可能已经放弃了这个项目——然后等一天、两天,甚至五分钟,那都不是 AGI。
我们这里构建的是递归式产品。智能体需要有能力以第一方源代码的形式修改世界,而不是以第三方打包二进制文件的形式。我们正在回到 contrib 目录和 Unix 补丁的时代。
如果你关心安全,你有两个选择
Justin 问 unikernel 还需要什么才能被人们发现。老实说,就是这个。我们培养了两代开发者,他们甚至不知道有这东西。
如果你真的非常关心安全,实际上只有两个选择:
- 你正在把卫星送入太空,所以你或许应该使用 seL4,一个经过形式化验证的操作系统。(我对澳大利亚政府解散那个团队的事仍然有点介怀。)
- 其他人都应该认真考虑 unikernel。别再试图加固那些极易被攻破的东西。反过来,从另一个方向进行设计。
另一个经典的批评是,许多早期 unikernel 设计把所有东西都跑在同一个特权级别:你的应用程序和操作系统处于同一个 ring。在 2026 年,只要你想要 ring 隔离,那只是一句 prompt 的距离。这肯定比祈求你的 systemd cgroup 配置正确要安全得多。
想一想,每次 Linux 出现新东西,企业要花多少时间去给整个世界打补丁。上游现在期望你每周都打一次内核补丁。在我们录制的那一周,有人攻破了 KVM(基本上就是 Firecracker,我们都以为那是良好沙箱化的核心原语),从 Vercel 和其他几家厂商那里拿到了 5 万美元。对于一个可能让全球每家托管云服务商都被提权的东西来说,这笔钱并不多。
spaceleans:一个分布式 unikernel 操作系统
大约七个月前,我深入研究了 unikernel,以验证我的心智模型是否正确。我给 Justin 看了我的 Mirage 文件夹,里面装着我需要添加的所有功能。
- unikernel 集群没有办法同步时间,所以我从另一种语言里拿了一个 NTP 客户端并移植过来。然后我基于 RADclock 构建了一个 NTP 服务器,并借鉴了 TigerBeetle 处理时间的方式:不是一个时钟源,而是多个,打包成库的形式。
- 网络协议栈、DNS、HTTP 客户端和服务器、结构化日志、OTel、Anthropic 和 OpenAI 客户端,以及通过 Airwallex 进行的支付。
- 一个用于处理 HTTP 背压的通用重试库,外加一些用来玩的 PPX 元编程。
- 在日志边界上的 PII 包装器,这样密钥和 PII 永远不会通过日志子系统泄漏出去。每个项目都应该有一个。它是可插拔的;只需使用一个 functor。
然后是最疯狂的东西,我以前从没给任何人看过:Spaceleans,Microsoft Orleans 移植到 OCaml,作为 unikernel 运行。
Orleans 是一个支持事务的分布式 actor 系统。你把许多物理机器合并成一个可寻址的堆。一个 actor 始终存在:await GetCustomer(),如果它不在内存中,就会从一个可插拔的存储提供程序中重新激活。你把 n 层架构折叠成 actor,不再关心某样东西是跑在机器 A、B、C 还是 D 上。运行时把它当作一个基础设施原语来处理。
所以从某种奇怪的意义上说,我用 actor 构建了一个分布式 unikernel 操作系统,上面跑着一个文件系统。Justin 称之为 Erlang 式,他是对的。我用了一周时间就完成了所有这些。我可能永远不会发布它,但它证伪了 unikernel 很难这个说法。
采样历史
年纪稍大一点意味着你可以采样历史,就像一位经验丰富的 DJ,比如 Carl Cox,他在圈子里待得够久,可以从以前的曲库中抽取并向前推出。所有这些想法在八十年代就已经存在了。模型读过那些论文。它们的训练数据里有 TAPL 和最先进的类型理论。
缺少的是人们去做这些疯狂事情的好奇心和雄心,以及知道存在可以采样的前人记录。
我们怎么让人们去尝试这些东西?我们直接干就是了。如果你有一辆更高效、更安全的涡轮增压兰博基尼外星太空火箭,那随你;你就占了先机。做酷炫的事,吸引好奇的新人,指导他们,成长。和以前一直一样。
与此同时,其他人还在想着用 Chainguard 保护他们的 Ruby on Rails 应用、用五十位 AWS 认证工程师来管理 AWS,而两个人用 Nix 和 Hetzner 就能搞定。最终,归根结底还是钱。更强大的工具效率更高,而效率会胜出,尤其是在 AI 压缩利润的情况下。
ocaml、rust、haskell 与背压
OCaml 的时代又来了吗?它依然势头强劲。某家交易公司就把它用得很好。今年年初我和 Yaron 碰面时,我问他OxCaml的存在是不是为了让他们的语言扩展最终进入训练数据,从而带动整个公司。我得到了一个非常「无可奉告」式的微笑。
对智能体来说,OCaml 相当不错。模块之间的函子很美。.mli 文件——一个类型化的头文件,解释模块应当如何工作——对智能体而言是非常高效的上下文。opam 和 Dune 都相当出色。还有 Hindley–Milner 类型推断。编译速度也很快。我看不出现在还有什么理由去用 F#。
Justin 主要在写 Rust,智能体也很擅长 Rust。但编译时间就是 背压 的代价。LLM 会产生幻觉,而当编译很慢时,每一次幻觉都很昂贵,因为你每分钟能得到的尝试次数更少。Justin 的 S3 克隆大约有一百万行 Rust;四个智能体同时编译时,会互相争抢磁盘和 CPU。结果你在快速机器上的开销比花在 token 上的还多。
Haskell 的类型系统很棒,模型也确实做得很好。但我并不乐意把它跑在生产环境里:空间泄漏潜伏在运行时状态空间之中,只有到生产环境才会暴露出来。Justin 指出,Rust 中的线性类型思路正是出自那些为了解决这一问题的 Haskell 论文。还有 Zig 的做法:一次性把所有内存分配到位,此后再也不分配——这正是八十年代和九十年代游戏开发者所做的。不过,在 Rust 里很难说服智能体这么做,因为常量内存的程序不在它的训练集里。
我认为依赖类型是下一代语言的赢家。任何能把更多东西形式化到类型系统里的能力,都是更多的背压。你大概不会惊讶地听说,我有一个带依赖类型的 Rust 工具链分支。现在你就可以直接动手。
面向智能体的语言,以及 cursed 教会我的东西
语言发展的速度一直受限于人类学习新概念的速度。操作符链式调用本质上只是给人类吃的糖。如果由智能体来写代码,我们就可以借助四十年的学术 PLT 研究成果——前提是你知道怎么从中取样。
业界在 Python 2 升级到 3 之后确立了「不要做破坏性变更」的规矩。Justin 知道有些公司花了几百人、花了好几年才完成那次迁移。我认为这条规矩已经不再适用。随破坏性变更一起发布一个技能包,让智能体自动迁移就行。
Justin 问,现在把一门新语言做成功实际上要花多少钱。Go 是最后一门有公司真正砸钱去做的语言,而且花了很长时间。这个问题我能回答。
我用 Claude 跑了三个月的循环,它创造了一门叫 cursed 的 Z 世代编程语言
它是唯一一门让你可以用 sus、slay 和 vibes 写代码的编译型语言。
Geoffrey HuntleyGeoffrey Huntley
Cursed 是用 Sonnet 3.5 和 3.7 构建的,使用了一份故意写得不够具体的提示词,并让它循环跑了三个月。我一开始用 C(背压不够;agent 反复覆盖自己的更新,我在 Valgrind 上浪费了太多时间),然后换 Rust,再然后换 Zig。选 Zig 是个错误;如果当时坚持用 Rust,今天就能跑通。整体大约花了 6000 美元,而且我整个过程重做了三遍。对比一下 Go 的成本。
现在说真正让我至今后怕的部分。如果你正确分配上下文窗口——也就是用一张词法结构和语法的查找表——模型就能用它权重里没有的编程语言来编程。虽然粗暴又低效,但确实管用。
想想 T 型图(墓碑图)。锁定你的语法和词法结构,做到二阶段的自举编译器,发布一套合理的标准库,然后开启下一轮训练。从那里开始,你就能快得离谱地做出 Roslyn 风格、带语言服务的自举编译器。Justin 问过,微调一个开源模型是否有助于引导这样一种语言。用不着。
一年半以前模型还弱得多的时候就已经是这样了。只要有一位编程语言设计师全力以赴,拿上这些好模型,就能让全世界大吃一惊。
下一步
酒吧打烊的时候,Justin 问我最近在想什么。如果你还没读过我的最新文章,去读一下。如果你管人,现在就给团队腾出空间和时间去做试验,因为再过半年,领导层就会要求你对员工做活力曲线排名。
如果你的团队只顾忙'本职工作',无暇试验 AI,那你就是在替他们铺好被替换的路。
对就业能力而言,使用 AI 已是硬性要求。
Geoffrey HuntleyGeoffrey Huntley
是的,这些实验室确实是在公共数据上训练的。我讨厌这一点,也理解这一点。但现实是你拿时间和技能换钱;雇主有最低标准,而在我们这个行业,这些标准的变化速度前所未有。保持好奇,去学怎么构建一个 agent,然后去创造美好的东西。我们正身处一场文艺复兴。
它是一种时间压缩装置。经验越丰富,你能采样的东西就越多。不是所有东西都会发布;我给 Justin 展示的一些东西可能永远见不到天日。我把这些项目当作编程 kata 来练,等模型变强了就重做一遍。
但如果你想构建真正安全的东西,请认真考虑一下 unikernel。
章节
- 0:21 — 发现 unikernel:从 Haskell 到 OCaml 再到 Mirage
- 0:55 — 什么是 unikernel,以及它为什么难
- 2:45 — 难,已经是过去式了
- 3:33 — 我们为什么要有操作系统?
- 4:17 — shell 是为数据外泄服务的管家
- 5:12 — 攻击面缩减这件事其实很模糊
- 7:26 — 别把编译器放到生产环境
- 9:13 — 把 Stripe 移植到 unikernel 里
- 9:54 — 用 Rust 实现极简 Linux 与 mkfs.xfs
- 12:22 — 一切皆 S3
- 13:48 — Nix、机器测试与 overlays
- 15:28 — 调用一个人类并不等于 AGI
- 15:53 — seL4 还是 unikernels
- 16:56 — 特权环
- 18:24 — 每周内核补丁与 KVM 逃逸
- 19:38 — 演示:Mirage 目录、NTP 与时间
- 22:05 — Spaceleans:用 OCaml 把 Orleans 写成 unikernel
- 25:31 — 采样的历史
- 26:52 — 怎么让人来试:直接开干
- 28:57 — OCaml 与 OxCaml
- 31:45 — Rust 编译时间与背压
- 33:06 — Haskell、空间泄漏与依赖类型
- 35:13 — 内存策略:Rust、Zig 与游戏开发者
- 36:10 — 为智能体设计的语言
- 37:41 — 破坏性变更与技能包
- 38:53 — Cursed
- 42:36 — Cursed 教会我的事
- 46:47 — 接下来:AI 的使用是强制性的
保持好奇。
来源:Hacker News · ghuntley.com