砍掉 runc 与 shim:Kiri 如何用 10 MB 定制内核和单线程 Rust PID 1 将容器启动压进 2.55 毫秒
在桌面端跑容器,为什么一个最简单的 alpine 退出命令在 Docker Desktop 上要耗费 213 毫秒,在号称极速的 OrbStack 上也要 380 毫秒?最近我通读了在 GitHub 上迅速蹿红的 Rust 开源容器运行时 Kiri(名字源自伊博语 ntakịrị,意为“微小”)的全套源码。它用令人惊叹的系统工程克制给出了一份硬核答案:通过彻底砍掉 dockerd、containerd、containerd-shim 与 runc 四层传统抽象,配合 10.2 MB 极限裁剪内核与单线程 Rust PID 1 代理,Kiri 将 Docker Engine API 级别的容器全生命周期耗时压进了 2.55 毫秒,守护进程常驻内存更是压缩到 9.7 MB。
更让我意外的是,Kiri 不只是一套兼容 docker CLI、Compose、Buildx 与单节点 Kubernetes 的跨平台引擎(横跨 macOS、Windows WSL2 与原生 Linux),它在最新版本中还直接内置了零依赖的原生系统 WebKit 界面、38 种语言(含 RTL 排版)以及一套完整替代 Ngrok 的多通道公网隧道协议 kiri share。本文将从代码层面逐一拆解这套系统是如何在毫厘之间压榨出两个数量级的性能差距的。
[!NOTE] 阅读与基准说明
本文所有架构结论、数据指标与代码分析,均基于我拉取审查的libracoder/kiri仓库主干源码(包括crates/、guest/kernel/与bench/results.json)。文中基准数据均来自仓库内记录的同一台 Apple Silicon Mac(macOS 27.0.1)实机测量结果(同台dockerCLI 29.7.2,取 30 次运行中位数 p50)。
一、架构拓扑演进:为什么要彻底消灭 shim 与 runc?
传统桌面容器引擎之所以笨重,核心症结在于把服务端大型集群的抽象原封不动搬到了个人电脑的虚拟机里。在典型架构下,一条 docker run --rm alpine true 命令需要依次穿过:
- 宿主机 API 转发层:
docker CLI请求落到客户端守护进程,再转发进虚拟机内部; - 双守护进程层:VM 内常驻的
dockerd(Docker 引擎)与containerd(容器管理守护进程),两者仅在 Apple Silicon 上就常驻约 216 MB 内存; - 每容器代理层:每启动一个容器,
containerd都会拉起一个独立的containerd-shim进程专门负责托管 stdio、收割退出码。我在bench/results.json中看到一组惊人的实测数据:每个 shim 平均吃掉 7.9 MB 内存,143 个容器的 shim 进程仅空转就占用了 1133 MB; - 一次性运行时:每次创建容器都要冷启动一次 Go 编写的
runc二进制,反复进行fork/exec、命名空间切换、cgroup 配置与外部iptables/nft命令调用。
Kiri 的设计哲学非常彻底:既然桌面端虚拟机只由我这一个宿主机守护进程独占控制,为什么要保留服务端多租户调度留下的层层代理?
通过上面这张前后拓扑对比可以看到,Kiri 直接将架构收敛成“宿主机单守护进程 + vsock 零分配帧总线 + 虚拟机内单线程 Rust PID 1”的三段式直通链路:
| 核心对比指标 | Docker Desktop | OrbStack | Kiri(实测 p50) | 数据来源与说明 |
|---|---|---|---|---|
API 生命周期 (alpine true) |
213.4 ms | 379.9 ms | 2.55 ms | Bridge 网络;--network none 静默环境仅 1.26 ms |
完整 docker CLI 往返 |
243.3 ms | 442.2 ms | 14.15 ms | 其中绝大部分耗时是 Go 编写的 docker CLI 自身冷启动 |
| 守护进程常驻内存 | — | 216 MB (dockerd+containerd) |
9.7 MB | 宿主机 kiri 9.0 MB + VM 内 kiri-init 0.69 MB |
单容器 shim 内存开销 |
~8 MB/个 | 7.9 MB/个 | 0 MB | 无任何 shim 进程,全部由 PID 1 单循环统一监管 |
| VM 冷启动至 Agent 就绪 | — | — | 112 ms | 内核到 PID 1 仅 8 ms,PID 1 初始化仅 0.4 ms |
二、深入 Guest PID 1:单线程 poll() 与一次 clone3 的极限压榨
整个 Kiri 虚拟机内部没有 systemd,没有 dockerd,没有 containerd,甚至连 ip、nft、tini 这些外部二进制文件都不存在。所有职责全部压缩在 crates/kiri-init 编译出的 ~650 KB 静态链接 Rust 二进制(直接作为 Linux initramfs 里的 /init,即 PID 1)之中。
我在阅读 crates/kiri-init/src/linux/agent.rs 和 container.rs 时,发现作者做了一个极其反直觉、却堪称点睛之笔的架构决定:Guest Agent 全程保持单线程事件循环。
2.1 为什么单线程比异步/多线程更快、更安全?
在 Linux 内核编程中,当进程需要以 clone3 创建不共享内存空间(即不带 CLONE_VM)的子进程并进入全新命名空间时,多线程程序极易遭遇 fork() 锁死风险(其他线程持有的全局分配器锁或 stdio 锁在子进程中永远无法释放)。
kiri-init 利用这一物理特性,在 agent.rs 的 serve() 函数里用原生 libc::poll() 统一多路复用 14 类核心文件描述符:
- 宿主机控制通道:
vsock连接(Port 1024)与usage_wake异步磁盘统计唤醒管道; - 信号与生命周期:
signalfd(零延迟收割所有容器与exec子进程退出码,无需任何额外线程等待); - 网络与服务:DHCP 租约监听、内嵌 DNS 套接字、端口转发、SSH Agent 转发(
/run/host-services/ssh-auth.sock)、容器内嵌 Docker API 中继、copy复制服务、USB/IP 透传; - 内存压力感知:监听 Linux cgroup
memory.pressure的POLLPRI紧急事件,一旦 Guest 内存吃紧立刻向宿主机发送Msg::MemoryPressure请求动态扩容; - 所有活跃容器的 stdio:每个正在运行容器的
stdout/stderr管道与 PTY 主端,以及按需开启POLLOUT的stdin写入队列。
2.2 一发入魂的 clone3(CLONE_INTO_CGROUP)
在 crates/kiri-init/src/linux/container.rs 的 spawn() 函数中,Kiri 在创建容器前就已经把参数表、环境变量、挂载清单与 /run/kiri/<id> 下的 /etc/hosts、/etc/resolv.conf 全部在 tmpfs 内存中准备完毕,然后通过单次 clone3 系统调用一步到位:
// crates/kiri-init/src/linux/container.rs 核心容器拉起逻辑
let mut flags = (libc::CLONE_NEWNS
| libc::CLONE_NEWPID
| libc::CLONE_NEWUTS
| libc::CLONE_NEWIPC
| libc::CLONE_NEWCGROUP) as u64
| CLONE_INTO_CGROUP;
if spec.flags & run_flags::NET_HOST == 0 && prepared.net_join.is_none() {
flags |= libc::CLONE_NEWNET as u64;
}
let args = CloneArgs {
flags,
exit_signal: libc::SIGCHLD as u64,
cgroup: cgroup_fd.as_raw_fd() as u64,
..Default::default()
};
// 单线程环境无锁安全执行 clone3,直接诞生于目标 cgroup v2 与全套新命名空间内
let pid = unsafe { sys::clone3(&args) }.map_err(cleanup)?;
注意其中的 CLONE_INTO_CGROUP 标志:传统容器运行时往往先 fork/clone 出子进程,再由父进程去写 cgroup.procs 文件把进程挪进去,这中间不仅多出系统调用,还存在窗口期资源逃逸。Kiri 直接在诞生瞬间将其钉死在已配置好 --memory、--cpus 与 --pids-limit 的 cgroup v2 节点里,紧接着用 overlayfs 挂接镜像层并执行 pivot_root。
2.3 手写 AF_NETLINK 二进制协议栈与黑名单 cBPF Seccomp
为了让整个创建流程控制在毫秒级,Kiri 连最常见的网络配置与系统调用过滤也完全拒绝外部命令和臃肿库:
- 零外部工具的网络配置(
crates/kiri-init/src/linux/netlink.rs):作者直接手写了AF_NETLINK(NETLINK_ROUTE与NETLINK_NETFILTER)报文构造器,用几十字节的RTM_NEWLINK、RTM_NEWADDR、RTM_NEWROUTE和大端序nf_tables净荷直接在内核态构建网桥、veth 对、IPv6 子网与端口 DNAT/MASQUERADE 规则,连ip和nft命令都省了; - 极简黑名单 Seccomp 与
SECCOMP_FILTER_FLAG_SPEC_ALLOW(crates/kiri-init/src/linux/seccomp.rs):Docker 默认的 Seccomp 策略是一份包含三百多个允许项的巨大白名单,导致每次系统调用都要扫过冗长的 BPF 指令。Kiri 反向构建了一份精准的黑名单 cBPF 程序(拦截add_key、keyctl、init_module、bpf、io_uring_*、unshare等跨命名空间高危调用),更关键的是加上了SECCOMP_FILTER_FLAG_SPEC_ALLOW。正如源码注释中揭示的:如果不加这个标志,Linux 内核会为受过滤进程强制开启 Speculative Store Bypass 硬件缓解,在虚拟机里每次上下文切换都要触发一次昂贵的固件调用,直接给短生命周期容器平添 0.5 毫秒的纯损耗! - 内置微缩
--init(crates/kiri-init/src/linux/pid1.rs):当用户传入--init时,Kiri 无需在镜像里塞入tini,而是由容器内子进程自身接管信号屏蔽队列与sigtimedwait转发,并循环waitpid(-1, WNOHANG)收割孤儿进程。
三、10.2 MB 定制内核与 5 项外科级内核补丁
如果把 Guest 用户态压到了极致,虚拟机启动还是卡在 810 毫秒怎么办?答案在 guest/kernel/ 目录里。
Kiri 放弃了发行版动辄 32 MB 的 defconfig 通用内核,改为从 allnoconfig 起步,只编译这台极简虚拟机实际拥有的设备驱动(virtio-pci、virtio-net、virtiofs、vsock、ext4、overlayfs、nftables、cgroup v2),把内核镜像直接压到了 10,264 KB(约 10.2 MB)。配合内核启动参数 rcupdate.rcu_expedited=1(加速 RCU 宽限期,将 cgroup2 挂载耗时从 23 ms 骤降至 0.1 ms)和 swiotlb=noforce(省去无用的 64 MB virtio 弹跳缓冲区),以及 5 项极为精准的内核源码补丁,冷启动耗时一路腰斩:
当我点开 guest/kernel/patches/ 下的 5 个 .patch 文件时,不得不佩服作者在底层踩坑与定位根因的功力——每一行改动都直击 macOS 虚拟化与现代 Linux 生态的真实痛点:
0001-virtio-pci-poll-reset-with-hrtimer-sleep.patch(启动立省 ~30 ms):
Linux 主线驱动在重置virtio-pci设备时调用msleep(1)轮询等待。在HZ=250的内核时钟下,msleep(1)会向上取整休眠一整拍(4~8 毫秒)!由于 Apple 的Virtualization.framework总是会在第一次轮询之后才完成设备复位,挂载的每个 virtio 设备开机都要白白挨上一整个调度滴答。Kiri 将其改为高精度定时器usleep_range(10, 50),直接从虚拟机冷启动路径上剃掉了 ~30 毫秒。0002-virtio-net-big-tcp-as-gso.patch(解决端口转发软中断瓶颈):
Apple 的 NAT 网络层会把宿主机 TCP 报文聚合成最高 64 KiB 的超大包递给 Guest,但既不设 DF 位也没有标注 GSO 元数据。如果报文只是发往 VM 自身还算正常,一旦通过docker run -p转发进容器 veth 网卡,Linux 内核会把它硬生生切碎成 ~44 个 MTU 大小的 IP 分片再由容器重组,瞬间把单核软中断打满、吞吐卡死在 ~9 Gbit/s。Kiri 在virtnet_receive_done()中新增virtnet_kiri_big_tcp(),识别出这类 64 KiB 聚合 TCP 包并打上SKB_GSO_TCPV4与gso_size = dev->mtu - hlen标记,让数据包顺畅地以 GSO 大包形态穿过网桥与 veth。0003-fuse-create-owner-readable.patch(修复 Nodefs.cpSync报错):
macOS 的virtiofs服务端在执行lookup时要求拥有文件读权限。当容器内(例如libstdc++的copy_file或 Node.js 的fs.cpSync)在绑定挂载目录上以0200(只写不可读)权限调用open(O_CREAT, 0200)时,Apple 的 virtiofs 会直接抛出EACCES并留下一个空残骸文件(Lima 的 vz 虚拟机也有同款 Bug)。补丁让内核在FUSE_CREATE时先附加所有者可读位,打开后再立刻用fuse_do_setattr恢复调用方请求的真实权限位。0004-gro-frag-list-gso-size.patch:
引入了上游关于 GROfrag_list合并的修复,防止线性部大于gso_size的聚合包在转发分段时触发skb_segment()内核BUG_ON崩溃。0005-binfmt-elf-hwcap3-only-when-set.patch(拯救 macOS 15 上的 Rosetta x86_64 容器):
Linux 6.13 及更高版本会在启动任何 ELF 程序时向辅助向量写入AT_HWCAP3(类型 29),哪怕它的值是 0。然而 macOS 15 自带的 Rosetta for Linux 只要遇到不认识的辅助向量类型就会直接报错终止(rosetta error: unhandled auxillary vector type 29),导致所有linux/amd64容器全军覆没。由于 Apple 虚拟化并不提供 MTE/LSFE 特性,ELF_HWCAP3恒为 0,Kiri 加上了if (ELF_HWCAP3)判断,只在非零时注入该字段,从而完美保住了 Apple Silicon 上的跨架构运行能力。
四、宿主机工程品味:vsock、objc2 内存气球与 Unix Socket NFSv3
除了 Guest 内部的狂暴优化,宿主机侧(crates/kiri、crates/kiri-vmm、crates/kiri-proto)的工程细节同样堪称教科书级别。
4.1 零安全隐患的 no_std 通信总线(crates/kiri-proto)
宿主机与 Guest 之间通过 Virtio vsock 通信,实测往返延迟 p50 仅 31 微秒、p99 为 62 微秒。整个协议栈 crates/kiri-proto 强制声明 #![forbid(unsafe_code)] 且 #![cfg_attr(not(test), no_std)]:
- 稳态零分配:采用
len: u32 LE | kind: u16 LE | payload紧凑二进制帧,编码写入调用方复用缓冲区,解码直接借用字节切片; - 大流量直通:拉取镜像层时,在发送
Msg::LayerBegin帧后直接向流里裸写size字节原始 Tar 流,彻底跳过帧封装与内存拷贝; - 4 MiB stdin 信用流控:通过
Msg::InputConsumed回执实现背压(INPUT_WINDOW = 4 << 20),防止慢消费者把 Guest 内存撑爆; - 跨系统硬件 Bug 绕过:针对
docker cp(COPY_PORT = 1028),协议专门设计了copy_flags::CHUNKED分块标记——因为源码注释指出,macOS 14 Intel 上的Virtualization.framework会把 vsock 的半关闭(shutdown(SHUT_WR))错误地透传成全关闭,导致 Guest 回写CopyDone时触发EPIPE。
4.2 为什么“60 秒闲置关机”比纯内存气球更聪明?
在 crates/kiri/src/daemon/engine.rs 中,Kiri 使用了 Apple 的 VZVirtioTraditionalMemoryBalloonDevice 动态收紧 Guest 内存(保持实际使用量 + 256 MB 裕量)。但作者在代码第 730 行的注释里道出了一个残酷的 macOS 底层真相:
[!IMPORTANT] 内存归还真相(出自
crates/kiri/src/daemon/engine.rs)
Apple 的气球设备(Balloon)虽然能从 Guest 回收内存页面,但并不会把这些物理内存真正释放归还给 macOS 宿主机! 唯一能将全部内存彻底吐回给 macOS 的方式就是关闭 VM。
正因为 Kiri 把冷启动压到了 112 毫秒(加上状态恢复约 0.15 秒),它才敢于设计出“无活跃容器与 API 请求超过 60 秒即自动关机睡眠”的激进策略。平时不跑容器时,VM 占用 0 MB 内存,仅宿主机 kiri daemon 以 ~9 MB 静默守候在 Unix Socket 上;下次敲下 docker ps 或 docker run 的瞬间,VM 在百毫秒级悄然醒来,用户几乎毫无体感延迟。
4.3 零内核扩展的 Finder 容器文件挂载:Unix Socket 上的 NFSv3
如何在不安装 macOS 内核扩展(kext)、不需要管理员权限、也不用 FUSE 的前提下,让用户直接在 Finder 的 ~/Kiri 目录里实时浏览所有运行中容器的文件系统?
Kiri 的做法是在 crates/kiri/src/daemon/nfs.rs 中从零手写了一个轻量级只读 NFSv3 + MOUNT 协议服务器,直接监听在用户私有目录的 Unix Domain Socket(~/.kiri/files/nfs.sock)上,并调用 macOS 原生的 mount_nfs 进行用户态挂载!
为什么不用本地回环 TCP(127.0.0.1)而要坚持走 Unix Socket?因为在同一台 Mac 上任何本地账号都能访问回环端口并伪造 NFS 请求里的 UID;而在 Unix Socket 上,Kiri 可以通过 libc::getpeereid() 让操作系统内核亲自校验对端连接者是不是 root(macOS 的内核 NFS 客户端身份)或当前登录用户本人。同时,由于 APFS 默认大小写不敏感,~/Kiri 与仓库克隆目录 ~/kiri 撞名,挂载前还会严格检查目录是否为空或仅包含旧 Kiri 残留挂载,绝不覆盖用户的个人代码目录。
4.4 原生 WebKit 界面与 NameConstraints 受限本地 CA
- 拒绝对标 Electron 的臃肿:
crates/kiri/src/ui/直接通过objc2驱动 macOS AppKit 的WKWebView(Linux 上走WebKitGTK,Windows 走WebView2),前端仅是一个内嵌在二进制里的单文件index.html,没有打包一整套 Chromium,却实现了实时容器日志搜索、嵌入式xterm.js终端、多语言实时切换(38 种语言,包含阿拉伯语、波斯语与中库尔德语等原生 RTL 排版)。 - 防泄漏约束的本地 HTTPS(
crates/kiri/src/daemon/tls.rs):为了让https://<name>.kiri.local开箱即用,Kiri 在~/.kiri/tls生成了一张本地根证书。但为了防止私钥万一泄露被用于签发任意公网网站或 IP 证书,作者在rcgen生成 CA 时严格写入了 X.509NameConstraints——permitted_subtrees仅允许kiri.local与kiri.localhost,并用excluded_subtrees显式封死了全部 IPv4 (0.0.0.0/0) 与 IPv6 (::/0) 网段!
五、杀手级彩蛋:kiri share 如何用纯 Rust 做出免费的 Ngrok 替代品?
在分析 crates/kiri-share-proto 和 crates/kiri/src/share.rs 时,我发现了 Kiri 除容器引擎之外的另一大王牌:极简内网穿透。只需在终端敲下 kiri 3000 或 kiri share <容器名>,就能立刻为本地端口或容器生成一个 https://<name>.runkiri.cloud 公网加密入口。
这套自研隧道协议同样保持着极高的工程纯度(#![forbid(unsafe_code)] + no_std):
- 单端口 ALPN 智能分流:客户端连接
edge.runkiri.com:443时在 TLS 握手中携带ALPN: kiri-share/1,边缘节点凭此在同一个 443 端口上无感知区分隧道控制连接与普通浏览器访客流量; - 抗滥用的构建证明机制(
crates/kiri-share-proto/src/attest.rs):官方发布版编译时会嵌入一枚 32 字节构建密钥(通过 XOR 掩码拆存防简单字符串提取)。当客户端设置FEATURE_ATTEST发起Hello时,Edge 下发一次性随机nonce挑战,客户端返回以"kiri-share attest v1"为上下文的HMAC-SHA-256证明(绑定当前nonce、Account Token 摘要与device_id长度前缀帧),既防止脚本恶意刷滥用流量,又支持单密钥版本吊销; - 多 TCP 通道条带化 + 滑动窗口防阻塞(
FEATURE_LANES+FEATURE_WINDOW):单条 TCP 长连接在公网丢包环境下极易受限速与队头阻塞困扰。若服务端在Welcome中下发了lanes > 0与lane_key,客户端可并行拉起多条附加连接发送Msg::Join,将单条流的数据切片为带递增序号seq与fin标志的Msg::Chunk(最大 32 KiB)分散到各条信道上并发传输,由 Edge 按序重组;同时每条虚拟流独立维持INITIAL_WINDOW = 256 KiB信用窗口流控,确保某一个慢速访客永远不会拖垮整条隧道。
六、跨平台哲学与实用体验边界
为了覆盖完整的个人开发者矩阵,Kiri 在另外两大桌面平台也没有妥协:
- Linux 原生运行:彻底不启动任何虚拟机,
crates/kiri-vmm/src/process.rs直接把kiri-init作为宿主机子进程运行在原生内核之上,同时提供打包完整的.deb/.rpm(含 Docker 工具链)以及纯引擎版kiri-lite; - Windows WSL2 专有命名管道桥(
crates/kiri-win):Windows 端最容易踩坑的是 WSL2 的 localhost 端口转发会吞掉 TCP 半关闭信号,导致docker run经常丢失进程末尾输出。Kiri 干脆绕过 TCP 中继,在 Windows 端暴露npipe:////./pipe/kiri,通过单条wsl.exe ... kiri bridge进程标准流把控制帧直接桥接进 WSL2 内的 Linux 引擎,彻底根除了半关闭丢字难题。
当然,在审读 docs/features.md 与 README.md 后,我也注意到了两个当前版本的客观限制,值得在上手前心里有数:
- 内置 BuildKit Builder 仍在路线图中:目前
docker build/buildx依靠在 Kiri 容器内按需拉起docker-container驱动的 BuildKit 容器实现(闲置 300 秒后自动暂停);在真正内置 Docker 原生 build 端点之前,若在 BuildKit 构建阶段用到--add-host name:host-gateway,需要暂时手动指定网关地址(如 Mac 下为198.19.248.254)或回退DOCKER_BUILDKIT=0; - macOS 休眠与 USB 透传:虽然 Kiri 已经支持将串口调试器、Zigbee 适配器或开发板通过 USB/IP 透传进容器并在设备重插后自动重连,但 macOS 系统睡眠时会自动断开 USB 总线供电,长时间跑烧录或串口监听任务需要配合
caffeinate -s保持唤醒。
七、总结与启示
通读 libracoder/kiri 的几十个核心 Rust 模块,我最大的感受是:现代基础软件的性能跃迁,往往不靠更花哨的并发技巧,而靠敢于删除“理所当然存在的中间层”的勇气。
当业界普遍把 dockerd + containerd + shim + runc + 通用发行版内核 当成不可动摇的行业标配时,Kiri 回归到操作系统最本质的原语——Linux 的 clone3、overlayfs、cgroup v2、AF_NETLINK、cBPF seccomp 与宿主机原生虚拟化框架——用几万行高内聚、零冗余的 Rust 代码与 5 个直击病灶的内核补丁,硬生生把容器冷启动打回了 2.55 毫秒的原生进程级体验。对于任何想深入学习 Rust 系统编程、Linux 容器底层隔离原语或跨平台桌面虚拟化工程的开发者来说,kiri 都是一座不可多得的宝藏代码库。
原文链接与参考资料
- Kiri 官方开源仓库:libracoder/kiri (GitHub)
- Kiri 官方网站与安装包下载:runkiri.com
- Kiri 架构设计与开发指南:docs/development.md (GitHub)
- Kiri 功能清单与已知问题列表:docs/features.md (GitHub)
- Kiri 实机基准测试数据明细:bench/results.json (GitHub)
- Guest 定制内核与 5 项外科补丁源码:guest/kernel/patches (GitHub)
- Guest PID 1 与容器隔离核心实现:crates/kiri-init/src/linux (GitHub)
- Share 内网穿透无线帧协议与构建证明:crates/kiri-share-proto (GitHub)