Demikernel — 微秒级数据中心的 datapath OS 架构
是什么
Demikernel 是一套把不同 kernel-bypass I/O 硬件统一成同一套接口的 datapath OS 库。日常类比:就像旅行用的万能电源转接头——不管是英式插座、美式插座还是欧式插座,你的设备只插一个接口,转接头在中间帮你搞定一切。
在微秒(µs,1µs = 百万分之一秒)级别的数据中心里,Linux 内核的 I/O 路径太慢了:一次 read() / write() 系统调用加上上下文切换(即操作系统暂停当前程序、切换到另一个程序再切回来),轻轻松松耗掉 5–50µs,把整个 RPC 延迟拉高到毫秒级。
RDMA(远程直接内存访问:一台机器直接读写另一台机器的内存,不经过对方的 CPU 或操作系统)和 DPDK(Data Plane Development Kit:在用户态直接操控网卡驱动,完全绕过 Linux 内核的网络协议栈)等 kernel-bypass 技术可以把延迟压到 1–10µs,但它们各有私有 API,DPDK 写的代码换 RDMA 网卡就得全部重写。io_uring 则是 Linux 5.1 引入的高性能异步 I/O 接口,虽然还经过内核,但比传统 read/write 快得多。
Demikernel 的解法:在用户态实现多套 LibOS(Catnip = DPDK、Catnap = io_uring、Catpowder = raw sockets、Catcollar = Linux TCP),全部暴露同一组 demi_* 函数。应用只要链接不同的 LibOS 动态库,就能在不改一行代码的情况下切换底层 I/O 硬件。
// 同一份应用代码,编译时链接 catnip.so(DPDK)或 catnap.so(io_uring)let qt = demi_socket(AF_INET, SOCK_STREAM, 0)?;demi_bind(qt, &addr)?;demi_listen(qt, 10)?;let result = demi_wait(&token, None)?; // 非阻塞轮询为什么重要
不理解 Demikernel,下面这些事都没法解释:
- 为什么 Linux TCP 最快也要 50µs,而 RDMA 可以做到 1µs——内核 I/O 路径有多少隐藏开销
- 为什么 kernel-bypass 应用这么难移植:在 DPDK 跑通的代码换 RDMA 基本等于重写
- 为什么 LibOS 思路(Exokernel 1995 → Arrakis 2014 → Demikernel 2021)反复出现——OS 抽象和硬件多样性之间永远有张力
- 为什么微秒级应用必须用协程而不是线程——一次线程上下文切换本身就要 5–20µs
核心要点
Demikernel 的三个设计支柱:
-
统一 datapath API(
demi_*接口):与 POSIX 不同,Demikernel 的接口全部是异步非阻塞的。demi_push发送数据时不阻塞,返回一个”承诺令牌”(queue token);demi_wait轮询这个令牌直到完成。类比:就像在柜台取号排队——你不在窗口前傻站,而是拿了号就去喝茶,叫到号再回来取件。 -
协程调度器(单线程非阻塞事件循环):整个 LibOS 在一个核上跑,用协程而不是线程切换。没有锁、没有上下文切换、没有内核陷入。类比:一个服务员同时服务 100 桌,靠的是”端菜→下单→端菜”轮番做,而不是 100 个服务员各守一桌。这样 CPU 时间几乎全花在搬数据,而不是调度开销上。
-
零拷贝 scatter-gather 内存(
dmtr_sgarray_t):网络包到达后,数据直接落在应用内存里,不经过内核缓冲区。应用通过 scatter-gather 数组(指针 + 长度对的列表)访问数据,发送时也直接告诉网卡”从这几段内存 DMA 出去”,全程不复制一个字节。类比:快递公司不把你的货物倒进自己的仓库再发货,而是直接把货车开到你仓库门口装载。
实践案例
案例 1:把现有 Redis 迁移到 kernel-bypass
Redis 是一个高度 I/O-bound 的 key-value store,原始版本用 Linux TCP,尾延迟受内核 I/O 路径拖慢。论文用 Redis 作为真实应用的迁移案例之一。
迁移步骤:
- 将网络调用(
read/write)替换为demi_push/demi_pop - 把主事件循环改为调用
demi_wait_any轮询多个队列令牌 - 编译时链接
catnip.so(DPDK LibOS)
下方为示意代码(接口细节以 Demikernel GitHub 实际 API 为准):
// 原来的阻塞读ssize_t n = read(fd, buf, len);
// 迁移后:异步 push/pop + waitdemi_qresult_t qr;demi_sgarray_t sga = demi_sgaalloc(1, len); // 分配 scatter-gather 内存demi_push(qd, &sga, &token);demi_wait(&qr, token, NULL); // 非阻塞轮询,直到操作完成demi_sgafree(sga); // 发送完成后必须手动释放实验结果:论文报告迁移后延迟与手写 DPDK 代码相当(端到端 RPC P99 在 20–40µs 量级),相比 Linux TCP 有数倍改善。应用代码改动集中在 I/O 路径,业务逻辑不变。
案例 2:跨 I/O 硬件的 apples-to-apples 延迟对比
研究员想精确对比 RDMA vs DPDK vs io_uring 在同一工作负载下的尾延迟差异,但三套 API 差异太大,直接对比等于比”苹果和橙子”。
Demikernel 的统一接口让这个对比变得干净:同一份 echo server 代码,只改编译参数链接不同 LibOS:
# RDMA 版本gcc echo_server.c -o echo_rdma -L. -lcatnip
# io_uring 版本gcc echo_server.c -o echo_uring -L. -lcatnap论文数据显示:在 40GbE 网络上,RDMA(Catnip)端到端 RPC P99 约 19µs,io_uring(Catnap)约 40µs,而 Linux TCP 裸机约 120µs。三者在控制变量下的精确对比,第一次有了系统性数据支撑。
案例 3:构建多租户微秒级 RPC 框架
云服务商想构建一套 RPC 框架,要求:
- 开发环境用普通 Linux(io_uring,低成本调试)
- 生产环境自动切换 RDMA 或 DPDK(取决于机器配置)
- 同一份代码无缝迁移
架构:在构建脚本中检测硬件环境,动态选择链接哪个 LibOS:
ifeq ($(HW), rdma) LIBOS = catnipelse ifeq ($(HW), dpdk) LIBOS = catnip-dpdkelse LIBOS = catnap # io_uring fallbackendif
rpc_server: rpc_server.c $(CC) $< -o $@ -ldemi$(LIBOS)RPC 框架本身只写一次,运维人员通过环境变量控制底层 I/O 栈,开发者完全感知不到差异。
踩过的坑
-
协程模型要求全面非阻塞:任何一处阻塞调用(
sleep、同步文件 I/O、阻塞的malloc)都会卡死整个 LibOS 事件循环。迁移现有阻塞式应用时,必须找出所有阻塞点,改写成协程 yield 或 async 风格,工作量往往超出预期。 -
零拷贝内存有严格所有权协议:
demi_sgaalloc分配的内存在demi_push调用后就归 LibOS 所有,不能再访问;demi_pop拿到的 sgarray 必须手动调demi_sgafree释放。忘了释放 = 内存泄漏,释放后再用 = 未定义行为,和裸指针一样危险。 -
单线程无法用满多核:Demikernel 的协程调度器绑定在单个 CPU 核上,对于需要并行处理大量连接的场景,必须在应用层手动分片(每个核一个 LibOS 实例,连接按哈希分配到各核)。这把多核扩展的复杂度甩回给了应用开发者。
-
不同 LibOS 性能特性差异大,结果不能直接类比:Catnip(DPDK)在 40GbE RDMA 网卡上延迟 19µs,但换成 25GbE DPDK 网卡可能是 35µs;Catnap(io_uring)在内核版本低于 5.10 时某些操作 fall back 到同步路径,延迟剧烈抖动。跨版本、跨硬件的测试结果需要仔细标注环境。
适用 vs 不适用场景
适用:
- 微秒级 RPC、key-value store、消息队列等 I/O-intensive 延迟敏感服务
- 需要在多种 kernel-bypass 硬件(RDMA / DPDK / io_uring)上部署同一套应用
- 云服务商希望应用层和 I/O 硬件解耦,方便硬件迭代
- 系统研究:需要在受控条件下对比不同 I/O 栈的延迟基线
不适用:
- CPU-bound 工作负载(矩阵乘法、压缩、加密)——kernel-bypass 对 CPU 瓶颈没有任何帮助
- 需要 fork / exec / 多进程的传统 UNIX 应用——Demikernel 的 LibOS 不支持进程间继承文件描述符
- 开发和调试阶段——DPDK 需要专用网卡,开发机通常没有,用 io_uring fallback 时延迟特性与生产差异大
- 对内核安全隔离有强要求的多租户场景——用户态 LibOS 没有内核提供的进程隔离保障
历史小故事(可跳过)
- 1995 年:MIT 的 Exokernel 论文提出”内核只做多路复用,OS 功能全移用户态 LibOS”,但受限于当时硬件,实验性质居多。
- 2014 年:Arrakis 把 Exokernel 思路带入 DPDK 时代,证明用户态 I/O 在真实服务器负载下能达到 5–10x 延迟改善。同年 Minos(IX dataplane)也做了类似探索。
- 2019 年:Demikernel 在 HotOS 以 workshop paper 亮相,提出”多 I/O 栈统一接口”的思路,引发社区关注。
- 2021 年:完整版发表在 SOSP 2021,带来了完整 Rust 实现(早期原型用 C++)、多 LibOS 完整实验数据、以及 Redis 真实应用迁移案例。
- 2024 年:Junction(OSDI 2024)进一步把 Demikernel 思路推广到”把整个 Linux 进程透明地 kernel-bypass 化”,不需要修改应用代码。
学到什么
- 微秒时代需要重新设计 OS 抽象:当应用 SLO 目标是 10µs,内核系统调用的 5µs 开销就是不可接受的,整个 I/O 路径必须从根基重新设计。
- 统一 API 是正确的抽象层:硬件多样性(RDMA / DPDK / io_uring)不应该泄漏到应用层,一个”足够薄”的 portable datapath 接口能同时保留灵活性和可移植性。
- 协程 + 单线程比线程池更适合 I/O-bound µs 场景:消除锁竞争和上下文切换本身就能省下几微秒,而这几微秒在 µs 级系统里至关重要。
- Rust 是 LibOS 的好材料:零成本抽象 + 内存安全让 Demikernel 的零拷贝内存管理在没有 GC 暂停的前提下避免了 use-after-free 类型的 bug。
延伸阅读
- Demikernel GitHub 仓库(Rust 实现,含所有 LibOS 源码)
- SOSP 2021 论文展示视频(作者讲解,约 25 分钟)
- arrakis-2014 —— Demikernel 的直接前身,第一个在真实 DPDK 上验证 LibOS 性能的系统
- shenango-2019 —— 同期竞品,聚焦微秒级线程调度而非 I/O 硬件抽象
- exokernel-1995 —— LibOS 思想的起点,Demikernel 的”祖宗”
关联
- exokernel-1995 —— Demikernel 的思想直系祖先;Exokernel 1995 年就提出”内核只做多路复用,LibOS 做一切”
- arrakis-2014 —— Demikernel 的前身;Arrakis 在真实数据中心 workload 上第一次验证了用户态 I/O 的可行性
- barrelfish-2009 —— 另一条多核 OS 研究路线;Barrelfish 把每个核当独立计算机,Demikernel 把每个核绑定独立 LibOS 实例,思路相近
- shenango-2019 —— 同时期解决 µs 级调度问题的系统;Shenango 侧重核的动态分配,Demikernel 侧重 I/O 硬件抽象
- hyperkernel-2017 —— 同期内核设计研究,侧重形式验证而非性能;与 Demikernel 代表了 OS 研究的两个不同维度
反向链接
- arrakis-2014 —— Arrakis 2014 — 让操作系统只管规则、硬件直接服务应用
- barrelfish-2009 —— Barrelfish / Multikernel — 把多核机器当成一个小型网络来设计 OS
- shenango-2019 —— Shenango — 每 5 微秒重新分一次核的中央调度器