贾斯汀·科马克(Justin Cormack)曾在 MirageOS(MirageOS)和 Unikernel Systems(Unikernel Systems)工作,他和我都注意到人们又在发现或重新发现 unikernels。他在 Newsletter 做系列对谈,我是第一个;他发邮件后五分钟,我就在酒吧拿着啤酒和他聊起 Mirage、奥尔良(Orleans)、哈斯克尔(Haskell)等。要旨是:unikernels 曾很难,关键词是 were;现在有了 AI。什么是 unikernels、它为何曾很难?我约在 2015 年第一次接触,当时带着哈斯克尔团队深入函数式编程,并由 OCaml 程序员找到 MirageOS 试用;unikernels 指应用即操作系统,无用户态。
正文
Justin Cormack 当年曾在 MirageOS 和 Unikernel Systems 工作,他注意到了我也注意到的一件事:人们又开始发现(或者说重新发现)unikernel 了。他正在为自己的 newsletter 做一系列相关对谈,而我是第一个嘉宾。他给我发了邮件,五分钟后我就在酒吧里一手拿着一升装的啤酒杯跟他聊上了。我们深入聊了 Mirage、Orleans、Haskell、Nix、Cursed 以及很多别的东西。
下面是整理好的要点。Justin 的编辑版文字记录发布在 Ignore Previous Directions。
Unikernel 曾经很难。关键词是「曾经」。现在我们有 AI 了。
什么是 unikernel,以及它们为什么曾经很难
我第一次接触 unikernel 是在 2015 年前后。当时我组建了一支 Haskell 工程师团队,在函数式编程上钻得比较深。有 Haskell 人的地方就会有 OCaml 程序员,顺着这条线你就会发现 MirageOS。这是个很棒的想法,我当时玩过。
unikernel 的理念是:你的应用程序就是操作系统。没有 userland。如果你想要一个 Web 服务器、DNS,或者要发邮件,没有什么东西可以让你 fork 或 spawn。你必须把这些东西作为库写进你的应用程序里。
这就是摩擦所在。Justin 对此记得很清楚:他们在构建 Mirage 时,已经有 TCP 栈和 HTTPS 栈,但存储方面几乎什么都没有。他们把 NetBSD 的驱动搬过来,因为他们可以在 userspace 里运行这些驱动。那时候这很难。
我们这行有很多教条。Nix 很难,Bazel 很难,unikernel 很难。是的,它们曾经很难。这些难的概念现在已经在模型的权重里了。你只需要用 prompt 把它们调出来,并且在认知上丢掉「它们很难」这个教条。
操作系统是一种设计债
每个不是 unikernel 的应用程序,都是基于这样一个假设构建的:上面是一个应用程序,下面垫着一个操作系统。我们究竟为什么需要操作系统?因为四十年前有一个人类操作员。我搞过 IBM 5250、AIX、Solaris 和大型机。多用户操作系统之所以存在,是因为当时有个人坐在它前面,然后我们把应用程序跑在上面。
我认为这是一种设计债,而它现在之所以重要,原因如下:应用会被打穿。在 AI 出现之前它们就一直被被打穿。有人打穿 userland 应用,拿到一个 shell。那个 shell 就是一个为数据渗出服务的 VIP 管家。
用 unikernel 的话,攻击面要小得多。如果某个功能不在应用程序(也就是操作系统)里,攻击者就无从下手了,没有下一跳。
Justin 在这里提出了反驳,而且反驳得有道理。缩减攻击面这件事,大家的认识很模糊。你可以把 Linux 容器里的 shell 去掉,但几乎每个 Linux 环境里仍然有某种事实上的解释器。即使没有可写文件系统,你也能执行一个新程序。你仍然要担心内存安全和 gadget。
这些都没错。但看看我们过去二十六年在做什么。我还记得 SunOS 和 cgi-bin 时代最早的一句格言:“不要把编译器放到生产环境里。”然后是构建容器和生产容器,再然后是 Chainguard。我们一直在一点点削减攻击面,而不是往相反的方向走,去确保根本不存在攻击面。
而这一点正是大家忽略的:如果没有 shell,也没有解释器,模型权重里就没有任何东西知道下一步该做什么。这就把一次顺手攻击(随便挑一个你这周用到的框架的 RCE,你就能拿到 shell,而模型权重知道拿到 shell 访问权之后该干什么)变成了必须拿到你的源代码才能进行的定向攻击。
现在你只要把缺失的库移植过去就行
经典的反对意见是:你的 unikernel 需要跟 Stripe 通信,而 OCaml 没有 Stripe 库。在 AI 之前,你会叹口气然后自己写。现在呢?跑一个 loop,把 Go 库移植到 OCaml。这样你就有了:unikernel 里的 Stripe。
Justin 有个很好的同类例子。他当时在为 appliance 构建最小化的 Linux OS 镜像,这本来就已经走到 unikernel 的一半了,因为你只把一个应用程序作为 PID 1 来运行。他需要创建一个 XFS 文件系统。他没有把 xfsprogs 和它带来的一大堆东西都拖进来,而是坐下来和 agent 一起,让它用 Rust 写了一个 mkfs.xfs,生成逐字节完全一致的输出,并且解释了每一个 flag。它逐个逆向出磁盘上的格式,还针对不同块大小写了测试。这花了几个小时。
这之所以可行,是因为原始工具是一个 golden oracle。用两个实现生成不同大小的文件系统,然后 diff 它们。把测试也移植过去。并把它自动化。
存储是另一个大缺口。如今大多数工作负载都是云原生形状的,哪怕在 on-prem 环境里也一样,所以可以采用 turbopuffer 的做法:把 S3 当作你的主要、可无限增长的存储,再配一个本地 NVMe 块缓存,以及一个 LRU(或者你喜欢的任何缓存算法)来放热点数据。Justin 也是「什么都放 S3」的忠实粉丝。只要延迟不是瓶颈,你就能获得无限存储和多用户访问能力,然后可以在此之上构建一切。
Nix 机器测试与 overlay
Justin 也在尝试 Nix,让他惊讶的是,他第一次让 agent 原型开发一个 OS 时,它把所有测试都构建进了 flakes。
Nix 这门语言很糟糕,但 Nixpkgs 很棒。NixOS machine tests 是个宝贝:你写一个测试,启动一整队机器,检验你的网络规则和应用程序之间的交互。这正是大家不了解的东西。
而上游什么东西坏了,或者你的依赖出现了供应链问题,那不过就是一个 overlay 而已。
如果你必须 tool-call 一个人类,也就是所谓的“Dear Maintainer”,而他可能在度假,或者已经弃坑了,然后你要等一天、两天,甚至五分钟,那就不是 AGI。
我们在这里构建的是递归式产品。agent 需要能够以第一方源码的方式去修改世界,而不是以第三方捆绑二进制的方式。我们要回到 contrib 目录和 Unix patch 的老路上去了。
如果你在乎安全,只有两个选择
Justin 问,要让更多人发现 unikernel,它们还缺什么。说实话,就是下面这点。我们已经培养了两代开发者,他们甚至不知道有 unikernel 这回事。
如果你非常在意安全,其实只有两个选择:
- 如果你要把卫星送上太空,那大概应该用 seL4,一个经过形式化验证的操作系统。(澳大利亚政府解散了那个团队,我到现在还有点耿耿于怀。)
- 其他所有人都应该认真考虑 unikernel。别再试图去加固一个非常容易被黑的东西。把它反过来,从另一个方向设计。
另一个经典批评是,很多早期的 unikernel 设计把所有东西都跑在单一特权级上:你的应用程序和 OS 在同一个 ring 里。到了 2026 年,如果你想要 ring 隔离,那也就是一个 prompt 的事。这肯定比祈祷你的 systemd cgroup 配置是对的更安全。
想想看,每当 Linux 里又冒出新漏洞时,企业要花多少时间去给全世界打补丁。现在上游期望你每周给内核打补丁。就在我们录制那一周,有人打穿了 KVM(本质上就是 Firecracker,我们都以为它是很好的沙箱原语),并从 Vercel 和其他几家厂商那里拿走了 5 万美元。对于一个可能 root 掉全球所有托管云厂商的漏洞来说,这点钱并不算多。
Spaceleans:一个分布式 unikernel 操作系统
大约七个月前,我深入研究了 unikernel,想验证一下我的心智模型是否正确。我给 Justin 看了我的 Mirage 文件夹,里面放着所有我需要补上的功能。
- 当时 unikernel 集群没办法保持时间,于是我从另一门语言里拿了一个 NTP 客户端移植过来。然后我基于 RADclock 构建了一个 NTP 服务器,并借鉴了 TigerBeetle 处理时间的方式:不是一个时钟源,而是很多个,打包成一个库。
- 网络栈、DNS、HTTP 客户端和服务器、结构化日志、OTel、Anthropic 和 OpenAI 的客户端,以及通过 Airwallex 的支付。
- 一个通用的重试库,用来处理 HTTP 上的 back pressure,外加一些好玩的 PPX 元编程。
- 在日志边界处加一个 PII wrapper,这样密钥和 PII 就永远不会通过日志子系统泄漏出去。每个项目都应该有一个。它是可插拔的;只要用个 functor 就行。
然后是最疯的东西,我之前从来没给任何人看过:Spaceleans,也就是把 Microsoft Orleans 移植到 OCaml,并作为 unikernel 运行。
Orleans 是一个带事务的分布式 actor 系统。你把很多台物理机合并成一个可寻址的堆。actor 始终存在:await GetCustomer(),如果它不在内存里,就会从一个可插拔的存储 provider 里重新水合。你把 n 层架构塌缩成 actor,不再关心某个东西是活在机器 A、B、C 还是 D 上。运行时把它当作基础设施原语来处理。
所以在某种意义上,我用 actor 搭出了一个分布式 unikernel 操作系统,上面再盖一层文件系统。Justin 说这很 Erlang 味,他是对的。我这一切只用了一周。我大概永远不会发布它,但它证伪了「unikernel 很难」这个说法。
采样历史
年纪稍大一点意味着你可以采样历史,就像 Carl Cox 那样经验丰富的 DJ,他在圈子里待得足够久,可以从以前的曲库里抽取元素并带到当下。所有这些想法在八十年代就存在了。模型读过那些论文,它们的训练数据里有 TAPL 和最先进的类型理论。
现在缺的是人们去做这些疯狂事情的好奇心与野心,以及知道还有前人留下的、可供采样的成果存在。
我们怎么让人们去尝试这些东西?直接去做就行了。如果你有一辆更高效的、更安全的外星涡轮兰博基尼太空火箭,那很好,你已经领先了一步。做酷的东西,吸引好奇的新人,指导他们,一起成长。这一直都是如此。
与此同时,其他人会忙着用 Chainguard 加固他们的 Ruby on Rails 应用,并雇五十个通过 AWS 认证的工程师来管理 AWS,而其实两个会用 Nix 和 Hetzner 的人就够了。最终,这归结为钱。更强大的工具效率更高,而效率会赢,尤其是当 AI 正在压薄利润的时候。
OCaml、Rust、Haskell 与 back pressure
OCaml 的时代又回来了吗?它依然坚挺。某家交易公司用得非常好。今年年初我和 Yaron 碰面时,我问他 OxCaml 的存在是不是为了让他们的语言扩展进入训练数据,并提升整个公司的水平。我得到了一个非常「无可奉告」的微笑。
对 agent 来说,OCaml 很可爱。模块之间的 functor 很美。.mli 文件,也就是解释一个模块应该如何工作的带类型的头文件,对 agent 来说是极其高效的上下文。opam 和 Dune 确实很好。Hindley–Milner。而且编译时间很快。我看现在没什么理由再去用 F# 了。
Justin 主要一直在写 Rust,而 agent 很擅长 Rust。但编译时间是对 back pressure 征收的税。LLM 会产生幻觉,而当编译很慢时,每一次幻觉的代价都很高,因为你每分钟能尝试的次数变少了。Justin 的 S3 克隆大约有一百万行 Rust;当四个 agent 同时编译时,它们会争抢磁盘和 CPU。最后你在快速机器上花的钱比在 token 上花的还多。
Haskell 的类型系统很棒,模型也做得很好。但我在生产环境跑它时感觉不太好:space leak 活在运行时状态空间里,而且只在生产环境才暴露出来。Justin 指出,Rust 里的线性类型思想来自 Haskell 的论文,正是为了解决这个问题。然后是 Zig 的做法:一开始就把所有东西都分配好,之后再也不分配,这正是八九十年代游戏开发者的做法。不过,很难说服 agent 在 Rust 里这么做,因为常量内存程序不在它的训练集里。
面向 agent 的语言,以及 Cursed 教会我的事
语言发展的速度,一直被人类学习新概念的速度拖累着。Operator chaining 本质上只是给人类吃的糖。如果代码由 agent 来写,我们就可以依赖学术界四十年的 PLT 研究,只要你知道怎么去采样它。
在 Python 2 到 3 之后,行业把「不要做破坏性变更」奉为圭臬。Justin 知道有些公司几百号人在那次迁移上耗了好几年。我认为这条规则已经不再成立。发布一个带有破坏性变更的 skill pack,然后让 agent 自动迁移。
Justin 问,现在让一门新语言真正成功到底要花多少钱。Go 是公司真金白银砸进去的最后一门语言,而且花了很长时间。这个问题我能回答。
Cursed 是用 Sonnet 3.5 和 3.7 构建的,用了一个刻意欠指定的 prompt,然后让它 loop 跑了三个月。我从 C 开始(back pressure 不够;agent 把自己的更新搞坏时,我在 Valgrind 上浪费了太多时间),然后是 Rust,然后是 Zig。选 Zig 是个错误;如果当初坚持用 Rust,今天就能跑起来。它花了大约 6000 美元,而我前后做了三次。对比一下 Go 花了多少钱吧。
现在说真正让我后怕的部分。如果你正确分配上下文窗口——一个词法结构和语法的查找表——模型就能用它权重里根本没有的语言编程。这是暴力且低效的,但它能工作。
想想 T-diagrams(墓碑图)。锁定你的语法和词法结构,做到 stage-two 自托管编译器,发布一个像样的标准库,然后启动下一轮训练。从那里出发,你能快得离谱地做出一个 Roslyn 风格、带语言服务的自托管编译器。Justin 问,微调一个开源模型是否有助于引导这样一门语言。不需要。
在一年半前,用那些弱得多的模型就已经是这样了。只需要有一个编程语言设计师全力以赴,配上现在的好模型,就足以震惊世界。
接下来会怎样
酒吧快打烊时,Justin 问我在想什么。如果你还没读我最新那篇文章,去读一下。如果你带人,现在就给他们创造试验的空间和时间,因为六个月之内,领导层就会要求你把人放到活力曲线(vitality curve)上。
是的,实验室是在公共资源上训练的。我讨厌这一点,但我理解。可你是在用时间和技能换钱;雇主有最低标准,而这些标准在我们这行变化得前所未有的快。保持好奇,学会怎么构建 agent,然后去创造美的东西。我们正处在一场文艺复兴里。
它是一个时间压缩装置。你经验越多,能采样的就越多。不是什么东西都会发布;我给 Justin 看的一些东西可能永远见不到天日。我把这些项目当作 kata,等模型变强了再重做一遍。
但如果你想构建安全的东西,认真考虑一下 unikernel。
评论
讨论气氛务实且带怀疑:多数人承认 unikernel 与微虚拟机在安全隔离和启动性能上有吸引力,但分歧主轴是「消灭通用操作系统的通用攻击面,是否值得付出调试困难、自研基础组件和生态薄弱的代价」。少数人走得更远,认为极致安全应转向 FPGA/硬件逻辑,甚至有人用 Linus 与 Tanenbaum 的争论嘲讽作者,称其只有提到 seL4 时没说谎,即便赢了也还是输。
认为 unikernel 的安全价值在于消灭通用操作系统,而非单纯减少代码量
- 通用操作系统天生要运行很多程序,漏洞利用、操作系统命令注入和系统入侵才是数据泄露的主要入口;unikernel 只跑一个程序,直击的是操作系统本身的通用性问题,而不只是“代码更少”。在人工智能让虚拟机与容器逃逸变容易的语境下,也有人主张更应把隔离和安全做进运行时,而非继续依赖传统虚拟机与容器。(认可度:较多人) > “操作系统本身就是问题,因为它天生要跑很多程序,而 unikernel 只跑一个。”(@eyberg)
认为硬件辅助的微虚拟机已在性能和落地成本上具备实用吸引力
- Netlify、Unikraft 等转向 Firecracker 微虚拟机后启动极快,说明虚拟化已吸收大量硬件能力,甚至可能比在用户态自己管理工作负载更轻;再加上已有极低成本的 unikernel 托管服务、嵌入式实时操作系统的瞬时启动,以及高频交易等本来就要做内核旁路的场景,单应用隔离不再只是理论优势。(认可度:较多人)
质疑每个应用自研文件/网络栈会重复造轮子,主张标准化库或中间形态
- 操作系统的意义就在于不必每次手搓文件系统和网络栈;如果每个 unikernel 都自己实现这些基础组件,会产生重复劳动和细微不一致,最终仍回到需要共享原语。更现实的折中是提供标准化、可插拔、跨应用复用的库,或在 unikernel 与完整 Linux 之间取中间形态,而不是非黑即白。(认可度:少数人) > “如果每个应用都自己写文件系统和网络,细微的不一致最终会让你回到需要共享原语的状态。”(@terabytest)
认为可观测性与调试能力是 unikernel 不可忽视的代价
- 单应用内存越界可能直接破坏网络栈,现有从数据线读取等调试方式并不实用;缩小攻击面很酷,但不该以牺牲系统可见性、调试能力和存活能力为代价。(认可度:少数人) > “降低攻击面很酷,但不该牺牲我对系统可见性和存活能力的掌握。”(@vsgherzi)
认为极致缩小攻击面应转向 FPGA/硬件逻辑,而非软件 unikernel
- 若目标是严肃的攻击面最小化,软件 unikernel(尤其大语言模型生成的)仍然太臃肿;更彻底的做法是像 Zynq UltraScale+ 那样把 ARM 与 FPGA 结合,把快速路径和高安全组件逐步放进硬件逻辑,而且大语言模型已经越来越会用综合工具。(认可度:少数人) > “真要极致缩小攻击面就得走向 FPGA/硬件逻辑,大语言模型写的 unikernel 还是太臃肿。”(@angry_octet)
有价值的补充
- 已有云服务以约 0.005 加元/小时提供 4MiB 内存、无磁盘、可联网的 unikernel 虚拟机(基于 BareMetal 内核),显示极小镜像和极低托管成本已经落地。
- 高频交易中的订单管理或 NYSE 买卖/撮合系统本来就做内核旁路并裁剪操作系统中断与抖动,unikernel 在此类场景可能替代部分 FPGA;但 DPDK、libfabric 等底层库仍依赖 Linux 以规范化方式与硬件交互,能否完全脱离 Linux 未有定论。
- Zephyr 这类嵌入式实时操作系统可与应用一起编译成极小镜像、启动近乎瞬时,并自带工具链、驱动和调试支持;但把 Nginx 等传统软件移植过去会非常痛苦,性能也不一定优于 Alpine 容器。
- 有人设想操作系统把每个程序绑定到单独处理器核心,核心用完就提示关闭程序,以消除上下文切换开销;这更像通用操作系统调度设想,与 unikernel 的单应用模型有呼应但并不等同。